Распределенная компиляция C/C++ проектов с помощью ICECC

    image

    … Работаете вы, например, над очень большим проектом. Проект реально очень большой, написан на C или C++, и его билд «с нуля» может занять несколько часов, да и сборка после каких-то фиксов или патчей тоже требует немало времени, особенно если изменения коснулись чего-то фундаментального или много где используемого. Вы запускаете компиляцию на своем десктопе, все ядра загружены, вентиляторы крутятся как бешеные, и при этом вокруг вас еще десяток машин ваших коллег, которые по сути дела простаивают. Нехорошо.

    Или, например, сделали вы себе в команде билд-сервер, на который можно отправлять задачи для сборки. Но если вдруг запустить компиляцию нужно будет сразу нескольким людям, сервер захлбенется, и ждать все равно придется долго. Вы добавляете второй или даже третий билд-сервер, распределяете сервера между членами проекта, но в итоге мы можем получить аналогичную ситуацию: один сервер максимально напрягается, пытаясь прожевать одновременно несколько сборок, другие простаивают. Можно, конечно, заранее смотреть, какой из серверов менее загружен, и запускать задачу именно на нем… Но лучше все-таки автоматизировать этот процесс, а в идеале — автоматически распараллелить компиляцию, используя все доступные ресурсы.

    Если даже отвлечься от суровой энтерпрайзной работы, подобные мысли могут приходить в голову и при уютном пилении какого-нибудь хобби-проекта: ваш компьютер активно что-то компилирует, а рядом лежит лаптоп с тоже мощным процессором, и было бы неплохо поделиться с ним работой.

    И в этом всём может помочь ICECC.

    На хабре про него упоминали только один раз мельком в статье в блоге PVS-Studio, поэтому я решил рассказать про него поподробнее.

    Где взять


    Во многих дистрибутивах пакеты уже есть в репах, например в Debian:

    sudo apt-get install icecc

    Настройка планировщика


    Координатор, он же планировщик, он же scheduler. Устанавливается на одном из узлов, и занимается распределениями задач компиляции между хостами.

    Редактируем конфигурационный файл icecc:

    sudo nano /etc/icecc/icecc.conf

    Нас интересует параметр ICECC_NETNAME. Присвоим ему любой строковой идентификатор, который будет общим для всех узлов в нашем кластере.

    ICECC_NETNAME="MYNET"

    Запускаем планировщик и настраиваем для него автоматический запуск при старте системы:

    systemctl start icecc-scheduler

    systemctl enable icecc-scheduler

    ICECC в своей работе использует порты TCP 10245 8765 8766 и UDP 8765, поэтому не забудьте разрешить их на фаерволе.

    Настройка клиентов


    sudo nano /etc/icecc/icecc.conf

    Нас интересуют следущие параметры

    # С каким приоритетом запускать задачи компиляции. Навряд ли мы хотим, чтобы чьи-нибудь чужие таски тормозили нам основную работу, поэтому ставим приоритет пониже

    ICECC_NICE_LEVEL="19"

    # Название кластера. Должно совпадать с тем, что мы задали для scheduler'а

    ICECC_NETNAME="MYNET"

    # Максимальное количество параллельно выполняемых процессов компиляции на узле. У меня на десктопе 8 ядер, соответственно я поставил значение 6, чтобы в случае чего остальные были свободны для основных процессов на машине

    ICECC_MAX_JOBS="6"

    # IP-адрес планировщика. Можно его не задавать, и тогда ICECC попробует найти его с помощью броадкаста по сети — для этого и нужен был 8765 UDP порт. Однако лучше зададим адрес явно.

    ICECC_SCHEDULER_HOST="192.168.0.1"

    # А вот с этой опцией поаккуратнее. Она позволит быть хитрой собакой на сене, и не разрешать запускать на своей машине чужие задачи. Свои на чужих — можно, чужие на своей — нельзя :)

    #ICECC_ALLOW_REMOTE="yes"
    

    После этого перезапускаем демон

    sudo service iceccd restart

    И всё готово.

    Как оно работает


    Чтобы начать компилировать код не локально, а в ICECC-кластере, нужно не вызывать компилятор как обычно, а использовать соответствующий бинарь ICECC:

    /usr/lib/icecc/bin/cc
    /usr/lib/icecc/bin/c++

    Это можно явно прописать в Makefile или в генераторе-конфигураторе типа CMake, а можно, учитывая совпадающие имена бинарников, просто прописать путь к /usr/lib/icecc/bin в начало переменной окружения PATH, чтобы система обращалась туда в первую очередь, а дальше ICECC все уже разрулит сам:

    export PATH=/usr/lib/icecc/bin:$PATH

    Ну и не забываем про опцию "-j", чтобы явно указать, что мы хотим собираться не в один, а в несколько потоков.

    Сам процесс же будет проходить следущим образом:

    • Препроцессирование кода выполняется на локальной машине
    • Сгенерированные препроцессором .i-файлы передаются на узлы кластера, выбранные планировщиком, и компилируются там
    • Полученные в результате всего этого объектные файлы линкуются снова на локальной машине.

    После прочтения этого может возникнуть вопрос: а что если на разных узлах кластера у нас будут разные версии компилятора, или, что еще хуже, компилятор будет как-то специфически пропатчен? Ответ на этот вопрос просто: ICECC подготавливает специальный «бандл» из всего необходимого (компилятор, стандартная библиотека, вспомогательные компоненты) и закидывает его на узлы, которые будут выполнять вашу задачу.

    Поглядеть, как это происходит, и даже поучаствовать в процессе, можно командой

    icecc --build-native

    Которая выдаст нам что-то подобное:

    adding file /bin/true
    adding file /lib/x86_64-linux-gnu/libc.so.6
    adding file /lib64/ld-linux-x86-64.so.2
    adding file /usr/bin/gcc
    adding file /usr/bin/g++
    adding file /usr/bin/cc1=/usr/lib/gcc/x86_64-linux-gnu/7/cc1
    adding file /usr/lib/x86_64-linux-gnu/libisl.so.19
    adding file /usr/lib/x86_64-linux-gnu/libmpc.so.3
    adding file /usr/lib/x86_64-linux-gnu/libmpfr.so.6
    adding file /usr/lib/x86_64-linux-gnu/libgmp.so.10
    adding file /lib/x86_64-linux-gnu/libdl.so.2
    adding file /lib/x86_64-linux-gnu/libz.so.1
    adding file /lib/x86_64-linux-gnu/libm.so.6
    adding file /usr/bin/cc1plus=/usr/lib/gcc/x86_64-linux-gnu/7/cc1plus
    adding file /usr/bin/as
    adding file /usr/lib/x86_64-linux-gnu/libopcodes-2.30-system.so
    adding file /usr/lib/x86_64-linux-gnu/libbfd-2.30-system.so
    adding file /usr/lib/gcc/x86_64-linux-gnu/7/liblto_plugin.so
    adding file /usr/bin/objcopy
    adding file /etc/ld.so.conf=/tmp/icecc_ld_so_conf2sYxQI
    creating 7379e30774ea2e8efcda5f7614ac2fc2.tar.gz
    

    В итоге мы получаем файл, где есть все необходимое, и более того, в будущем, отправляя задачи на сборку, можно явно указывать, какой бандл будет использовать для компиляции.
    ICECC_VERSION=<filename_of_archive_containing_your_environment>

    С ICECC также возможна кросс-компиляция (когда, например, у вас узлы в кластере с разной процессорной архитектурой) и даже сборка проектов для embedded-систем. Всё это описано в документации.

    Мониторинг


    Если вы установили пакет icecc-monitor, то можно посмотреть, что происходит в кластере:

    icemon -n MYNET
    (где MYNET это названием вашей сети узлов).

    image

    Как видно, на мнемосхеме отображаются все сборочные машины и планировщик. Каждому узлу выдается свой цвет, а поскольку другое название ICECC — icecream, то называются цвета по сортам мороженого. Наведя курсор на узел, можно увидеть, что планировщик ICECC оценивает производительность и загруженность узла, что влияет на то, будут ли отправляться на него задачи и сколько.

    Благодаря цветовой дифференциации хорошо различимо, чьи задачи компиляции сейчас выполняются и на каких конкретно нодах.

    Заключение


    В целом, впечатления от ICECC только положительные. Понятно, что везде есть своя специфика, и все зависит от конкретной кодовой базы, но в нашем случае ускорение наблюдается вполне существенное (бенчарки не проводили, все субъективно), да и иметь дело с icecc одно удовольствие — заработал он буквально с пол-пинка :)

    Подробную документацию и исходники можно найти на Github проекта.
    Поделиться публикацией

    Комментарии 17

      +1
      Эх, сперва обрадовался, что еще одна замена distcc подъехала, а потом посмотрел и расстроился, что по факту поддержка тех же самых платформ.
      У кого-нибудь получалось distcc или ice завести под Cygwin, поделитесь?
      Хотя в первую очередь интересно, конечно, распределение сборки MSVC =)

      (Ясен пень, у меня есть моя система распределённой сборки Wuild, которая это все поддерживает, но интересны же конкурирующие решения)
        0

        Icecc основан на distcc, к нему по сути дела просто прикрутили выделенный планировщик и некоторые другие вкусности.


        На гитхабе пишут, что icecc запускается под WSL и даже пытается что-то билдить, но результат немного непредсказуем: https://github.com/icecc/icecream/issues/206

          0

          Incredibuild?

            0
            Нет, спасибо, у него вагон минусов. Я писал проект Wuild как замена Incredibuild прежде всего. Мы им пользовались в течение двух лет примерно. Ну и да, он мало того что платный, у них еще конская политика лицензирования)
            И да, у меня получилось сборку сделать быстрее чем с Incredibuild, в первую очередь в сценарии инкрементной сборки.
          0
          На мой взгляд, главный минус и distcc, и icecc — то что нет тесной интеграции с системой сборки (make/ninja). Т.о. при запуске make нужно заранее знать сколько потоков параллелить (а если сборка запускается на двух машинах/билдах, то по факту они обе могут драться за одно и то же количество удаленных потоков — просто писать -j 150 может быть совершенно бессмысленно. )
          А еще представьте, что у вас сборочный кластер например из 8 машин по 24 ядра, и 5 сборочных серверов, на каждом из которых может идти несколько сборок.
          Какой -j там передавать наиболее эффективно? это нерешаемая задача)
          make и ninja начинают адово тупить, если считают что компоновать надо в стопицот потоков…
          А если распределенную сборку интегрировать внутрь системы сборки, планировщик может учитывать как локальные ресурсы (препроцессинг и линковка), так и удаленные :)
            0

            Кстати да, интересная идея, иметь возможность запустить клиента со специальной опцией, чтобы тот спросил у планировщика, сколько ядер суммарно сейчас доступно в кластере и выдал это значение в stdout.
            А уже потом можно использовать это значение в билд-скрипте.
            Стоит задуматься о том как запилить подобное на досуге.

              0
              … и все равно это не учтет такой сценарий:
              допустим у нас есть 2 билда, 100 ядер в кластере, ну и проект ясен пень уже идеально распараллен, с использованием всех ядер проблем нет.

              QA ставят сборки почти одновременно;
              — на первом билде сборка стартует, «спрашивает» как вы говорите количество свободных ядер, передает 100 в make, сборка начинается, ядра отмечаются как занятые;
              — стартует второй билд, запрашивает удаленные ядра, их 0, поэтому сборка идет чисто локально (j 16 или j 32, сколько у нас там локальных ядер на билде)
              — первая сборка через полминуты сваливается с ошибкой компиляции (да или просто завершается потому что билдили небольшой проект), второй билд курит бабмук =)

              Еще раз говорю, чтобы максимально эффективно это делать, планировщик системы сборки и распределенной должен быть объединен =)

              И нет, я не выдумываю сценарии на ходу, я реально решал такие проблемы в своем проекте, когда идут релизы, сборки ставятся очень часто.

              p.s. вот например как я это с ninja интегрировал:
              github.com/mapron/Wuild/blob/master/3rdparty/ninja-1.8.2/src/build.cc#L730
                0
                а нельзя это сделать в докере? через марафон, например, у которого спрашивать о свободных ресурсах?
            0
            mingw-w64-i686 и mingw-w64-x64 в поддержке есть?
              0
              Мы под Win не собираемся, поэтому точно сказать не могу.
              Если верить тому, что обсуждают на SO, в свежих версиях должно сработать.
              0
              но в нашем случае ускорение наблюдается вполне существенное (бенчарки не проводили, все субъективно),

              Неплохо было бы все-таки бенчмарки провести. А то субъективное впечатление не всегда совпадает с реальностью.

              Помню, как-то на большом проекте выясняли, на что тратится время — и вполне ожидаемо, что на компиляцию исходников С++ ушло около 70%. Однако когда мы попробовали распределенную сборку, выяснилось, что таки да, компиляция успешно размазывается на несколько машин. Вот только на пересылку файлов туда-сюда система сборки тратит слишком много времени — особенно когда машин много, а сетка — стомегабитная. :)
                0
                Быстрая сеть (у нас 1G) и близкое физическое расположение узлов здесь важно, да.
                Возможно так же эффективность повысится при использовании jumbo-билдов, но у нас пока кодовая база к такому не полностью готова.
                  0
                  Стомегабитной сетки вполне достаточно для рапределённой сборки.
                  Я собираю дома через VPN 35 мб сеть, все равно локальные ресурсы используются полностью)
                  Итоговое время по wall clock ненамного выше чем при сборке из офиса (на 10-15%), ну а так, время исполнения одной задачи конечно выше, сетевой оверхед до 40-50% по времени доходит.

                  И да, я собираю дебажный билд, итоговый размер директории сборки около 5 Гб выходит (хотя если у вас проект в дебаге гигов по 100 имеет, как хромиум какой-то, то наверное да, скорость уже начинает дико решать).
                  0
                  Когда-то с icecc и еще некоторым шаманизмом, уменьшили время билда с 8 часов до 20 минут.

                  А сегодня хотелось бы узнать, есть ли аналогичный стабильный продукт для nodejs и java?
                    0
                    Я очень удивился, что ни в статье, ни в комментариях никто не упомянул систему сборки Google Bazel. На текущий момент она является наиболее взрослой и функциональной технологией для распределенной сборки мультиязыковых проектов с поддержкой шаринга результатов сборки среди разработчиков.

                    У меня на работе после настройки на проекте remote caching время сборки плюсового проекта на машине разработчика с нуля снизилось с 80 минут до всего 10 минут (это debug+release). По сути только копируются по сети объектные файлы с сервера CI.

                    Также можно настроить сборку на кластере, чтобы CI билдер собирал каждую ревизию существенно быстрее (насколько быстрее — не знаю, лично не щупал). Уже есть три open source реализации такого сборочного кластера — remote execution.

                    Распределенная сборка и кеширование результатов также работают и с другими языками. Например, databricks используют bazel для сборки проектов на scala, правда в в своей статье они упоминают только remote caching.

                      0
                      Спасибо, интересно, гляну обязательно, на Bazel не натыкался, и в моей статье про распределенную сборку тоже про него не упомянули)
                      Из минусов — то что надо писать новые правила, если проект уже имеет вагон cmake скриптов, портировать может быть затратно.
                      Я в основном затачивался под связку cmake+ninja, чтобы ничего в проекте менять не надо было)
                        0
                        Грустненько, что сразу расстроило — помимо переписывания проектов, конечно — нет никакой интеграции с MSVS (не Code) и XCode (вернее, уже для xcode есть beta. не затестил еще).

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое