En esta página, se explica cómo usar trabajadores persistentes, sus beneficios y requisitos, y cómo afectan al aislamiento de procesos.
Un trabajador persistente es un proceso de larga duración que inicia el servidor de Bazel y que funciona como un wrapper alrededor de la herramienta real (por lo general, un compilador) o es la herramienta en sí. Para aprovechar los beneficios 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. Es posible que se llame al mismo trabajador con y sin la marca --persistent_worker
en la misma compilación, y es responsable de iniciar y comunicarse con la herramienta de manera adecuada, así como de cerrar los trabajadores al salir. A cada instancia de trabajador se le asigna (pero no se le hace chroot) un directorio de trabajo independiente en <outputBase>/bazel-workers
.
El uso de trabajadores persistentes es una estrategia de ejecución que disminuye la sobrecarga de inicio, permite una mayor compilación JIT y habilita el almacenamiento en caché, por ejemplo, de los árboles de sintaxis abstracta en la ejecución de acciones. Esta estrategia logra estas mejoras enviando varias solicitudes a un proceso de larga duración.
Los trabajadores persistentes se implementan para varios lenguajes, incluidos Java, Scala, Kotlin y muchos más.
Los programas que usan un entorno de ejecución de 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. En el caso de las acciones que no admiten trabajadores persistentes, Bazel recurre al inicio de una instancia de herramienta para cada acción. Puedes configurar explícitamente tu compilación para que use trabajadores persistentes si estableces la estrategia worker
para los mnemónicos de herramientas aplicables. Como práctica recomendada, este ejemplo incluye la especificación de local
como alternativa a la estrategia worker
:
bazel build //my:target --strategy=Javac=worker,local
Usar la estrategia de trabajadores en lugar de la estrategia local puede aumentar la velocidad de compilación de forma significativa, según la implementación. En el caso de Java, las compilaciones pueden ser de 2 a 4 veces más rápidas y, a veces, incluso más rápidas para la compilación incremental. La compilación de Bazel es aproximadamente 2.5 veces más rápida con trabajadores. Para obtener más detalles, consulta la sección "Cómo elegir la cantidad 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 carrera entre una ejecución remota y una ejecución de trabajador. Para habilitar la estrategia dinámica, pasa la marca --experimental_spawn_scheduler. Esta estrategia habilita automáticamente los trabajadores, por lo que no es necesario especificar la estrategia worker
, pero aún puedes usar local
o sandboxed
como alternativas.
Cómo elegir la cantidad de trabajadores
La cantidad predeterminada de instancias de trabajador por mnemónico es 4, pero se puede ajustar con la marca worker_max_instances
. Existe una compensación entre el buen uso de las CPU disponibles y la cantidad de compilaciones JIT y aciertos de caché que obtienes. Con más trabajadores, más destinos pagarán los costos de inicio de la ejecución de código no compilado con JIT y de los accesos a la caché fría. Si tienes una pequeña cantidad de destinos para compilar, un solo trabajador puede ofrecer la mejor compensación entre la velocidad de compilación y el uso de recursos (por ejemplo, consulta el problema #8586).
La marca worker_max_instances
establece la cantidad máxima de instancias de trabajador por conjunto de marcas y mnemónicos (consulta a continuación), por lo que, en un sistema mixto, podrías terminar usando mucha memoria si mantienes el valor predeterminado. En el caso de las compilaciones incrementales, el beneficio de tener varias instancias de trabajadores es aún menor.
En este gráfico, se muestran los tiempos de compilación desde cero para Bazel (destino //src:bazel
) en una estación de trabajo Linux Intel Xeon de 6 núcleos con hiperprocesamiento de 3.5 GHz y 64 GB de RAM. Para cada configuración de trabajador, se ejecutan cinco compilaciones limpias y se toma el promedio de las últimas cuatro.
Figura 1: Gráfico de las mejoras de rendimiento de las compilaciones limpias.
Para esta configuración, dos trabajadores proporcionan la compilación más rápida, aunque con una mejora de solo el 14% en comparación con un trabajador. Un trabajador es una buena opción si quieres usar menos memoria.
Por lo general, la compilación incremental es aún más beneficiosa. 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. El ejemplo anterior también tiene algunas acciones de empaquetado que no son de Java y que pueden eclipsar el tiempo de compilación incremental.
Volver a 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 cadena interna en AbstractContainerizingSandboxedSpawn.java proporciona una aceleración de 3 veces (promedio de 20 compilaciones incrementales con una compilación de preparación descartada):
Figura 2: Gráfico de las mejoras de rendimiento de las compilaciones incrementales.
La aceleración depende del cambio que se realice. En la situación anterior, se mide una aceleración de un factor 6 cuando se cambia una constante de uso común.
Cómo modificar trabajadores persistentes
Puedes pasar la marca --worker_extra_flag
para especificar marcas de inicio para los trabajadores, con claves mnemónicas. Por ejemplo, pasar --worker_extra_flag=javac=--debug
activa la depuración solo para Javac.
Solo se puede establecer una marca de trabajador por uso de esta marca y solo para una mnemónica.
Los trabajadores no solo se crean por separado para cada mnemónico, sino también para las variaciones en sus marcas de inicio. Cada combinación de mnemónico y marcas 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 una mnemotecnia que se debe ejecutar con preferencia a las mnemotecnias de prioridad normal. Esto puede ayudarte a priorizar las acciones que siempre están en la ruta crítica. Si hay dos o más trabajadores de alta prioridad que ejecutan solicitudes, se impide que se ejecuten todos los demás trabajadores. Esta marca se puede usar varias veces.
Si pasas la marca --worker_sandboxing
, cada solicitud de trabajador usará un directorio de zona de pruebas independiente para todas sus entradas. Configurar la zona de pruebas lleva un poco más de tiempo, especialmente en macOS, pero ofrece una mejor garantía de corrección.
La marca --worker_quit_after_build
es útil principalmente para la depuración y la generación de 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 información sobre lo que hacen los trabajadores. Esta marca se refleja en el campo verbosity
de 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 la mnemónica. Dado que puede haber más de un WorkerKey
por mnemónico, es posible que veas más de worker_max_instances
archivos de registro para un mnemónico determinado.
Para las compilaciones de Android, consulta los detalles en la página Rendimiento de la compilación de Android.
Implementa trabajadores persistentes
Consulta la página Cómo crear trabajadores persistentes para obtener más información sobre cómo crear un trabajador.
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 "snake case" (request_id
), el protocolo JSON usa "camel case" (requestId
). En este documento, usaremos camel case en los ejemplos de JSON, pero snake case cuando hablemos 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 como JSON). Luego, el trabajador realiza la acción y envía un WorkResponse
con formato JSON a Bazel en su stdout. Luego, Bazel analiza esta respuesta y la convierte manualmente en un proto WorkResponse
. Para comunicarse con el trabajador asociado usando protobuf codificado de forma binaria en lugar de JSON, requires-worker-protocol
se establecería en 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 establecerá de forma predeterminada que la comunicación del trabajador use protobuf.
Bazel deriva el WorkerKey
del mnemónico y las marcas compartidas, por lo que, si esta configuración permitiera cambiar el parámetro max_mem
, se generaría un trabajador independiente para cada valor utilizado. Esto puede provocar 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 multiplex workers permite usar varios subprocesos si la herramienta subyacente es de subprocesos múltiples y el wrapper está configurado para comprender esto.
En este repo de GitHub, puedes ver ejemplos de wrappers de trabajadores escritos en Java y en Python. Si trabajas en JavaScript o TypeScript, el paquete@bazel/worker y el ejemplo de trabajador de Node.js pueden ser útiles.
¿Cómo afectan los trabajadores al aislamiento?
Usar la estrategia worker
de forma predeterminada no ejecuta la acción en un sandbox, de manera similar a la estrategia local
. Puedes establecer la marca --worker_sandboxing
para ejecutar todos los trabajadores dentro de zonas de pruebas, lo que garantiza que cada ejecución de la herramienta solo vea los archivos de entrada que debería tener. Es posible que la herramienta siga filtrando 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 un entorno de pruebas.
Para permitir el uso correcto de las memorias caché 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 resúmenes de entrada para evitar el almacenamiento en caché no deseado, los trabajadores de 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 de multiplexación solo se pueden ejecutar en un entorno de pruebas si la implementación del trabajador lo admite, y este entorno de pruebas se debe habilitar por separado con la marca --experimental_worker_multiplex_sandboxing
. Obtén más detalles en el documento de diseño.
Lecturas adicionales
Para obtener más información sobre los trabajadores persistentes, consulta los siguientes recursos:
- Entrada de blog original sobre los trabajadores persistentes
- Descripción de la implementación de Haskell
- Entrada de blog de Mike Morearty
- Desarrollo de front-end con Bazel: Angular/TypeScript y trabajadores persistentes con Asana
- Explicación de las estrategias de Bazel
- Debate informativo sobre la estrategia de trabajadores en la lista de distribución de bazel-discuss