La ejecución dinámica es una función de Bazel en la que la ejecución local y remota de la misma acción se inicia en paralelo, con la salida de la primera rama que finaliza y la cancelación de la otra. Combina la potencia de ejecución o la gran caché compartida de un sistema de compilación remota con la baja latencia de la ejecución local, lo que proporciona lo mejor de ambos mundos para compilaciones limpias y también incrementales.
En esta página, se describe cómo habilitar, ajustar y depurar la ejecución dinámica. Si tienes configuradas las ejecuciones locales y remotas, y quieres ajustar la configuración de Bazel para obtener un mejor rendimiento, esta página es para ti. Si aún no tienes configurada la ejecución remota, primero ve a la Descripción general de la ejecución remota de Bazel.
¿Habilitar la ejecución dinámica?
El módulo de ejecución dinámica forma parte de Bazel, pero para usarlo, ya debes poder compilar de forma local y remota desde la misma configuración de Bazel.
Para habilitar el módulo de ejecución dinámica, pasa la marca --internal_spawn_scheduler a Bazel. Esto agrega una nueva estrategia de ejecución llamada dynamic. Ahora puedes usarla como estrategia para los mnemónicos que deseas ejecutar de forma dinámica, como --strategy=Javac=dynamic. Consulta la siguiente sección para elegir los mnemónicos para los que habilitar la ejecución dinámica.
Para cualquier mnemónico que use la estrategia dinámica, las estrategias de ejecución remota se toman de la marca --dynamic_remote_strategy y las estrategias locales de la marca --dynamic_local_strategy. Si pasas --dynamic_local_strategy=worker,sandboxed, se establece el valor predeterminado para que la rama local de la ejecución dinámica intente con trabajadores o con la ejecución en zona de pruebas en ese orden. Si pasas --dynamic_local_strategy=Javac=worker, se anula el valor predeterminado solo para el mnemónico de Javac. La versión remota funciona de la misma manera. Ambas marcas se pueden especificar varias veces. Si una acción no se puede ejecutar de forma local, se ejecuta de forma remota como de costumbre y viceversa.
Si tu sistema remoto tiene una caché, la marca --dynamic_local_execution_delay agrega un retraso en milisegundos a la ejecución local después de que el sistema remoto haya indicado un acierto de caché. Esto evita que se ejecute la ejecución local cuando es probable que haya más aciertos de caché. El valor predeterminado es de 1,000 ms, pero debe ajustarse para que sea un poco más largo que lo que suelen tardar los aciertos de caché. El tiempo real depende del sistema remoto y de cuánto tarda un viaje de ida y vuelta. Por lo general, el valor será el mismo para todos los usuarios de un sistema remoto determinado, a menos que algunos de ellos estén lo suficientemente lejos como para agregar latencia de ida y vuelta. Puedes usar las funciones de generación de perfiles de Bazel
para ver cuánto tardan los aciertos de caché típicos
.
La ejecución dinámica se puede usar con la estrategia local en zona de pruebas, así como con
trabajadores persistentes. Los trabajadores persistentes se ejecutarán automáticamente
con la zona de pruebas cuando se usen con la ejecución dinámica y no podrán usar trabajadores multiplex. En los sistemas Darwin y Windows, la estrategia en zona de pruebas puede ser lenta. Puedes pasar --reuse_sandbox_directories para reducir la sobrecarga de la creación de zonas de pruebas en estos sistemas.
La ejecución dinámica también se puede ejecutar con la estrategia standalone, aunque, como la estrategia standalone debe tomar el bloqueo de salida cuando comienza a ejecutarse, bloquea de manera efectiva que la estrategia remota termine primero. La marca --experimental_local_lockfree_output permite solucionar este problema, ya que permite que la ejecución local escriba directamente en la salida, pero que la ejecución remota la anule si termina primero.
Si una de las ramas de la ejecución dinámica termina primero, pero falla, toda la acción falla. Esta es una elección intencional para evitar que pasen desapercibidas las diferencias entre la ejecución local y remota.
Para obtener más información sobre cómo funciona la ejecución dinámica y su bloqueo, consulta las excelentes entradas de blog de Julio Merino.
¿Cuándo se debe usar la ejecución dinámica?
La ejecución dinámica requiere algún tipo de sistema de ejecución remota. Actualmente, no es posible usar un sistema remoto solo de caché, ya que una falta de caché se consideraría una acción fallida.
No todos los tipos de acciones son adecuados para la ejecución remota. Los mejores candidatos son aquellos que son inherentemente más rápidos de forma local, por ejemplo, mediante el uso de trabajadores persistentes, o aquellos que se ejecutan lo suficientemente rápido como para que la sobrecarga de la ejecución remota domine el tiempo de ejecución. Dado que cada acción ejecutada de forma local bloquea una cierta cantidad de recursos de CPU y memoria, ejecutar acciones que no entran en esas categorías solo retrasa la ejecución de las que sí lo hacen.
A partir de la versión
5.0.0-pre.20210708.4,
la generación de perfiles de rendimiento contiene datos
sobre la ejecución del trabajador, incluido el tiempo que se tarda en finalizar una solicitud de trabajo después de
perder una carrera de ejecución dinámica. Si ves que los subprocesos de trabajo de ejecución dinámica pasan mucho tiempo adquiriendo recursos o mucho tiempo en async-worker-finish, es posible que tengas algunas acciones locales lentas que retrasen los subprocesos de trabajo.
En el perfil anterior, que usa 8 trabajadores de Javac, vemos que muchos trabajadores de Javac perdieron las carreras y terminaron su trabajo en los subprocesos async-worker-finish. Esto se debió a que un mnemónico que no es de trabajador tomó suficientes recursos para retrasar a los trabajadores.
Cuando solo se ejecuta Javac con ejecución dinámica, solo alrededor de la mitad de los trabajadores iniciados terminan perdiendo la carrera después de comenzar su trabajo.
La marca --experimental_spawn_scheduler recomendada anteriormente dejó de estar disponible.
Activa la ejecución dinámica y establece dynamic como la estrategia predeterminada para todos los mnemónicos, lo que a menudo generaba este tipo de problemas.
Rendimiento
El enfoque de ejecución dinámica supone que hay suficientes recursos disponibles de forma local y remota para que valga la pena invertir algunos recursos adicionales para mejorar el rendimiento general. Sin embargo, el uso excesivo de recursos puede ralentizar Bazel o la máquina en la que se ejecuta, o bien ejercer una presión inesperada en un sistema remoto. Existen varias opciones para cambiar el comportamiento de la ejecución dinámica:
--dynamic_local_execution_delay retrasa el inicio de una rama local en una cantidad de milisegundos después de que se haya iniciado la rama remota, pero solo si hubo un acierto de caché remota durante la compilación actual. Esto hace que las compilaciones que se benefician del almacenamiento en caché remoto no desperdicien recursos locales cuando es probable que la mayoría de las salidas se encuentren en la caché. Según la calidad de la caché, reducir esto podría mejorar las velocidades de compilación, a costa de usar más recursos locales.
--experimental_dynamic_local_load_factor es una opción experimental avanzada de administración de recursos. Toma un valor de 0 a 1, y 0 desactiva esta función.
Cuando se establece en un valor superior a 0, Bazel ajusta la cantidad de acciones programadas de forma local cuando muchas acciones esperan ser programadas. Si se establece en 1, se permite programar tantas acciones como CPUs disponibles haya (según --local_cpu_resources). Los valores más bajos establecen la cantidad de acciones programadas en una cantidad correspondientemente menor a medida que hay más acciones disponibles para ejecutarse. Esto puede parecer contradictorio, pero con un buen sistema remoto, la ejecución local no ayuda mucho cuando se ejecutan muchas acciones, y la CPU local se usa mejor para administrar acciones remotas.
--experimental_dynamic_slow_remote_time prioriza el inicio de las ramas locales cuando la rama remota se ha estado ejecutando durante al menos este tiempo. Por lo general, la acción programada más recientemente tiene prioridad, ya que tiene la mayor probabilidad de ganar la carrera, pero si el sistema remoto a veces se bloquea o tarda más de lo normal, esto puede hacer que una compilación avance. Esta opción no está habilitada de forma predeterminada, ya que podría ocultar problemas con el sistema remoto que deberían solucionarse. Asegúrate de supervisar el rendimiento de tu sistema remoto si habilitas esta opción.
--experimental_dynamic_ignore_local_signals se puede usar para permitir que la rama remota tome el control cuando una generación local sale debido a una señal determinada. Esto es
útil principalmente junto con los límites de recursos del trabajador (consulta
--experimental_worker_memory_limit_mb,
--experimental_worker_sandbox_hardening,
y
--experimental_sandbox_memory_limit_mb)),
en los que los procesos del trabajador pueden finalizarse cuando usan demasiados recursos.
El perfil de seguimiento de JSON contiene varios gráficos relacionados con el rendimiento que pueden ayudar a identificar formas de mejorar la compensación del rendimiento y el uso de recursos.
Solución de problemas
Los problemas con la ejecución dinámica pueden ser sutiles y difíciles de depurar, ya que solo pueden manifestarse en algunas combinaciones específicas de ejecución local y remota.
La marca --debug_spawn_scheduler agrega una salida adicional del sistema de ejecución dinámica que puede ayudar a depurar estos problemas. También puedes ajustar la marca --dynamic_local_execution_delay y la cantidad de trabajos remotos en comparación con los locales para que sea más fácil reproducir los problemas.
Si tienes problemas con la ejecución dinámica con la estrategia standalone, intenta ejecutar sin --experimental_local_lockfree_output o ejecuta tus acciones locales en zona de pruebas. Esto puede ralentizar un poco la compilación (consulta lo anterior si usas Mac o Windows), pero quita algunas posibles causas de fallas.