Workers persistentes

Informar um problema Mostrar 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.

Você pode usar o --high_priority_workers para especificar um mnemônico que deve ser executado em prioridade mnemônicas. Isso pode ajudar a priorizar ações que estão sempre no caminho. Se houver dois ou mais workers de alta prioridade executando solicitações, todos outros workers sejam impedidos de ser executados. Essa sinalização pode ser usada várias vezes.

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: