Compilaciones distribuidas

Informar un problema Ver código fuente Nightly · 8.0 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Cuando tienes una base de código grande, las cadenas de dependencias pueden volverse muy profundas. Incluso los objetos binarios simples suelen depender de decenas de miles de destinos de compilación. A esta escala, es simplemente imposible completar una compilación en un tiempo razonable en una sola máquina: ningún sistema de compilación puede eludir las leyes fundamentales de la física impuestas en el hardware de una máquina. La única manera de que esto funcione es con un sistema de compilación que admita compilaciones distribuidas en las que las unidades de trabajo que realiza el sistema se distribuyen en una cantidad arbitraria y escalable de máquinas. Si suponemos que dividimos el trabajo del sistema en unidades lo suficientemente pequeñas (hablaremos más sobre esto más adelante), esto nos permitiría completar cualquier compilación de cualquier tamaño lo más rápido que estemos dispuestos a pagar. Esta escalabilidad es el santo grial con el que hemos estado trabajando definiendo un sistema de compilación basado en artefactos.

Almacenamiento en caché remota

El tipo más simple de compilación distribuida es aquel que solo aprovecha la caché remota, que se muestra en la Figura 1.

Compilación distribuida con almacenamiento en caché remoto

Figura 1. Una compilación distribuida que muestra el almacenamiento en caché remoto

Cada sistema que realiza compilaciones, incluidas las estaciones de trabajo de los desarrolladores y los sistemas de integración continua, comparte una referencia a un servicio de almacenamiento en caché remoto común. Este servicio puede ser un sistema de almacenamiento a corto plazo rápido y local, como Redis, o un servicio en la nube, como Google Cloud Storage. Cada vez que un usuario necesita compilar un artefacto, ya sea directamente o como dependencia, el sistema primero verifica con la caché remota para ver si ese artefacto ya existe allí. Si es así, puede descargar el artefacto en lugar de compilarlo. De lo contrario, el sistema compila el artefacto y vuelve a subir el resultado a la caché. Esto significa que las dependencias de bajo nivel que no cambian con frecuencia se pueden compilar una vez y compartir entre los usuarios, en lugar de que cada usuario tenga que volver a compilarlas. En Google, muchos artefactos se entregan desde una caché en lugar de compilarse desde cero, lo que reduce en gran medida el costo de ejecutar nuestro sistema de compilación.

Para que funcione un sistema de almacenamiento en caché remoto, el sistema de compilación debe garantizar que las compilaciones sean completamente reproducibles. Es decir, para cualquier destino de compilación, debe ser posible determinar el conjunto de entradas para ese destino de modo que el mismo conjunto de entradas produzca exactamente el mismo resultado en cualquier máquina. Esta es la única manera de garantizar que los resultados de la descarga de un artefacto sean los mismos que los de la compilación por parte del usuario. Ten en cuenta que esto requiere que cada artefacto de la caché se clave en su destino y en un hash de sus entradas. De esta manera, diferentes ingenieros podrían realizar diferentes modificaciones en el mismo destino al mismo tiempo, y la caché remota almacenaría todos los artefactos resultantes y los entregaría de forma adecuada sin conflictos.

Por supuesto, para que una caché remota tenga algún beneficio, la descarga de un artefacto debe ser más rápida que su compilación. Esto no siempre es así, en especial, si el servidor de caché está lejos de la máquina que realiza la compilación. La red y el sistema de compilación de Google están ajustados cuidadosamente para poder compartir rápidamente los resultados de la compilación.

Ejecución remota

El almacenamiento en caché remoto no es una compilación distribuida real. Si se pierde la caché o si realizas un cambio de bajo nivel que requiere que se vuelva a compilar todo, deberás realizar la compilación completa de forma local en tu máquina. El verdadero objetivo es admitir la ejecución remota, en la que el trabajo real de realizar la compilación se puede distribuir entre cualquier cantidad de trabajadores. En la Figura 2, se muestra un sistema de ejecución remota.

Sistema de ejecución remota

Figura 2. Un sistema de ejecución remoto

La herramienta de compilación que se ejecuta en la máquina de cada usuario (en la que los usuarios son ingenieros humanos o sistemas de compilación automatizados) envía solicitudes a un compilador principal central. El maestro de compilación divide las solicitudes en sus acciones de componentes y programa la ejecución de esas acciones en un grupo escalable de trabajadores. Cada trabajador realiza las acciones que se le solicitan con las entradas que especifica el usuario y escribe los artefactos resultantes. Estos artefactos se comparten entre las otras máquinas que ejecutan acciones que los requieren hasta que se puede producir el resultado final y enviarlo al usuario.

La parte más complicada de implementar un sistema de este tipo es administrar la comunicación entre los trabajadores, el principal y la máquina local del usuario. Los trabajadores pueden depender de artefactos intermedios producidos por otros trabajadores, y el resultado final se debe enviar a la máquina local del usuario. Para ello, podemos basarnos en la caché distribuida que se describió anteriormente haciendo que cada trabajador escriba sus resultados en la caché y lea sus dependencias desde ella. El maestro bloquea a los trabajadores para que no continúen hasta que termine todo de lo que dependen, en cuyo caso podrán leer sus entradas desde la caché. El producto final también se almacena en caché, lo que permite que la máquina local lo descargue. Ten en cuenta que también necesitamos un medio independiente para exportar los cambios locales en el árbol de origen del usuario para que los trabajadores puedan aplicar esos cambios antes de compilar.

Para que esto funcione, todas las partes de los sistemas de compilación basados en artefactos que se describieron antes deben unirse. Los entornos de compilación deben ser completamente autodescriptivos para que podamos iniciar trabajadores sin intervención humana. Los procesos de compilación deben ser completamente independientes, ya que cada paso se puede ejecutar en una máquina diferente. Los resultados deben ser completamente deterministas para que cada trabajador pueda confiar en los resultados que recibe de otros trabajadores. Es muy difícil que un sistema basado en tareas proporcione esas garantías, lo que hace que sea casi imposible compilar un sistema de ejecución remota confiable sobre uno.

Compilaciones distribuidas en Google

Desde 2008, Google usa un sistema de compilación distribuido que emplea la ejecución y el almacenamiento en caché remotos, como se ilustra en la Figura 3.

Sistema de compilación de alto nivel

Figura 3. Sistema de compilación distribuido de Google

La caché remota de Google se llama ObjFS. Consiste en un backend que almacena los resultados de la compilación en Bigtables distribuidos en nuestra flota de máquinas de producción y un daemon de FUSE de frontend llamado objfsd que se ejecuta en la máquina de cada desarrollador. El daemon de FUSE permite a los ingenieros explorar los resultados de la compilación como si fueran archivos normales almacenados en la estación de trabajo, pero con el contenido del archivo descargado a pedido solo para los pocos archivos que el usuario solicita directamente. La entrega del contenido de los archivos a pedido reduce en gran medida el uso de la red y el disco, y el sistema puede compilar el contenido dos veces más rápido en comparación con cuando almacenamos todo el resultado de la compilación en el disco local del desarrollador.

El sistema de ejecución remota de Google se llama Forge. Un cliente de Forge en Blaze (el equivalente interno de Bazel) llamado el distribuidor envía solicitudes para cada acción a un trabajo que se ejecuta en nuestros centros de datos llamado el programador. El programador mantiene una caché de los resultados de la acción, lo que le permite mostrar una respuesta de inmediato si cualquier otro usuario del sistema ya creó la acción. De lo contrario, coloca la acción en una fila. Un gran grupo de trabajos de Executor lee acciones de esta cola de forma continua, los ejecuta y almacena los resultados directamente en las Bigtables de ObjFS. Estos resultados están disponibles para los ejecutores para acciones futuras o para que el usuario final los descargue a través de objfsd.

El resultado final es un sistema que se escala para admitir de manera eficiente todas las compilaciones que se realizan en Google. Además, la escala de las compilaciones de Google es realmente masiva: Google ejecuta millones de compilaciones que ejecutan millones de casos de prueba y producen petabytes de resultados de compilación a partir de miles de millones de líneas de código fuente todos los días. Un sistema de este tipo no solo permite que nuestros ingenieros compilen bases de código complejas con rapidez, sino que también nos permite implementar una gran cantidad de herramientas y sistemas automatizados que dependen de nuestra compilación.