Постоянные работники

На этой странице рассказывается, как использовать постоянные рабочие процессы, преимущества, требования и то, как рабочие процессы влияют на песочницу.

Постоянный рабочий процесс — это длительный процесс, запускаемый сервером Bazel, который функционирует как оболочка вокруг фактического инструмента (обычно компилятора) или является самим инструментом . Чтобы воспользоваться постоянными рабочими процессами, инструмент должен поддерживать выполнение последовательности компиляций, а оболочка должна выполнять перевод между API инструмента и форматом запроса/ответа, описанным ниже. Один и тот же рабочий процесс может вызываться с флагом --persistent_worker и без него в одной и той же сборке, и он отвечает за правильный запуск инструмента и взаимодействие с ним, а также за завершение рабочих процессов при выходе. Каждому рабочему экземпляру назначается (но не chroot) отдельный рабочий каталог в <outputBase>/bazel-workers .

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

Постоянные рабочие процессы реализованы для нескольких языков, включая Java, Scala , Kotlin и другие.

Программы, использующие среду выполнения NodeJS, могут использовать вспомогательную библиотеку @bazel/worker для реализации рабочего протокола.

Использование постоянных рабочих

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

bazel build //my:target --strategy=Javac=worker,local

Использование рабочей стратегии вместо локальной стратегии может значительно повысить скорость компиляции, в зависимости от реализации. Для Java сборка может быть в 2–4 раза быстрее, а иногда и больше для инкрементной компиляции. Компиляция Bazel с рабочими процессами выполняется примерно в 2,5 раза быстрее. Подробнее см. в разделе « Выбор количества рабочих ».

Если у вас также есть среда удаленной сборки, которая соответствует вашей локальной среде сборки, вы можете использовать экспериментальную динамическую стратегию , которая включает в себя удаленное выполнение и выполнение рабочего процесса. Чтобы включить динамическую стратегию, передайте флаг --experimental_spawn_scheduler . Эта стратегия автоматически включает рабочие процессы, поэтому нет необходимости указывать worker стратегию, но вы все равно можете использовать local или sandboxed в качестве запасных вариантов.

Выбор количества рабочих

