Trabajadores persistentes

Informar un problema Ver fuente Noche}

En esta página, se explica cómo usar trabajadores persistentes, sus beneficios, sus requisitos y cómo afectan a 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 en torno a la herramienta real (en general, un compilador) o es la herramienta en sí. Para beneficiarte de los trabajadores persistentes, la herramienta debe admitir una secuencia de compilaciones, y el wrapper debe traducirse entre la API de la herramienta y el formato de solicitud o 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, y es responsable de iniciar y comunicarse con la herramienta de forma adecuada, así como de cerrar a los trabajadores al salir. A cada instancia de trabajador se le asigna (pero no se le chroot) un directorio de trabajo separado 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 de JIT y habilita el almacenamiento en caché, por ejemplo, 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 y Kotlin, entre otros.

Los programas que usan un entorno de ejecución NodeJS pueden utilizar 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 ejecutan compilaciones, aunque la ejecución remota tiene prioridad. En el caso de las acciones que no admiten trabajadores persistentes, Bazel recurre a iniciar una instancia de herramienta para cada acción. Puedes configurar de forma explícita tu compilación para que use trabajadores persistentes si estableces la estrategia worker para la mnemotecnia de la herramienta correspondiente. Como práctica recomendada, en este ejemplo se incluye especificar local como un resguardo de la estrategia 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. La compilación de Bazel es aproximadamente 2.5 veces más rápida 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 a los trabajadores automáticamente, por lo que no es necesario especificar la estrategia worker, pero puedes usar local o sandboxed como resguardos.

Elige la cantidad de trabajadores

La cantidad predeterminada de instancias de trabajador por nombre mnemotécnico 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 JIT y aciertos de caché que obtienes. Con más trabajadores, más destinos pagarán los costos de inicio por ejecutar código que no sea JIT y acceder a cachés fríos. Si debes compilar una pequeña cantidad de objetivos, un solo trabajador puede ofrecer la mejor compensación entre la velocidad de compilación y el uso de recursos (por ejemplo, consulta el error #8586). La marca worker_max_instances establece la cantidad máxima de instancias de trabajador por conjunto mnemotécnico y de marcas (consulta a continuación), por lo que en un sistema mixto podrías terminar usando mucha memoria si mantienes el valor predeterminado. Para las compilaciones incrementales, el beneficio de múltiples instancias de trabajador es aún menor.

En este gráfico, se muestran los tiempos de compilación desde cero para Bazel (//src:bazel objetivo) en una estación de trabajo Linux con hipersubproceso de Intel Xeon de 3.5 GHz y 6 núcleos 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.

Gráfico de mejoras en el rendimiento de compilaciones limpias

Figura 1: Gráfico de mejoras en el rendimiento de compilaciones limpias.

Para esta configuración, dos trabajadores ofrecen 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 quieres usar menos memoria.

Por lo general, la compilación incremental se beneficia aún más. Las compilaciones limpias son relativamente raras, 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 reemplazar 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 cadena interna en AbstractContainerizingSandboxedSpawn.java da una aceleración 3 veces (en promedio, 20 compilaciones incrementales con una compilación de preparación descartada):

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

Figura 2: Gráfico de mejoras en el rendimiento de las compilaciones incrementales.

La aceleración depende del cambio que se realice. La aceleración 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 a los trabajadores, con una clave mnemotécnica. Por ejemplo, si se pasa --worker_extra_flag=javac=--debug, se activa la depuración solo para Javac. Solo se puede configurar una marca de trabajador por uso de esta marca, y solo para un nombre nemotécnico. Los trabajadores no solo se crean por separado para cada nombre mnemotécnico, sino también para las variaciones de 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 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 mnemotécnico que se debe ejecutar en lugar de los mnemotécnicos 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 alta prioridad ejecutando solicitudes, todos los demás trabajadores no podrán ejecutarse. 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. Configurar la sandbox lleva algo de tiempo adicional, especialmente en macOS, pero ofrece una mejor garantía de precisión.

La marca --worker_quit_after_build es útil, sobre todo, para depurar y generar perfiles. Esta marca obliga a todos los trabajadores a cerrarse una vez que se completa la 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 trabajador 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 nemotécnico. Dado que puede haber más de un WorkerKey por nombre mnemotécnico, es posible que veas más de archivos de registro worker_max_instances para un nombre mnemotécnico determinado.

Para las compilaciones de Android, consulta los detalles en la página de rendimiento de compilación de Android.

Cómo implementar trabajadores persistentes

Consulta la página sobre 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 "capitalización medial" (requestId). En este documento, se usa "mayúsculas y minúsculas" en los ejemplos de JSON, pero Snake case cuando se hable sobre el 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 se configura en 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 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 establecerá de forma predeterminada la comunicación del trabajador para usar protobuf.

Bazel deriva el WorkerKey de la marca mnemotécnica y de las marcas compartidas, por lo que, si esta configuración permitiera cambiar el parámetro max_mem, se generaría un trabajador separado para cada valor usado. Esto puede generar un consumo de memoria excesivo si se usan demasiadas variaciones.

Por el momento, 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 tiene varios subprocesos y el wrapper está configurado para entender esto.

En este repositorio de GitHub, puedes ver ejemplos de wrappers de trabajador escritos en Java y en Python. Si trabajas en JavaScript o TypeScript, el paquete@bazel/worker y el ejemplo de nodejs worker pueden ser útiles.

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

El uso de la estrategia worker de forma predeterminada no ejecuta la acción en una sandbox, similar a la estrategia local. Puedes configurar la marca --worker_sandboxing para que ejecute todos los trabajadores dentro de zonas de pruebas y asegurarte de que cada ejecución de la herramienta solo vea los archivos de entrada que debe tener. La herramienta aún puede filtrar información entre solicitudes de manera 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 trabajadores, se pasa un resumen junto con cada archivo de entrada. Por lo tanto, el compilador o el wrapper pueden comprobar si la entrada aún es válida sin tener que leer el archivo.

Incluso cuando se usan los resúmenes de entradas 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 vio afectado por solicitudes anteriores.

Los trabajadores multiplex solo pueden incluirse en una zona de pruebas si la implementación del trabajador los admite, y esta zona de pruebas debe habilitarse por separado con la marca --experimental_worker_multiplex_sandboxing. Consulta más detalles en el documento de diseño).

Lecturas adicionales

Para obtener más información sobre los trabajadores persistentes, consulta: