Quando você tem uma grande base de código, as cadeias de dependências podem se tornar muito profundas. Até os binários simples podem depender de dezenas de milhares de destinos de compilação. Em nessa escala, é simplesmente impossível concluir um build em um valor razoável em uma única máquina: nenhum sistema de build consegue contornar as configurações leis da física impostas ao hardware de uma máquina. A única maneira de fazer isso funcionar é com um sistema de compilação que suporta builds distribuídos, em que as unidades de do trabalho sendo realizado pelo sistema são espalhados por uma área arbitrária e de máquinas. Supondo que o trabalho do sistema tenha sido dividido unidades (falaremos mais sobre isso posteriormente), isso nos permitiria concluir qualquer criação de qualquer tamanho o mais rápido que estivermos dispostos a pagar. Essa escalabilidade é o Santo Graal para definir um sistema de build baseado em artefatos.
Armazenamento em cache remoto
O tipo mais simples de build distribuído é aquele que usa apenas o remote armazenamento em cache, mostrado na Figura 1.
Figura 1. Uma versão distribuída mostrando armazenamento em cache remoto
Todos os sistemas que executam builds, incluindo estações de trabalho do desenvolvedor e de integração contínua, compartilha uma referência a um cache remoto comum serviço. Esse serviço pode ser um sistema de armazenamento de curto prazo rápido e local, como o Redis ou um serviço em nuvem como o Google Cloud Storage. Sempre que um usuário precisa criar um artefato, seja diretamente ou como uma dependência, o sistema primeiro verifica com o cache remoto para ver se esse artefato já existe lá. Nesse caso, fazer o download do artefato em vez de criá-lo. Caso contrário, o sistema cria a artefato em si e faz upload do resultado de volta para o cache. Isso significa que dependências de baixo nível que não mudam com muita frequência podem ser criadas uma vez e compartilhadas entre os usuários, sem precisar ser recriada por cada uma delas. No Google, muitos os artefatos são disponibilizados em cache, não criados do zero. reduzindo o custo de execução do nosso sistema de build.
Para que um sistema de armazenamento em cache remoto funcione, o sistema de compilação precisa garantir que os builds sejam completamente reproduzíveis. Ou seja, para qualquer destino de build, deve ser possível determinar o conjunto de entradas para esse destino, de modo que o mesmo conjunto de entradas vai produzir exatamente a mesma saída em qualquer máquina. Essa é a única maneira de Garantir que os resultados do download de um artefato sejam iguais aos resultados. de construir por conta própria. Observe que isso exige que cada artefato no cache ser codificados no destino e em um hash das entradas, os engenheiros podem fazer modificações diferentes no mesmo alvo com o mesmo tempo de execução, e o cache remoto armazena todos os artefatos resultantes e veicula adequadamente sem conflito.
Para que haja algum benefício com um cache remoto, fazer o download artefato precisa ser mais rápida do que criá-lo. Nem sempre é o caso, especialmente se o servidor de cache estiver longe da máquina que faz o build. As O sistema de build e a rede é cuidadosamente ajustado para compartilhar rapidamente resultados.
Execução remota
O armazenamento em cache remoto não é um build distribuído de verdade. Se o cache for perdido ou se você fazer uma mudança de baixo nível que exija reconstrução de tudo, você ainda precisa para executar toda a compilação localmente em sua máquina. O verdadeiro objetivo é apoiar execução remota, na qual o trabalho real de fazer a compilação pode ser espalhado em qualquer número de workers. A Figura 2 ilustra um sistema de execução remota.
Figura 2. Um sistema de execução remota
a ferramenta de criação executada na máquina de cada usuário, em que os usuários são humanos engenheiros ou sistemas de compilação automatizados) envia solicitações para um mestre de compilação central. O mestre de build divide as solicitações em ações e programações dos componentes. a execução dessas ações em um pool escalonável de workers. Cada worker executa as ações solicitadas com as entradas especificadas pelo usuário e e grava os artefatos resultantes. Esses artefatos são compartilhados entre os outros máquinas virtuais que executam ações que exigem delas até que a saída final seja produzidos e enviados ao usuário.
A parte mais complicada de implementar esse sistema é gerenciar a comunicação entre os workers, o mestre e a máquina local do usuário. Workers poderiam dependem de artefatos intermediários produzidos por outros workers e o resultado precisam ser enviados de volta à máquina local do usuário. Para fazer isso, podemos aproveitar do cache distribuído descrito anteriormente, fazendo com que cada worker grave e ler as dependências dele no cache. Os blocos principais de prosseguir até que tudo de que dependem seja concluído, ou seja, nesse caso, eles poderão ler as entradas do cache. O produto final armazenados em cache, permitindo o download pela máquina local. Também precisamos meios separados de exportar as alterações locais na árvore de origem do usuário, de modo que os workers possam aplicar essas alterações antes da criação.
Para que isso funcione, todas as partes dos sistemas de compilação baseados em artefatos descritos precisam se unir. Os ambientes de build devem ser totalmente autodescritivo, para que possamos provisionar trabalhadores sem intervenção humana. Criação e processos precisam ser totalmente autônomos, porque cada etapa pode ser executados em outra máquina. As saídas precisam ser completamente deterministas, para que cada worker confie nos resultados que recebe de outros. Essas garantias são extremamente difíceis para um sistema baseado em tarefas fornecer, o que torna quase impossível construir um sistema de execução remota confiável um.
Builds distribuídos no Google
Desde 2008, o Google tem usado um sistema de build distribuído que emprega armazenamento em cache e execução remota, ilustrados na figura 3.
Figura 3. Sistema de build distribuído do Google
O cache remoto do Google é chamado de ObjFS. Ele consiste em um back-end que armazena criar saídas em Bigtables distribuídas por toda nossa frota de produção e um daemon de front-end do FUSE chamado objfsd, que é executado no sistema máquina virtual. O daemon do FUSE permite que os engenheiros procurem as saídas de build como se estivessem eram arquivos normais armazenados na estação de trabalho, mas com o conteúdo do arquivo baixado sob demanda apenas para os arquivos que são solicitados diretamente pelo usuário. A disponibilização de conteúdos de arquivos sob demanda reduz bastante a rede e o disco e o sistema é capaz de criar duas vezes mais rápido do que quando armazenamos todas as saídas de build no disco local do desenvolvedor.
O sistema de execução remota do Google se chama Forge. Um cliente Forge no Blaze (equivalente interno do Bazel) chamado o distribuidor envia solicitações de cada ação a um job em execução na datacenters chamados de Programador. O Programador mantém um cache de ação resultados, permitindo o retorno de uma resposta imediatamente se a ação já tiver sido criado por outro usuário do sistema. Caso contrário, ele coloca a ação uma fila. Um grande pool de jobs do Executor lê continuamente as ações dessa fila. executá-los e armazenar os resultados diretamente nos Bigtables do ObjFS. Esses os resultados ficam disponíveis aos executores para ações futuras ou para download. pelo usuário final via objfsd.
O resultado final é um sistema que pode ser escalonado para oferecer suporte eficiente a todos os builds realizada no Google. E a escala dos builds do Google é realmente enorme: executa milhões de builds, executa milhões de casos de teste e produz petabytes de resultados de build com bilhões de linhas de código-fonte todos os dias. Além de esse sistema permite que nossos engenheiros criem rapidamente bases de código complexas, além de permitir implementar um grande número de ferramentas e sistemas automatizados que dependem de nossos ser construído.