Количество рабочих экземпляров по умолчанию на мнемонику равно 4, но может быть изменено с помощью флага worker_max_instances . Существует компромисс между эффективным использованием доступных ЦП и количеством JIT-компиляции и попаданий в кеш, которые вы получаете. Чем больше работников, тем больше целей оплатит начальные затраты на выполнение кода без JIT-компиляции и обращение к холодным кешам. Если у вас есть небольшое количество целевых объектов для сборки, один рабочий процесс может дать наилучший компромисс между скоростью компиляции и использованием ресурсов (например, см. проблему № worker_max_instances Флаг worker_max_instances устанавливает максимальное количество экземпляров рабочего процесса на мнемонику и флаг set (см. ниже), поэтому в смешанной системе вы можете использовать довольно много памяти, если сохраните значение по умолчанию.Для инкрементных сборок преимущество нескольких рабочих экземпляров еще меньше.

На этом графике показано время компиляции с нуля для Bazel (target //src:bazel ) на 6-ядерной рабочей станции Intel Xeon 3,5 ГГц с поддержкой Hyper-Threading Linux и 64 ГБ ОЗУ. Для каждой рабочей конфигурации запускается пять чистых сборок и берется среднее значение последних четырех.

Graph of performance improvements of clean builds

Рис. 1. График повышения производительности чистых сборок.

Для этой конфигурации два рабочих процесса обеспечивают самую быструю компиляцию, хотя и всего на 14% лучше, чем один рабочий процесс. Один рабочий процесс — хороший вариант, если вы хотите использовать меньше памяти.

Инкрементная компиляция обычно дает еще больше преимуществ. Чистые сборки относительно редки, но изменение одного файла между компиляциями — обычное дело, особенно при разработке через тестирование. В приведенном выше примере также есть некоторые действия по упаковке, отличные от Java, которые могут затмить добавочное время компиляции.

Перекомпиляция только исходных кодов Java ( //src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar ) после изменения внутренней строковой константы в AbstractContainerizingSandboxedSpawn.java дает 3-кратное ускорение (в среднем 20 добавочные сборки с одной отброшенной прогревочной сборкой):

Graph of performance improvements of incremental builds

Рис. 2. График улучшения производительности инкрементных сборок.

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

Изменение постоянных рабочих процессов

Вы можете передать флаг --worker_extra_flag , чтобы указать стартовые флаги для воркеров с мнемоническим ключом. Например, передача --worker_extra_flag=javac=--debug включает отладку только для Javac. Для каждого использования этого флага может быть установлен только один рабочий флаг и только для одной мнемоники. Рабочие создаются не только отдельно для каждой мнемоники, но и для вариаций их флагов запуска. Каждая комбинация мнемонических и стартовых флагов объединяется в WorkerKey , и для каждого WorkerKey может быть создано до worker_max_instances . См. следующий раздел о том, как конфигурация действия может также указывать установочные флаги.

Вы можете использовать флаг --high_priority_workers , чтобы указать мнемонику, которая должна выполняться вместо мнемоник с обычным приоритетом. Это может помочь определить приоритеты действий, которые всегда находятся на критическом пути. Если есть два или более высокоприоритетных воркера, выполняющих запросы, все остальные воркеры не могут быть запущены. Этот флаг можно использовать несколько раз.

Передача флага --worker_sandboxing заставляет каждый рабочий запрос использовать отдельный каталог песочницы для всех своих входных данных. Настройка песочницы занимает некоторое дополнительное время, особенно в macOS, но дает лучшую гарантию правильности.

Флаг --worker_quit_after_build в основном полезен для отладки и профилирования. Этот флаг заставляет всех рабочих уволиться после завершения сборки. Вы также можете передать --worker_verbose , чтобы получить больше информации о том, что делают работники. Этот флаг отражается в поле подробностей в verbosity , что позволяет реализациям рабочих WorkRequest быть более подробными.

Рабочие процессы хранят свои журналы в <outputBase>/bazel-workers , например, /tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log . Имя файла включает рабочий идентификатор и мнемонику. Поскольку на мнемонику может быть несколько WorkerKey , вы можете увидеть больше файлов журнала worker_max_instances для данной мнемоники.

Подробнее о сборках Android см. на странице Производительность сборки Android .

Внедрение постоянных работников

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

В этом примере показана конфигурация Starlark для воркера, использующего JSON:

args_file = ctx.actions.declare_file(ctx.label.name + "_args_file")
ctx.actions.write(
    output = args_file,
    content = "\n".join(["-g", "-source", "1.5"] + ctx.files.srcs),
)
ctx.actions.run(
    mnemonic = "SomeCompiler",
    executable = "bin/some_compiler_wrapper",
    inputs = inputs,
    outputs = outputs,
    arguments = [ "-max_mem=4G",  "@%s" % args_file.path],
    execution_requirements = {
        "supports-workers" : "1", "requires-worker-protocol" : "json" }
)

С этим определением первое использование этого действия начнется с выполнения командной строки /bin/some_compiler -max_mem=4G --persistent_worker . Тогда запрос на компиляцию Foo.java будет выглядеть так:

arguments: [ "-g", "-source", "1.5", "Foo.java" ]
inputs: [
  {path: "symlinkfarm/input1" digest: "d49a..." },
  {path: "symlinkfarm/input2", digest: "093d..."},
]

Рабочий процесс получает это на stdin в формате JSON с разделителями строк (поскольку для requires-worker-protocol установлено значение JSON). Затем рабочий выполняет действие и отправляет WorkResponse в формате JSON в WorkResponse на своем стандартном выводе. Затем Bazel анализирует этот ответ и вручную преобразует его в WorkResponse . Для связи со связанным рабочим процессом с использованием protobuf с двоичным кодированием вместо JSON для require requires-worker-protocol будет установлено значение proto , например:

  execution_requirements = {
    "supports-workers" : "1" ,
    "requires-worker-protocol" : "proto"
  }

Если вы не укажете require requires-worker-protocol в требованиях к выполнению, Bazel по умолчанию будет использовать для связи рабочего процесса protobuf.

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

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

В этом репозитории GitHub вы можете увидеть примеры рабочих оболочек, написанных как на Java, так и на Python. Если вы работаете с JavaScript или TypeScript, пакет @bazel/worker и пример рабочего процесса nodejs могут оказаться полезными.

Как рабочие влияют на песочницу?

Использование worker стратегии по умолчанию не запускает действие в песочнице , аналогично local стратегии. Вы можете установить флаг --worker_sandboxing для запуска всех воркеров внутри песочниц, убедившись, что при каждом выполнении инструмента видны только те входные файлы, которые он должен иметь. Инструмент может по-прежнему передавать информацию между запросами внутри, например, через кеш. Использование dynamic стратегии требует изолирования рабочих процессов .

Чтобы обеспечить правильное использование кэшей компилятора с рабочими процессами, вместе с каждым входным файлом передается дайджест. Таким образом, компилятор или оболочка могут проверить, действителен ли ввод, не читая файл.

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

Мультиплексные рабочие процессы могут быть изолированы только в том случае, если реализация рабочего процесса поддерживает это, и эта изолированная программная среда должна быть включена отдельно с помощью флага --experimental_worker_multiplex_sandboxing . Подробнее смотрите в дизайн-документе ).

дальнейшее чтение

Дополнительные сведения о постоянных рабочих процессах см.