Compilaciones distribuidas

Informa un problema Ver código fuente

Cuando tienes una gran base de código, las cadenas de dependencias pueden ser muy profundas. Incluso los objetos binarios simples a menudo pueden depender de decenas de miles de destinos de compilación. En esta escala, es imposible completar una compilación en un tiempo razonable en una sola máquina: ningún sistema de compilación puede evitar las leyes fundamentales de la física que se imponen en el hardware de una máquina. La única forma de hacer este trabajo es con un sistema de compilación que admita compilaciones distribuidas en las que las unidades de trabajo que realiza el sistema estén distribuidas en una cantidad arbitraria y escalable de máquinas. Si suponemos que dividimos el trabajo del sistema en unidades lo suficientemente pequeñas (más adelante, esto nos permitiría completar cualquier compilación de cualquier tamaño tan rápido como estemos dispuestos a pagar). Esta escalabilidad es el espíritu supremo en el que trabajamos para definir un sistema de compilación basado en artefactos.

Almacenamiento en caché remota

El tipo de compilación distribuida más simple es uno que solo aprovecha el almacenamiento en caché remoto, como 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

Todos los sistemas que realizan compilaciones, incluidas las estaciones de trabajo para desarrolladores y los sistemas de integración continua, comparten una referencia a un servicio común de caché remota. Este servicio puede ser un sistema de almacenamiento a corto plazo local y rápido 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 una dependencia, el sistema primero verifica con la caché remota para ver si ese artefacto ya existe. 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 nivel bajo que no cambian con mucha frecuencia se pueden compilar una vez y se pueden compartir entre los usuarios, en lugar de tener que volver a compilarlas. En Google, muchos artefactos se entregan desde una caché y no desde cero, lo que reduce en gran medida el costo de ejecutar nuestro sistema de compilación.

Para que un sistema de almacenamiento en caché remoto funcione, el sistema de compilación debe garantizar que las compilaciones se puedan reproducir por completo. Es decir, para cualquier destino de compilación, debe ser posible determinar el conjunto de entradas a ese destino, de modo que el mismo conjunto de entradas produzca exactamente el mismo resultado en cualquier máquina. Esta es la única forma de garantizar que los resultados de la descarga de un artefacto sean los mismos que los de compilarlo uno mismo. Ten en cuenta que esto requiere que cada artefacto de la caché se vincule en su destino y en un hash de sus entradas. De esa manera, distintos ingenieros pueden realizar modificaciones distintas al mismo destino al mismo momento, y la caché remota almacenaría todos los artefactos resultantes y los publicaría de forma correcta sin causar ningún conflicto.

Por supuesto, para que exista un beneficio de una caché remota, 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 cuidadosamente configurados para poder compartir resultados de compilación con rapidez.

Ejecución remota

El almacenamiento en caché remoto no es una verdadera compilación distribuida. Si se pierde la caché o si realizas un cambio de bajo nivel que requiere que todo se vuelva a compilar, deberás realizar toda la compilación de forma local en tu máquina. El verdadero objetivo es admitir la ejecución remota, en la que el trabajo real de 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 remota

La herramienta de compilación que se ejecuta en la máquina de cada usuario (ya sean ingenieros humanos o sistemas de compilación automatizada) envía solicitudes a una instancia principal de compilación central. La instancia principal de compilación divide las solicitudes en 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 el resultado final se puede producir y enviar al usuario.

La parte más difícil de implementar ese sistema es administrar la comunicación entre los trabajadores, la instancia principal y la máquina local del usuario. Los trabajadores pueden depender de artefactos intermedios producidos por otros trabajadores, y el resultado final debe enviarse de vuelta a la máquina local del usuario. Para ello, podemos compilar sobre la caché distribuida descrita antes si hacemos que cada trabajador escriba los resultados y lea sus dependencias desde la caché. La instancia principal bloquea a los trabajadores para que no continúen hasta que finalice la tarea de la que dependan, 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 fuente del usuario a fin de que los trabajadores puedan aplicar esos cambios antes de la compilación.

Para que esto funcione, todas las partes de los sistemas de compilación basados en artefactos descritos antes deben estar unidas. Los entornos de compilación deben ser autodescriptivos para poder iniciar trabajadores sin intervención humana. Los procesos de compilación deben ser independientes, ya que cada paso puede ejecutarse en una máquina diferente. Los resultados deben ser completamente deterministas de modo que cada trabajador pueda confiar en los resultados que recibe de otros trabajadores. Tales garantías son muy difíciles de proporcionar para un sistema basado en tareas, lo que hace que sea casi imposible compilar un sistema de ejecución remoto confiable sobre uno.

Compilaciones distribuidas en Google

Desde 2008, Google usa un sistema de compilación distribuido que emplea almacenamiento en caché remoto y ejecución remota, 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 resultados de compilaciones en Bigtable distribuidos en toda 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 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 de forma directa. La entrega de contenido de archivos a pedido reduce en gran medida el uso de la red y del disco, y el sistema puede compilar el doble de rápido que cuando almacenamos todos los resultados de 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 Scheduler. El programador mantiene una caché de resultados de acciones, lo que le permite mostrar una respuesta de inmediato si otro usuario del sistema ya creó la acción. De lo contrario, coloca la acción en una cola. Un grupo grande de trabajos de ejecutor lee de forma continua las acciones de esta cola, las ejecuta y almacena los resultados directamente en los Bigtable de ObjFS. Estos resultados están disponibles para los ejecutores para acciones futuras o para que el usuario final los descargue mediante objfsd.

El resultado final es un sistema que se escala para admitir de forma eficiente todas las compilaciones realizadas en Google. Y la escala de las compilaciones de Google es realmente enorme: Google ejecuta millones de compilaciones que ejecutan millones de casos de prueba y produce petabytes de resultados de compilación a partir de miles de millones de líneas de código fuente todos los días. Este sistema no solo permite a nuestros ingenieros compilar bases de código complejas con rapidez, sino que también nos permite implementar una gran cantidad de herramientas y sistemas automatizados que se basan en nuestra compilación.