Trabajadores persistentes

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í misma. Para beneficiarse de los trabajadores persistentes, la herramienta debe admitir la realización de 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 la marca --persistent_worker y sin ella en la misma compilación, y es responsable de iniciar y hablar con la herramienta de manera adecuada, así como de cerrar 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 a otros...

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 habilita, por ejemplo, el almacenamiento en caché de los á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 muchos más.

Los programas que usan un entorno de ejecución NodeNode pueden usar la biblioteca de ayuda @bazel/worker para implementar el protocolo de trabajador.

Usar 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 a una instancia de herramienta para cada acción. Puedes configurar tu compilación de forma explícita para usar trabajadores persistentes si estableces la estrategia de worker para los recursos nemotécnicos aplicables de la herramienta. 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 aumentar 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 detalles, consulta la sección Elige 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 ejecución remota y una de trabajador. Para habilitar la estrategia dinámica, pasa la marca --experimental_spawn_scheduler. Esta estrategia habilita automáticamente a los trabajadores, por lo que no es necesario especificar la estrategia worker, pero aún puedes usar local o sandboxed como resguardos.

Elección de 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 hacer un buen uso de las CPU disponibles y la cantidad de compilación de JIT y aciertos de caché que obtienes. Con más trabajadores, más destinos pagarán costos de inicio por ejecutar código que no sea JIT y golpear cachés en frío. Si tienes una pequeña cantidad de objetivos 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.° 8586). La marca worker_max_instances establece la cantidad máxima de instancias de trabajador por nemotécnico y conjunto de marcas (consulta a continuación), por lo que, en un sistema mixto, podrías terminar usando bastante memoria si conservas el valor predeterminado. Para compilaciones incrementales, el beneficio de múltiples instancias de trabajador es aún más pequeño.

En este gráfico, se muestran los tiempos de compilación desde cero para Bazel (objetivo //src:bazel) en una estación de trabajo Intel Xeon de 3.5 GHz, hipersubproceso, con 64 GB de RAM. Para cada configuración de trabajador, se ejecutan cinco compilaciones limpias y se toma el promedio de las últimas cuatro.

Grafo de mejoras de rendimiento de compilaciones limpias

Figura 1: Grafo de mejoras de rendimiento de 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 beneficia aún más. Las compilaciones limpias son relativamente poco comunes, pero es común cambiar un solo archivo entre compilaciones, especialmente en el desarrollo basado en pruebas. En el ejemplo anterior, también hay algunas acciones de empaquetado que no son de Java y que pueden oscurecer el tiempo de compilación incremental.

Volver a compilar las fuentes de Java solo (//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 una velocidad 3 veces mayor (un promedio de 20 compilaciones incrementales con una compilación de preparación descartada):

Gráfico de mejoras en el rendimiento de las compilaciones incrementales

Figura 2: Grafo de mejoras de rendimiento de compilaciones incrementales.

La aceleración depende del cambio que se realice. Una aceleración de un factor 6 se mide en la situación anterior cuando se cambia una constante de uso común.

Modificar trabajadores persistentes

Puedes pasar la marca --worker_extra_flag a fin de especificar marcas de inicio a los trabajadores, con claves nemotécnicas. Por ejemplo, pasar --worker_extra_flag=javac=--debug solo activa la depuración para Javac. Solo se puede establecer una marca de trabajador por uso de esta marca y solo para una mnemotécnica. 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 marcas mnemotécnicas 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 descubrir 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 valor mnemotécnico que se deberá ejecutar en lugar de los de prioridad normal. Esto puede 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 evita que todos los demás trabajadores se ejecuten. 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 separado para todas sus entradas. La configuración de la zona de pruebas requiere tiempo adicional, en especial 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 fuerza a todos los trabajadores a cerrarse 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 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 de trabajador y el nombre nemotécnico. Como 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 de rendimiento de la compilación de Android.

Implementar trabajadores persistentes

Consulta la página sobre cómo crear trabajadores persistentes para obtener información sobre cómo crear un trabajador.

En este ejemplo, se muestra la 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:

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 manualmente 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 establecerá la comunicación de trabajador predeterminada para usar protobuf.

Bazel deriva la WorkerKey de las marcas mnemotécnicas 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 provocar un consumo excesivo de la memoria si se usan demasiadas variaciones.

Actualmente, cada trabajador solo puede procesar una solicitud a la vez. La función experimental múltiples trabajadores permite usar varios subprocesos si la herramienta subyacente es de varios subprocesos y el wrapper está configurado para comprender esto.

En este repositorio 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 trabajadores de nodejs podrían ser útiles.

¿Cómo afectan los trabajadores a la zona de pruebas?

Cuando usas la estrategia worker de forma predeterminada, no se ejecuta la acción en una zona de pruebas, similar a la estrategia de 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 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 zona de pruebas.

Para permitir el uso correcto de las cachés del compilador con trabajadores, se pasa un resumen junto con cada archivo de entrada. Por lo tanto, el compilador o el wrapper pueden verificar si la entrada aún es 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 incluir en una zona de pruebas si su implementación es compatible, 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, consulte los siguientes artículos: