En esta página, se explica cómo usar trabajadores persistentes, los beneficios, los requisitos y cómo los trabajadores afectan la zona de pruebas.
Un trabajador persistente es un proceso de larga duración iniciado por el servidor de Bazel, que funciona como un wrapper alrededor de la tool real (por lo general, un compilador) o es la herramienta en sí. Para beneficiarse de los trabajadores persistentes, la herramienta debe admitir una secuencia de compilaciones, y el wrapper debe traducir entre la API de la herramienta y el formato de solicitud/respuesta que se describe a continuación. Se puede llamar al mismo trabajador con y sin la marca --persistent_worker
en la misma compilación. Además, es responsable de iniciar y hablar de forma adecuada con la herramienta, además de cerrar los trabajadores al salir. A cada instancia de trabajador se le asigna un directorio de trabajo independiente en <outputBase>/bazel-workers
, pero no se le asigna un historial.
El uso de trabajadores persistentes es una estrategia de ejecución que disminuye la sobrecarga de inicio, permite más compilación de JIT y permite el almacenamiento en caché de árboles de sintaxis abstractos en la ejecución de la acción. Esta estrategia logra estas mejoras mediante el envío de varias solicitudes a un proceso de larga duración.
Los trabajadores persistentes se implementan para varios lenguajes, incluidos Java, Scala, Kotlin y más.
Los programas que usan un entorno de ejecución NodeJS pueden usar la biblioteca auxiliar @bazel/worker para implementar el protocolo de trabajador.
Usa trabajadores persistentes
Bazel 0.27 y versiones posteriores usan trabajadores persistentes de forma predeterminada cuando se ejecutan compilaciones, aunque la ejecución remota tiene prioridad. Para las acciones que no admiten trabajadores persistentes, Bazel regresa a iniciar una instancia de herramienta para cada acción. Puedes configurar de forma explícita tu compilación a fin de que use trabajadores persistentes si configuras la estrategia worker
para los mnemónicos de herramientas aplicables. Como práctica recomendada, en este ejemplo se incluye la especificación de local
como resguardo de la estrategia de worker
:
bazel build //my:target --strategy=Javac=worker,local
El uso de la estrategia de trabajadores en lugar de la estrategia local puede mejorar la velocidad de compilación de forma significativa, según la implementación. En el caso de Java, las compilaciones pueden ser entre 2 y 4 veces más rápidas, a veces más para la compilación incremental. Compilar Bazel es unas 2.5 veces más rápido con los trabajadores. Para obtener más información, consulta la sección para elegir un número de trabajadores.
Si también tienes un entorno de compilación remoto que coincide con tu entorno de compilación
local, puedes usar la
estrategia dinámica experimental, que ejecuta una ejecución remota y una de trabajador. Para habilitar la estrategia dinámica, pasa la marca --experimental_spawn_scheduler. Esta estrategia habilita a los trabajadores de forma automática, por lo que no es necesario especificar la estrategia worker
, pero aún puedes usar local
o sandboxed
como resguardos.
Elige la cantidad de trabajadores
La cantidad predeterminada de instancias de trabajador por cantidad es de 4, pero se puede ajustar con la marca worker_max_instances
. Existe una compensación entre hacer un buen uso de las CPU disponibles y la cantidad de compilación de JIT y aciertos de caché que recibes. Con más trabajadores, más destinos pagarán los costos iniciales por ejecutar código sin JIT y almacenar en cachés frías. Si tienes una pequeña cantidad de destinos para compilar, un solo trabajador puede proporcionar la mejor compensación entre la velocidad de compilación y el uso de recursos (por ejemplo, consulta el problema n.o 8586).
La marca worker_max_instances
establece la cantidad máxima de instancias de trabajadores por conjunto nemotécnico y de marca (consulta a continuación), por lo que, en un sistema mixto, podrías terminar usando bastante memoria si conservas el valor predeterminado. En compilaciones incrementales, el beneficio de varias instancias de trabajador es aún más pequeño.
En este gráfico, se muestran los tiempos de compilación que se crearon desde cero para Bazel (objetivo //src:bazel
) en una estación de trabajo Intel Xeon de 3 núcleos y 6 núcleos de Linux con 6.4 GB de RAM. Para cada configuración de trabajador, se ejecutan cinco compilaciones limpias y se toman el promedio de las últimas cuatro.
Figura 1: Grafo de mejoras de rendimiento de compilaciones limpias.
Para esta configuración, dos trabajadores realizan la compilación más rápida, aunque solo una mejora del 14% en comparación con un trabajador. Un trabajador es una buena opción si deseas usar menos memoria.
La compilación incremental generalmente se beneficia aún más. Las compilaciones limpias son relativamente poco frecuentes, pero cambiar un solo archivo entre compilaciones es común, en particular en el desarrollo basado en pruebas. En el ejemplo anterior, también hay algunas acciones de empaquetado que no son de Java que pueden sombrear el tiempo de compilación incremental.
Compilar solo las fuentes de Java (//src/main/java/com/google/devtools/build/lib/bazel:BazelServer_deploy.jar
) después de cambiar una constante de string interna en AbstractContainerizingSandboxedSpawn.java proporciona un aumento de 3 veces (un promedio de 20 compilaciones incrementales con una compilación de preparación descartada):
Figura 2: Grafo de mejoras en el rendimiento de las compilaciones incrementales
La velocidad dependerá del cambio que se realice. Un aumento de velocidad de un factor 6 se mide en la situación anterior cuando se cambia una constante de uso común.
Modifica trabajadores persistentes
Puedes pasar la marca --worker_extra_flag
para especificar marcas de inicio para trabajadores, con claves mnemotécnicas. Por ejemplo, si pasas --worker_extra_flag=javac=--debug
, solo se activará la depuración para Javac.
Solo se puede establecer una marca de trabajador por uso y solo para un parámetro mnemotécnico.
Los trabajadores no solo se crean por separado para cada mnemónico, sino también para las variaciones en las marcas de inicio. Cada combinación de marcas mnemónicas y de inicio se combina en un WorkerKey
, y para cada WorkerKey
se pueden crear hasta worker_max_instances
trabajadores. Consulta la siguiente sección para saber cómo la configuración de la acción también puede especificar marcas de configuración.
Puedes usar la marca --high_priority_workers
para especificar un mnemónico que se debe ejecutar antes que los mnemónicos de prioridad normal. Esto puede ayudar a priorizar las acciones que siempre están en la ruta
crítica. Si hay dos o más trabajadores de prioridad alta que ejecutan solicitudes, se impide que se ejecuten todos los demás trabajadores. Esta marca se puede usar varias veces.
Pasar la marca --worker_sandboxing
hace que cada solicitud de trabajador use un directorio de zona de pruebas independiente para todas sus entradas. La configuración de la zona de pruebas requiere tiempo adicional, en especial en macOS, pero garantiza una mejor precisión.
La marca --worker_quit_after_build
es útil principalmente para depurar y generar perfiles. Esta marca obliga a todos los trabajadores a salir una vez que se completa una compilación. También puedes pasar --worker_verbose
para obtener más resultados sobre lo que hacen los trabajadores. Esta marca se refleja en el campo verbosity
en WorkRequest
, lo que permite que las implementaciones de trabajadores también sean más detalladas.
Los trabajadores almacenan sus registros en el directorio <outputBase>/bazel-workers
, por ejemplo, /tmp/_bazel_larsrc/191013354bebe14fdddae77f2679c3ef/bazel-workers/worker-1-Javac.log
.
El nombre del archivo incluye el ID del trabajador y el nombre mnemónico. Dado que puede haber más de un WorkerKey
por mnemónica, es posible que veas más de worker_max_instances
archivos de registro para una mnemónica determinada.
Para obtener información sobre las compilaciones de Android, consulta los detalles en la página de rendimiento de compilación de Android.
Implementar trabajadores persistentes
Consulta la página sobre cómo crear trabajadores persistentes para obtener más información sobre cómo convertirlo.
En este ejemplo, se muestra una configuración de Starlark para un trabajador 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" }
)
Con esta definición, el primer uso de esta acción comenzaría con la ejecución de la línea de comandos /bin/some_compiler -max_mem=4G --persistent_worker
. Una solicitud para compilar Foo.java
se vería de la siguiente manera:
NOTA: Si bien la especificación del búfer de protocolo usa “caso de serpiente” (request_id
), el protocolo JSON usa “caso de camello” (requestId
). En este documento, usaremos el caso de camellos en los ejemplos de JSON, pero el caso de Snake cuando hables del campo, independientemente del protocolo.
{
"arguments": [ "-g", "-source", "1.5", "Foo.java" ]
"inputs": [
{ "path": "symlinkfarm/input1", "digest": "d49a..." },
{ "path": "symlinkfarm/input2", "digest": "093d..." },
],
}
El trabajador recibe esto en stdin
en formato JSON delimitado por saltos de línea (porque requires-worker-protocol
está configurado en JSON). Luego, el trabajador realiza la acción
y envía una WorkResponse
con formato JSON a Bazel en su stdout. Luego, Bazel
analiza esta respuesta y la convierte de forma manual en un proto WorkResponse
. Para comunicarse con el trabajador asociado mediante protobuf con codificación binaria en lugar de JSON, requires-worker-protocol
se configuraría como proto
, de la siguiente manera:
execution_requirements = {
"supports-workers" : "1" ,
"requires-worker-protocol" : "proto"
}
Si no incluyes requires-worker-protocol
en los requisitos de ejecución,
Bazel usará la comunicación de trabajador de forma predeterminada para usar protobuf.
Bazel deriva la WorkerKey
de las marcas mnemónicas y compartidas, por lo que, si esta
configuración permitiera cambiar el parámetro max_mem
, se generaría un trabajador independiente
para cada valor usado. Esto puede generar un consumo excesivo de memoria si se usan demasiadas variaciones.
Actualmente, cada trabajador solo puede procesar una solicitud a la vez. La función experimental de trabajadores multiplex permite usar varios subprocesos si la herramienta subyacente es multiproceso y el wrapper está configurado para entender esto.
En este repositorio de GitHub, puedes ver contenedores de trabajadores de ejemplo escritos en Java y en Python. Si trabajas en JavaScript o TypeScript, el paquete @bazel/worker y el ejemplo de trabajador nodejs pueden ser útiles.
¿Cómo afectan los trabajadores a la zona de pruebas?
Usar la estrategia worker
de forma predeterminada no ejecuta la acción en una zona de pruebas, similar a la estrategia local
. Puedes configurar la marca --worker_sandboxing
para ejecutar todos los trabajadores dentro de las zonas de pruebas y asegurarte de que cada ejecución de la herramienta solo vea los archivos de entrada que debe tener. Es posible que la herramienta aún filtre información entre solicitudes de forma interna, por ejemplo, a través de una caché. El uso de la estrategia dynamic
requiere que los trabajadores estén en una zona de pruebas.
Para permitir el uso correcto de las cachés del compilador con los trabajadores, se pasa un resumen junto con cada archivo de entrada. Por lo tanto, el compilador o el wrapper pueden verificar si la entrada sigue siendo válida sin tener que leer el archivo.
Incluso cuando se usan los resúmenes de entrada para protegerse del almacenamiento en caché no deseado, los trabajadores de la zona de pruebas ofrecen una zona de pruebas menos estricta que una zona de pruebas pura, ya que la herramienta puede mantener otro estado interno que se haya visto afectado por solicitudes anteriores.
Los trabajadores multiplex solo se pueden poner en zona de pruebas si la implementación de trabajadores lo admite, y esta zona de pruebas debe habilitarse por separado con la marca --experimental_worker_multiplex_sandboxing
. Obtén más información en el documento de diseño).
Lecturas adicionales
Para obtener más información sobre los trabajadores persistentes, consulta:
- Entrada de blog sobre trabajadores persistentes originales
- Descripción de la implementación de Haskell
- Entrada de blog de Mike Morearty
- Desarrollo de frontend con Bazel: Angular/TypeScript y trabajadores persistentes con Asana
- Estrategias de Bazel explicadas
- Discusión sobre la estrategia de los trabajadores informativos en la lista de distribución de la discusión del bazel