A execução dinâmica é um recurso do Bazel em que a execução local e remota da mesma ação são iniciadas em paralelo, usando a saída da primeira ramificação que termina e cancelando a outra. Ela combina o poder de execução e/ou o grande cache compartilhado de um sistema de build remoto com a baixa latência da execução local, oferecendo o melhor dos dois mundos para builds limpos e incrementais iguais.
Esta página descreve como ativar, ajustar e depurar a execução dinâmica. Se você tiver a execução local e remota configuradas e estiver tentando ajustar as configurações do Bazel para melhorar o desempenho, esta página é para você. Se você ainda não configurou a execução remota, acesse a Visão geral da execução remota do Bazel primeiro.
Como ativar a execução dinâmica?
O módulo de execução dinâmica faz parte do Bazel, mas, para usar a execução dinâmica , você já precisa compilar local e remotamente na mesma configuração do Bazel.
Para ativar o módulo de execução dinâmica, transmita a --internal_spawn_scheduler
flag para o Bazel. Isso adiciona uma nova estratégia de execução chamada dynamic. Agora você pode
usar essa estratégia para os mnemônicos que quer executar dinamicamente, como
--strategy=Javac=dynamic. Consulte a próxima seção para saber como escolher os mnemônicos
para ativar a execução dinâmica.
Para qualquer mnemônico que use a estratégia dinâmica, as estratégias de execução remota são
extraídas da flag --dynamic_remote_strategy e as estratégias locais da
--dynamic_local_strategy flag. A transmissão de
--dynamic_local_strategy=worker,sandboxed define o padrão para a ramificação local da execução dinâmica para tentar com workers ou execução em sandbox nessa
ordem. A transmissão de --dynamic_local_strategy=Javac=worker substitui o padrão para
o mnemônico Javac apenas. A versão remota funciona da mesma maneira. As duas flags podem
ser especificadas várias vezes. Se uma ação não puder ser executada localmente, ela será
executada remotamente como normal e vice-versa.
Se o sistema remoto tiver um cache, a flag --dynamic_local_execution_delay
vai adicionar um atraso em milissegundos à execução local depois que o sistema remoto
indicar um acerto de cache. Isso evita a execução local quando é provável que haja mais acertos de cache. O valor padrão é de 1.000 ms, mas precisa ser ajustado para ser um pouco
maior do que os acertos de cache normalmente levam. O tempo real depende do sistema remoto
e de quanto tempo uma viagem de ida e volta leva. Normalmente, o valor será o mesmo
para todos os usuários de um determinado sistema remoto, a menos que alguns deles estejam longe o suficiente
para adicionar latência de ida e volta. Você pode usar os recursos de criação de perfil do Bazel para conferir quanto tempo os acertos de cache típicos
levam.
A execução dinâmica pode ser usada com a estratégia de sandbox local e com
workers persistentes. Os workers persistentes serão executados automaticamente
com o sandbox quando usados com a execução dinâmica e não poderão usar workers
multiplexados. Em sistemas Darwin e Windows, a estratégia de sandbox
pode ser lenta. Você pode transmitir --reuse_sandbox_directories para reduzir
a sobrecarga da criação de sandboxes nesses sistemas.
A execução dinâmica também pode ser executada com a estratégia standalone, embora, como a
standalone estratégia precise usar o bloqueio de saída quando começar a execução, ela
bloqueie a estratégia remota de terminar primeiro. A flag
--experimental_local_lockfree_output permite contornar esse problema, permitindo que a execução local grave diretamente na saída, mas seja interrompida pela execução remota, caso ela termine primeiro.
Se uma das ramificações da execução dinâmica terminar primeiro, mas for uma falha, a ação inteira falhará. Essa é uma escolha intencional para evitar que diferenças entre a execução local e remota passem despercebidas.
Para mais informações sobre como a execução dinâmica e o bloqueio funcionam, consulte as excelentes postagens do blog de Julio Merino (links em inglês)blog posts.
Quando devo usar a execução dinâmica?
A execução dinâmica requer alguma forma de sistema de execução remota. No momento, não é possível usar um sistema remoto somente de cache, já que um erro de cache seria considerado uma ação com falha.
Nem todos os tipos de ações são adequados para execução remota. Os melhores candidatos são aqueles que são inerentemente mais rápidos localmente, por exemplo, usando workers persistentes, ou aqueles que são executados rápido o suficiente para que a sobrecarga da execução remota domine o tempo de execução. Como cada ação executada localmente bloqueia uma certa quantidade de recursos de CPU e memória, a execução de ações que não se enquadram nessas categorias apenas atrasa a execução daquelas que se enquadram.
A partir da versão
5.0.0-pre.20210708.4,
a criação de perfil de desempenho contém dados
sobre a execução do worker, incluindo o tempo gasto para concluir uma solicitação de trabalho após
perder uma corrida de execução dinâmica. Se você notar que as linhas de execução do worker de execução dinâmica
gastam muito tempo adquirindo recursos ou muito tempo no
async-worker-finish, talvez haja algumas ações locais lentas atrasando as linhas de execução do worker.
No perfil acima, que usa oito workers Javac, vemos muitos workers Javac
que perderam as corridas e terminaram o trabalho nas async-worker-finish
linhas de execução. Isso foi causado por um mnemônico não worker que usou recursos suficientes para
atrasar os workers.
Quando apenas o Javac é executado com execução dinâmica, apenas cerca de metade dos workers iniciados acabam perdendo a corrida após iniciar o trabalho.
A flag --experimental_spawn_scheduler recomendada anteriormente foi descontinuada.
Ela ativa a execução dinâmica e define dynamic como a estratégia padrão para todos os
mnemônicos, o que geralmente leva a esses tipos de problemas.
Desempenho
A abordagem de execução dinâmica pressupõe que há recursos suficientes disponíveis local e remotamente para que valha a pena gastar alguns recursos extras para melhorar o desempenho geral. No entanto, o uso excessivo de recursos pode desacelerar o próprio Bazel ou a máquina em que ele é executado ou colocar uma pressão inesperada em um sistema remoto. Há várias opções para mudar o comportamento da execução dinâmica:
--dynamic_local_execution_delay atrasa o início de uma ramificação local por um número
de milissegundos após o início da ramificação remota, mas apenas se houver
um acerto de cache remoto durante o build atual. Isso faz com que os builds que se beneficiam
do armazenamento em cache remoto não desperdicem recursos locais quando é provável que a maioria
das saídas possa ser encontrada no cache. Dependendo da qualidade do cache,
a redução desse valor pode melhorar a velocidade do build, ao custo de usar mais recursos locais.
--experimental_dynamic_local_load_factor é uma opção avançada experimental de gerenciamento de recursos. Ela usa um valor de 0 a 1, sendo que 0 desativa esse recurso.
Quando definido como um valor acima de 0, o Bazel ajusta o número de
ações programadas localmente quando muitas ações estão aguardando a
programação. A definição como 1 permite que o maior número possível de ações seja programado, conforme as CPUs disponíveis (de acordo com --local_cpu_resources). Valores mais baixos definem o número de ações programadas para um número correspondente menor, à medida que um número maior de ações está disponível para execução. Isso pode parecer contraintuitivo, mas, com um bom sistema remoto
, a execução local não ajuda muito quando muitas ações estão sendo executadas, e
a CPU local é melhor gasta gerenciando ações remotas.
--experimental_dynamic_slow_remote_time prioriza o início de ramificações locais
quando a ramificação remota está em execução há pelo menos esse tempo. Normalmente a
ação programada mais recentemente tem prioridade, já que tem a maior chance de
vencer a corrida, mas, se o sistema remoto às vezes trava ou leva muito tempo,
isso pode fazer com que um build avance. Essa opção não está ativada por padrão, porque pode
ocultar problemas com o sistema remoto que precisam ser corrigidos. Monitore o desempenho do sistema remoto se você ativar essa opção.
--experimental_dynamic_ignore_local_signals pode ser usado para permitir que a ramificação remota
assuma o controle quando um spawn local sair devido a um determinado sinal. Isso
é útil principalmente com limites de recursos do worker (consulte
--experimental_worker_memory_limit_mb,
--experimental_worker_sandbox_hardening,
e
--experimental_sandbox_memory_limit_mb)),
em que os processos do worker podem ser encerrados quando usam muitos recursos.
O perfil de rastreamento JSON contém vários gráficos relacionados ao desempenho que podem ajudar a identificar maneiras de melhorar a relação entre desempenho e uso de recursos.
Solução de problemas
Os problemas com a execução dinâmica podem ser sutis e difíceis de depurar, já que podem
se manifestar apenas em algumas combinações específicas de execução local e remota.
A flag --debug_spawn_scheduler adiciona mais saída do sistema de execução
dinâmica que pode ajudar a depurar esses problemas. Você também pode ajustar a
--dynamic_local_execution_delay flag e o número de jobs remotos em comparação com os locais para
facilitar a reprodução dos problemas.
Se você estiver com problemas na execução dinâmica usando a standalone
estratégia, tente executar sem --experimental_local_lockfree_output ou execute
as ações locais em sandbox. Isso pode desacelerar um pouco o build (consulte acima se
você estiver no Mac ou Windows), mas remove algumas possíveis causas de falhas.