Workers persistentes

Relatar um problema Conferir código-fonte Por noite · 7,3 · 7,2 · 7,1 · 7,0 · 6,5

Nesta página, explicamos como usar workers persistentes, os benefícios, os requisitos e como os workers afetam o sandbox.

Um worker permanente é um processo de longa duração iniciado pelo servidor do Bazel, como um wrapper em torno da ferramenta real (geralmente um compilador), ou a própria ferramenta. Para aproveitar os workers persistentes, a ferramenta precisa 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 na mesmo build e é responsável por iniciar e comunicar adequadamente com os além de desligar os workers na saída. Cada instância de worker é atribuída (mas não com chroot para) um diretório de trabalho separado em <outputBase>/bazel-workers:

Usar workers permanentes é uma estratégia de execução que diminui de inicialização, permite mais compilação JIT e permite o armazenamento em cache de as árvores de sintaxe abstratas na execução da ação. Essa estratégia alcança essas melhorias com o envio de várias solicitações para uma interface de usuário de desenvolvimento de software.

Os workers permanentes são implementados em várias linguagens, incluindo Java, Scala, Kotlin e muito mais.

Os programas que usam um ambiente de execução NodeJS podem usar a biblioteca auxiliar @bazel/worker para implementar o protocolo do worker.

Como usar workers permanentes

Bazel versão 0.27 e mais recente usa workers permanentes por padrão ao executar builds, embora e a execução têm prioridade. Para ações que não dão suporte a workers persistentes, O Bazel volta a iniciar uma instância de ferramenta para cada ação. É possível definir defina o build para usar workers permanentes definindo a propriedade worker. estratégia para a ferramenta aplicável mnemônicas. Como prática recomendada, este exemplo inclui especificar local como um para a estratégia worker:

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

Usar a estratégia workers em vez da estratégia local pode impulsionar a compilação. a velocidade significativamente, dependendo da implementação. Para Java, os builds podem ser de 2 a 4 vezes mais rápido e, às vezes, mais para compilação incremental. A compilação do Bazel é cerca de 2,5 vezes mais rápido com workers. Para mais detalhes, consulte a "Como escolher o número de workers" nesta seção.

Se você também tiver um ambiente de build remoto que corresponda ao build local ambiente, use o modelo estratégia dinâmica, que gera uma execução remota e uma execução de worker. Para ativar de uma estratégia, transmita a --experimental_spawn_scheduler . Essa estratégia ativa os workers automaticamente, então não é preciso especificar a estratégia worker, mas ainda é possível usar local ou sandboxed como substitutos.

Como 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 o worker_max_instances . Há uma compensação entre fazer bom uso das CPUs disponíveis e da de compilação JIT e de ocorrências em cache. Com mais workers, mais as metas vão pagar os custos de inicialização para executar o código não JITted e clicar no armazenamento em cache. Se você tiver poucos destinos para criar, um único worker poderá a melhor compensação entre velocidade de compilação e uso de recursos (por exemplo, consulte o problema 8586. A flag worker_max_instances define o número máximo de instâncias de worker por mnemônico e de flag (veja abaixo). Em um sistema misto, você pode acabar usando muita memória se mantiver o valor padrão. Para builds incrementais, de várias instâncias de worker é ainda menor.

Este gráfico mostra os tempos de compilação do Bazel do zero (destino) //src:bazel) em uma estação de trabalho Linux com hiperthread de 6 núcleos Intel Xeon 3,5 GHz com 64 GB de RAM. Para cada configuração de worker, cinco builds limpos são executados e a média dos últimos quatro foi usada.

Gráfico de melhorias de desempenho de builds limpos

Figura 1. Gráfico de melhorias no desempenho de builds limpos.

Para essa configuração, dois workers fazem 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 usam menos memória.

A compilação incremental costuma ser ainda mais vantajosa. Builds limpos são relativamente raro, mas alterar um único arquivo entre compilações é comum, em especialmente no desenvolvimento orientado a testes. O exemplo acima também tem componentes ações de empacotamento que podem ofuscar o tempo de compilação incremental.

Recompilar apenas as fontes Java (//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar) depois de mudar uma constante interna de string AbstractContainerizingSandboxedSpawn.java proporciona uma aceleração de três vezes (média de 20 builds incrementais com um build de aquecimento) descartadas):

Gráfico das melhorias de desempenho de builds incrementais

Figura 2. Gráfico de melhorias no desempenho de builds incrementais.

A aceleração depende da mudança que está sendo feita. A aceleração de um fator 6 medida na situação acima, quando uma constante usada com frequência é alterada.

Como modificar workers permanentes

É possível transmitir --worker_extra_flag para especificar sinalizações de inicialização para workers, codificadas por mnemônicos. Por exemplo: transmitir --worker_extra_flag=javac=--debug ativa a depuração apenas para Javac. Somente uma flag de worker pode ser definida por uso dessa flag e apenas para um mnemônico. Os workers não são criados apenas separadamente para cada mnemônica, mas também para variações nas sinalizações de inicialização. Cada combinação de funções mnemônicas e de inicialização são combinadas em um WorkerKey, e para cada WorkerKey até É possível criar worker_max_instances workers. Consulte a próxima seção para saber como o a configuração de ações também pode especificar flags de configuração.

Transmitir o valor-chave --worker_sandboxing faz com que cada solicitação de worker use um diretório de sandbox separado para todas as de entrada. A configuração do sandbox leva algum tempo extra, especialmente no macOS, mas oferece uma garantia de correção melhor.

O --worker_quit_after_build é útil principalmente para depuração e criação de perfil. Essa flag força todos os workers a sair quando o build for concluído. Também é possível transmitir --worker_verbose para receber mais resultados sobre o que os workers estão fazendo. Essa sinalização é refletida na O campo verbosity em WorkRequest, permitindo que as implementações de worker também sejam mais detalhado.

Os workers armazenam os registros no diretório <outputBase>/bazel-workers para 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, você poderá ver mais de worker_max_instances os arquivos de registro de um determinado mnemônico.

Para builds do Android, consulte detalhes na Página "Desempenho do build do Android".

Como implementar workers persistentes

Consulte a página Como criar workers permanentes 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ça com a execução a linha de comando /bin/some_compiler -max_mem=4G --persistent_worker. Uma solicitação para compilar o Foo.java ficará assim:

OBSERVAÇÃO: embora a especificação do buffer de protocolo use "snake case" (request_id), o protocolo JSON usa letras concatenadas (requestId). Neste documento, usaremos letras concatenadas nos exemplos JSON, mas snake-case ao falar sobre o campo. independentemente 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 executa a ação, e envia um WorkResponse formatado em JSON para o Bazel na stdout. 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, desta forma:

  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 padronizar a comunicação do worker para usar protobuf.

O Bazel deriva o WorkerKey das flags mnemônicas e compartilhadas. Portanto, se esse permite mudar o parâmetro max_mem, um worker separado gerados para cada valor usado. Isso pode causar consumo excessivo de memória se muitas variações forem usadas.

No momento, cada worker só pode processar uma solicitação por vez. A fase experimental recurso multiplex workers permite o uso de diversas se a ferramenta subjacente tiver várias linhas de execução e o wrapper estiver configurado para entender isso.

Em neste repositório do GitHub, você pode ver exemplos de wrappers de worker escritos em Java e Python. Se você estão trabalhando em JavaScript ou TypeScript, Pacote@bazel/worker e Exemplo de worker nodejs pode ser útil.

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. É possível definir a flag --worker_sandboxing para executar todos os workers dentro de sandboxes, garantindo que cada execução da ferramenta só veja os arquivos de entrada que deveria ter. A ferramenta ainda podem vazar informações entre solicitações internamente, por exemplo, por meio de um cache. Usando a estratégia dynamic exige que os workers estejam no 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 pode verificar se a entrada é ainda é válido sem precisar ler o arquivo.

Mesmo com o uso de resumos de entrada para proteger contra armazenamento em cache indesejado, os oferecem uma sandbox menos rígida do que uma sandbox pura, pois a ferramenta pode manter outro estado interno que foi afetado por solicitações anteriores.

Os workers multiplex só podem ser colocados no sandbox se a implementação do worker for compatível. e esse sandbox precisa ser ativado separadamente com o sinalização --experimental_worker_multiplex_sandboxing. Confira mais detalhes em documento de design).

Leitura adicional

Para mais informações sobre workers permanentes, consulte: