Esta página explica como usar workers persistentes, os benefícios, os requisitos e como eles afetam o sandbox.
Um worker persistente é um processo de longa duração iniciado pelo servidor do Bazel, que funciona como um wrapper em torno da ferramenta real (geralmente um compilador) ou é a ferramenta em si. Para aproveitar os benefícios dos workers persistentes, a ferramenta precisa
oferecer suporte a uma sequência de compilações, e o wrapper precisa traduzir
entre a API da ferramenta e o formato de solicitação/resposta descrito abaixo. O mesmo
worker pode ser chamado com e sem a flag --persistent_worker
no
mesmo build e é responsável por iniciar e se comunicar adequadamente com a
ferramenta, além de encerrar os workers ao sair. Cada instância de worker é atribuída (mas não chrooted) a um diretório de trabalho separado em <outputBase>/bazel-workers
.
O uso de workers persistentes é uma estratégia de execução que diminui a sobrecarga de inicialização, permite mais compilação JIT e ativa o armazenamento em cache, por exemplo, das árvores de sintaxe abstrata na execução da ação. Essa estratégia envia várias solicitações a um processo de longa duração para alcançar essas melhorias.
Os workers persistentes são implementados para várias linguagens, incluindo Java, Scala, Kotlin e outras.
Programas que usam um ambiente de execução do NodeJS podem usar a biblioteca auxiliar @bazel/worker para implementar o protocolo de worker.
Como usar workers persistentes
O Bazel 0.27 e versões mais recentes
usam workers persistentes por padrão ao executar builds, embora a execução
remota tenha precedência. Para ações que não oferecem suporte a workers permanentes, o Bazel volta a iniciar uma instância de ferramenta para cada ação. É possível definir explicitamente
o build para usar workers persistentes definindo a worker
estratégia para os mnemônicos de ferramentas
aplicáveis. Como prática recomendada, este exemplo inclui a especificação de local
como um
fallback para a estratégia worker
:
bazel build //my:target --strategy=Javac=worker,local
Usar a estratégia de workers em vez da local pode aumentar significativamente a velocidade de compilação, dependendo da implementação. Para Java, os builds podem ser de 2 a 4 vezes mais rápidos, às vezes mais para compilação incremental. A compilação do Bazel é cerca de 2,5 vezes mais rápida com workers. Para mais detalhes, consulte a seção Escolher o número de workers.
Se você também tiver um ambiente de build remoto que corresponda ao ambiente de build local, use a estratégia dinâmica experimental, que executa uma execução remota e uma execução de worker. Para ativar a estratégia dinâmica, transmita a flag --experimental_spawn_scheduler. Essa estratégia ativa automaticamente os workers. Portanto, não é necessário especificar a estratégia worker
, mas ainda é possível usar local
ou sandboxed
como substitutos.
Escolher o número de workers
O número padrão de instâncias de worker por mnemônico é 4, mas pode ser ajustado
com a flag
worker_max_instances
. Há uma compensação entre o bom uso das CPUs disponíveis e a quantidade de compilação JIT e acertos de cache que você recebe. Com mais workers, mais
destinos vão pagar os custos de inicialização da execução de código não JITted e atingir caches
frios. Se você tiver um pequeno número de destinos para criar, um único worker poderá oferecer a melhor compensação entre velocidade de compilação e uso de recursos. Por exemplo, consulte problema nº 8586.
A flag worker_max_instances
define o número máximo de instâncias de worker por mnemônico e conjunto de flags (veja abaixo). Portanto, em um sistema misto, você pode acabar usando muita memória se mantiver o valor padrão. Para builds incrementais, o benefício de várias instâncias de worker é ainda menor.
Este gráfico mostra os tempos de compilação do zero para o Bazel (destino
//src:bazel
) em uma estação de trabalho Linux Intel Xeon de 3,5 GHz com hiperesamento de 6 núcleos
e 64 GB de RAM. Para cada configuração de worker, cinco builds limpos são executados, e a média dos últimos quatro é calculada.
Figura 1. Gráfico de melhorias de desempenho de builds limpas.
Para essa configuração, dois workers oferecem a compilação mais rápida, embora com apenas 14% de melhoria em comparação com um worker. Um worker é uma boa opção se você quiser usar menos memória.
A compilação incremental geralmente traz ainda mais benefícios. Builds limpos são relativamente raros, mas é comum mudar um único arquivo entre as compilações, principalmente no desenvolvimento orientado a testes. O exemplo acima também tem algumas ações de empacotamento que não são do Java e podem ofuscar o tempo de compilação incremental.
A recompilação das fontes Java apenas
(//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar
)
depois de mudar uma constante de string interna em
AbstractContainerizingSandboxedSpawn.java
resulta em uma aceleração de 3 vezes (média de 20 builds incrementais com um build de pré-aquecimento
descartado):
Figura 2. Gráfico das melhorias de performance das builds incrementais.
A aceleração depende da mudança que está sendo feita. Um aumento de velocidade de um fator 6 é medido na situação acima quando uma constante usada com frequência é alterada.
Como modificar workers permanentes
É possível transmitir a flag
--worker_extra_flag
para especificar flags de inicialização para workers, com chave por mnemônico. Por exemplo, transmitir --worker_extra_flag=javac=--debug
ativa a depuração apenas para o Javac.
Só é possível definir uma flag de worker por uso dessa flag e apenas para um mnemônico.
Os workers não são criados separadamente apenas para cada mnemônico, mas também para
variações nas flags de inicialização. Cada combinação de mnemônico e flags de inicialização
é combinada em um WorkerKey
, e para cada WorkerKey
até
worker_max_instances
workers podem ser criados. Consulte a próxima seção para saber como a configuração de ação também pode especificar flags de configuração.
Ao transmitir a flag
--worker_sandboxing
,
cada solicitação de worker usa um diretório de sandbox separado para todas as entradas. Configurar o sandbox leva um tempo extra, principalmente no macOS, mas oferece uma garantia de correção melhor.
A flag
--worker_quit_after_build
é útil principalmente para depuração e criação de perfil. Essa flag força todos os workers
a sair assim que um build é concluído. Também é possível transmitir --worker_verbose
para receber mais informações sobre o que os workers estão fazendo. Essa flag é refletida no campo verbosity
em WorkRequest
, permitindo que as implementações de worker também sejam mais detalhadas.
Os workers armazenam os registros no diretório <outputBase>/bazel-workers
, por exemplo, /tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log
.
O nome do arquivo inclui o ID do worker e o mnemônico. Como pode haver mais de um WorkerKey
por mnemônico, talvez você veja mais de worker_max_instances
arquivos de registros para um determinado mnemônico.
Para builds do Android, consulte detalhes na página de desempenho do build do Android.
Implementar workers persistentes
Consulte a página Como criar workers persistentes para mais informações sobre como criar um worker.
Este exemplo mostra uma configuração do Starlark para um worker que usa 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" }
)
Com essa definição, o primeiro uso dessa ação começaria com a execução
da linha de comando /bin/some_compiler -max_mem=4G --persistent_worker
. Uma solicitação
para compilar Foo.java
ficaria assim:
OBSERVAÇÃO: embora a especificação do buffer de protocolo use "snake case" (request_id
),
o protocolo JSON usa "camel case" (requestId
). Neste documento, vamos usar
camel case nos exemplos JSON, mas snake case ao falar sobre o campo
independente do protocolo.
{
"arguments": [ "-g", "-source", "1.5", "Foo.java" ]
"inputs": [
{ "path": "symlinkfarm/input1", "digest": "d49a..." },
{ "path": "symlinkfarm/input2", "digest": "093d..." },
],
}
O worker recebe isso em stdin
no formato JSON delimitado por nova linha (porque
requires-worker-protocol
está definido como JSON). Em seguida, o worker realiza a ação
e envia um WorkResponse
formatado em JSON para o Bazel no stdout. Em seguida, o Bazel analisa essa resposta e a converte manualmente em um proto WorkResponse
. Para
se comunicar com o worker associado usando protobuf codificado em binário em vez de
JSON, requires-worker-protocol
seria definido como proto
, assim:
execution_requirements = {
"supports-workers" : "1" ,
"requires-worker-protocol" : "proto"
}
Se você não incluir requires-worker-protocol
nos requisitos de execução, o Bazel vai usar protobuf por padrão para a comunicação do worker.
O Bazel deriva o WorkerKey
do mnemônico e das flags compartilhadas. Portanto, se essa
configuração permitisse mudar o parâmetro max_mem
, um worker separado seria
gerado para cada valor usado. Isso pode levar a um consumo excessivo de memória se muitas variações forem usadas.
No momento, cada worker só pode processar uma solicitação por vez. O recurso experimental multiplex workers permite usar várias linhas de execução se a ferramenta subjacente for multithread e o wrapper estiver configurado para entender isso.
Em este repositório do GitHub, você pode conferir exemplos de wrappers de worker escritos em Java e em Python. Se você estiver trabalhando em JavaScript ou TypeScript, o pacote@bazel/worker e o exemplo de worker do nodejs podem ser úteis.
Como os workers afetam o sandbox?
Usar a estratégia worker
por padrão não executa a ação em um sandbox, semelhante à estratégia local
. Você pode definir a flag --worker_sandboxing
para executar todos os workers em sandbox, garantindo que cada execução da ferramenta veja apenas os arquivos de entrada que deve ter. A ferramenta ainda pode vazar informações entre solicitações internamente, por exemplo, por um cache. O uso da estratégia dynamic
exige que os workers estejam em sandbox.
Para permitir o uso correto de caches do compilador com workers, um resumo é transmitido com cada arquivo de entrada. Assim, o compilador ou o wrapper podem verificar se a entrada ainda é válida sem precisar ler o arquivo.
Mesmo ao usar os resumos de entrada para evitar o cache indesejado, os workers em sandbox oferecem um sandbox menos restrito do que um sandbox puro, porque a ferramenta pode manter outro estado interno afetado por solicitações anteriores.
Os workers multiplex só podem ser isolados em sandbox se a implementação do worker oferecer suporte a isso. Esse isolamento precisa ser ativado separadamente com a flag --experimental_worker_multiplex_sandboxing
. Confira mais detalhes no
documento de design).
Leitura adicional
Para mais informações sobre workers persistentes, consulte:
- Postagem original do blog sobre trabalhadores persistentes
- Descrição da implementação em Haskell
- Postagem do blog de Mike Morearty
- Desenvolvimento de front-end com Bazel: Angular/TypeScript e trabalhadores persistentes com Asana
- Explicação das estratégias do Bazel
- Discussão informativa sobre a estratégia de workers na lista de e-mails bazel-discuss