OpenAI

2 de novembro de 2026

Engenharia

Escalar o Git na OpenAI

Criar um sistema de controlo de versões à escala dos agentes, investindo no ecossistema Git.

Por Aaron Friel, Taylor Blau e Ted Nyman, membros da equipa técnica

A carregar…

Ao longo do último ano, a dimensão do nosso monorepositório e as exigências sobre ele aumentaram a um ritmo impressionante. Era claro que precisávamos de reformular o nosso sistema de controlo de versões para suportar o nível de trabalho simultâneo que o Codex torna possível.

Criámos o Pando, um novo backend para repositórios Git, sem interromper o trabalho de engenharia nem alterar a forma como a nossa equipa utiliza o Git. O Pando suporta agora todo o desenvolvimento no monorepositório da OpenAI. O Pando foi concebido para cumprir dois objetivos ambiciosos: primeiro, escritas com escalabilidade horizontal, otimizadas para pushes simultâneos para muitos ramos. Segundo, maximizar a taxa de processamento das leituras, para que os clientes consigam descarregar dados suficientemente depressa para utilizar toda a largura de banda de rede disponível.

Atingimos estes objetivos mantendo a compatibilidade com o protocolo e o cliente Git, o que permitiu aos nossos engenheiros e agentes integrar o Pando sem dificuldades nos seus fluxos de trabalho existentes. Em produção, o Pando reduziu a latência mediana dos pushes para quase um quinto. Atualmente, fornece quase um petabyte de dados Git por dia.

Neste artigo, explicamos o que nos levou a criar um novo sistema de controlo de versões e por que motivo optámos por manter a compatibilidade com o Git. Vamos comparar o nosso sistema com abordagens comuns para escalar o Git e explicar por que razão o Pando está numa posição única para satisfazer as necessidades de escalabilidade da OpenAI. Abordamos também os próximos passos e a forma como continuaremos a contribuir para o Git para suportar trabalho à escala dos agentes.

Porque precisávamos de um novo sistema de controlo de versões

O Codex mudou a escala e o ritmo de desenvolvimento na OpenAI, permitindo aos engenheiros fazer muito mais trabalho em paralelo. Isto significa mais ramos e pushes, mais merges e mais fetches e clones a partir de ambientes de desenvolvimento, tarefas de investigação e integração contínua (CI).

Estas exigências acumulam-se. Um engenheiro tem agora várias alterações prontas para CI ao mesmo tempo, e cada push pode desencadear várias tarefas que fazem fetch do mesmo commit.

Por volta do final do ano passado, os sistemas que suportavam o nosso monorepositório começaram a ter dificuldade em acompanhar este ritmo. Tivemos fetches e pushes lentos, bem como falhas nos merges, com a fiabilidade a cair por vezes abaixo dos 90%. Tínhamos atingido um limite do próprio Git ou da forma como disponibilizávamos o nosso monorepositório? O Git nasceu de uma comunidade que trocava patches para o kernel por e-mail. Há mais de duas décadas que os programadores adaptam as suas ferramentas em torno do Git, à medida que evolui a forma como desenvolvem software. Quando começámos a sentir dificuldades no nosso próprio monorepositório, era importante para nós poder continuar a aproveitar esse trabalho.

Vimos uma forma de manter esse fluxo de trabalho familiar, reconstruindo o backend para processar pushes para diferentes ramos em paralelo e fornecer o mesmo histórico a muitos clientes de leitura em simultâneo. Usar o Git como base do nosso fluxo de trabalho de controlo de versões significava que as melhorias no cliente poderiam ser úteis fora da OpenAI. Queríamos contribuir com essas melhorias da nossa cópia do Git⁠(abre numa nova janela) para o projeto principal, partilhando-as com toda a comunidade.

Engenharia para o Git em grande escala

Escalar um sistema de controlo de versões tem duas dimensões essenciais: muitos clientes de escrita a fazer push para diferentes ramos e muitos clientes de leitura a fazer fetch do mesmo histórico. O Pando otimiza ambas.

Primeiro, o Pando coordena as atualizações de cada ramo de forma independente. As atualizações de ramos sem relação entre si podem avançar em paralelo, e um cliente que faça fetch de um ramo recebe o histórico necessário para essa revisão. Em contrapartida, um cliente que consulte vários ramos pode vê-los em estados correspondentes a instantes diferentes.

Segundo, o Pando reutiliza packfiles (o formato do Git para comprimir vários objetos em conjunto) entre fetches, em vez de gerar um packfile individual para cada cliente. Um cliente pode descarregar alguns objetos que já tem, mas o servidor consegue fornecer os mesmos bytes a muitos clientes com pouco processamento.

Estas escolhas permitem-nos escalar as escritas entre ramos e servir leituras a partir de caches. Para perceber como se articulam, comecemos pela forma como o Git armazena e transfere o histórico.

Introdução ao armazenamento de objetos no Git

O Git tem de conseguir manter duas partes de um repositório sincronizadas: objetos e referências.

Um repositório Git regista imagens do estado dos ficheiros e o histórico que as liga sob a forma de um grafo de objetos. Um objeto pode ser um blob, uma árvore, um commit ou uma tag. Os blobs contêm o conteúdo de um ficheiro individual, e as árvores associam nomes a blobs ou a outras árvores para formar uma estrutura de diretórios. Os commits apontam para uma árvore raiz e para zero ou mais commits pais, formando o histórico do repositório.

Os objetos são identificados por um hash do seu tipo e conteúdo. Os objetos também são imutáveis: registar um ficheiro alterado cria um novo blob, enquanto os ficheiros e diretórios inalterados podem reutilizar objetos existentes.

Os ramos e as tags são referências, ou «refs», que apontam para commits. Fazer commit num ramo cria um novo commit e atualiza a ref do ramo para apontar para ele.

O ramo refs/heads/main aponta para o commit C2, cujo pai é C1. C1 aponta para a árvore raiz T1, onde parser.py referencia o blob B1. C2 aponta para a árvore raiz T2, onde parser.py referencia o blob B2. Em ambas as árvores raiz, docs/ referencia a mesma árvore, T3. A entrada README.md dessa árvore aponta para o blob B3, e usage.md aponta para o blob B4.

Os objetos Git referenciam-se entre si pelos respetivos IDs: os commits referenciam árvores, e as árvores referenciam outras árvores e blobs. O ramo refs/heads/main aponta para o commit C2, cujo pai é C1. C1 aponta para a árvore raiz T1, onde parser.py referencia o blob B1. C2 aponta para a árvore raiz T2, onde parser.py referencia o blob B2. Em ambas as árvores raiz, docs/ referencia a mesma árvore, T3. A entrada README.md dessa árvore aponta para o blob B3, e usage.md aponta para o blob B4.

O Git armazena os objetos em estado «solto» ou «empacotado». Os objetos soltos são armazenados em ficheiros individuais identificados pelo respetivo ID de objeto. Os objetos empacotados são armazenados em conjunto num packfile, permitindo guardar objetos semelhantes como deltas uns em relação aos outros.

Duas versões de parser.py aparecem em $GIT_DIR/objects. Como objetos soltos, o blob A contém parse() e o blob B contém parse() e parse_file(). O packfile guarda A na íntegra e representa B como instruções para copiar todo o conteúdo de A e acrescentar parse_file(). O ficheiro *.idx associado localiza ambos os blobs no packfile pelo ID de objeto.

O Git pode guardar blobs em ficheiros separados ou empacotá-los em conjunto, codificando um blob como um delta em relação a outro.

No Git, um objeto é alcançável a partir de um commit se o Git o conseguir encontrar seguindo as ligações desse commit através dos seus pais, árvores e blobs. Durante um fetch, o cliente indica ao servidor os commits que pretende obter e os que já possui. O servidor encontra os objetos alcançáveis a partir dos commits pretendidos, exclui aqueles que sabe que o cliente já possui e envia os restantes num packfile com o mesmo formato que o Git utiliza em disco. Um clone completo começa sem quaisquer objetos, pelo que necessita de todos os objetos alcançáveis a partir do histórico que está a ser clonado.

Um push transfere objetos no sentido inverso, pedindo ao servidor que atualize uma ou mais refs. O cliente usa o conjunto de refs anunciado pelo servidor para evitar enviar objetos que o servidor já possui. O pack enviado pode, por isso, depender de objetos existentes: receber apenas o pack não basta para reconstruir o novo histórico.

Replicar um repositório Git significa manter tanto os objetos como as referências disponíveis em vários servidores. Antes de uma réplica poder servir o novo valor de uma referência, tem de conseguir fornecer os objetos alcançáveis a partir desse valor. O serviço também precisa de coordenar atualizações de referências concorrentes, ou seja, pushes que tentam mover o mesmo ramo da mesma ponta antiga para novos commits diferentes. Os sistemas que impõem uma ordem única a todas as escritas num repositório podem obrigar pushes para ramos sem relação entre si a esperar uns pelos outros.

Padrões existentes de serviços Git distribuídos

Ao conceber o Pando, avaliámos abordagens convencionais para escalar o controlo de versões com Git: uma que coordena as atualizações de refs entre réplicas do repositório através de commit em três fases (3PC, um protocolo para coordenar um commit entre vários nós) e outra que ordena as atualizações através de um registo antecipado de escrita partilhado (WAL, um registo das alterações escrito antes de estas serem aplicadas) num armazenamento de blobs.

Commit em três fases (3PC)

Na arquitetura de commit em três fases que modelámos, o serviço Git mantém uma cópia completa do repositório em várias réplicas. Quando qualquer servidor recebe um push, o packfile recebido é copiado e enviado para todas as réplicas em paralelo. Para publicar o push, a maioria das réplicas tem de aceitar a atualização da ref. O servidor que gere este processo é o coordenador, e o processo exige várias rondas de comunicação entre o coordenador e cada uma das outras réplicas.

Para aceitar uma atualização de referência, cada réplica bloqueia a sua cópia da ref e verifica se esta ainda aponta para o commit esperado pelo cliente. O coordenador envia então uma mensagem de pré-commit às réplicas que aceitam. Assim que a maioria confirma a mensagem de pré-commit, o coordenador instrui essas réplicas a efetuar a atualização. Qualquer réplica atualizada pode então anunciar a nova ponta do ramo a partir da sua cópia local do repositório.

Manter réplicas completas permite servir leituras localmente e garante aos clientes uma visão consistente de todas as referências, independentemente da réplica que escolham. Como consequência, as referências são atualizadas uma de cada vez, e cada push exige várias comunicações de ida e volta para coordenar a atualização da ref entre as réplicas.

A espera por um quórum faz com que o desempenho dos discos das réplicas e a latência de comunicação condicionem o push. À medida que o volume de escrita aumenta, as réplicas têm de acompanhar tanto os packs recebidos como o tráfego de leitura.

Registo antecipado de escrita (WAL)

Na arquitetura baseada em WAL que modelámos, o serviço Git usa um manifesto partilhado num armazenamento de blobs para ordenar as atualizações em todo o repositório. Quando um servidor recebe um push, carrega o packfile recebido para um armazenamento de blobs e prepara a atualização da ref na sua cópia local do repositório. Para publicar o push, acrescenta uma entrada a um manifesto partilhado no armazenamento de blobs, que regista a atualização da ref e aponta para o packfile. O servidor Git atualiza então o manifesto através de uma operação compare-and-swap. Esta operação substitui o manifesto atomicamente ou rejeita a alteração. Se outro cliente de escrita o tiver atualizado entretanto, o servidor lê as entradas entretanto acrescentadas, verifica se a sua atualização da ref continua a ser válida e tenta novamente com o packfile que já carregou.

Os clientes de escrita podem carregar os seus packfiles em paralelo, mas mesmo os pushes para ramos sem relação entre si exigem a atualização do mesmo manifesto. O tempo de ida e volta de uma operação compare-and-swap bem-sucedida limita a frequência com que o repositório pode publicar um novo manifesto. Um armazenamento de blobs de baixa latência torna esta etapa rápida. O processamento em lote permite que um manifesto atualize várias refs ao mesmo tempo, à custa de colocar as atualizações em fila de espera e aumentar a latência.

O armazenamento de blobs pode receber packfiles em paralelo, mas as atualizações do manifesto partilhado continuam a competir entre si. Aumentar apenas a capacidade do armazenamento de blobs não resolve esse estrangulamento. Cada réplica continua a ter de ler e indexar os packs do armazenamento de blobs antes de os poder servir localmente. Se as escritas chegarem a um ritmo superior ao desse trabalho, as réplicas ficam para trás e os clientes esperam mais tempo para obter commits acabados de enviar.

Conceber a arquitetura do Pando

Isto leva-nos ao Pando. O Pando é uma nova arquitetura para disponibilizar repositórios Git, concebida para escalar e suportar muitos clientes a fazer push em simultâneo. Funciona assim:

Repensar as restrições

Antes de desenvolvermos o Pando, suspeitávamos de que seria possível satisfazer os nossos requisitos sem abdicar do Git. No entanto, tivemos de fazer concessões em alguns aspetos que a maioria dos sistemas Git considera requisitos:

  • Consistência em todo o repositório: Fornecer uma imagem consistente de todas as refs num dado instante exige coordenação em todo o repositório. Num sistema que ordena conjuntamente todas as atualizações de refs, os pushes para ramos sem relação entre si disputam um único ponto de coordenação.

  • Packfiles exclusivos para cada cliente: Se dois clientes com históricos locais diferentes fizerem fetch do mesmo ramo, o servidor poderá ter de gerar um pack diferente para cada um, para evitar enviar objetos que já possuem. Criar estas respostas consome CPU e operações de E/S de disco, o que dificulta o aumento da capacidade para servir muitos clientes de leitura.

Em alternativa, o Pando fornece um modelo de consistência menos restritivo (consistência causal) para escalar as escritas e reutiliza packfiles para escalar as leituras.

Consistência causal (CC)

Quando um cliente faz push para um ramo, o Pando guarda o packfile recebido como um blob imutável num armazenamento de blobs. Sempre que o ramo avança, o Pando regista num armazenamento de metadados uma versão de ref que contém a nova ponta do ramo, o pack que a forneceu e quaisquer versões de ref anteriores das quais o pack dependa. Tanto o armazenamento de blobs como o de metadados permitem escalabilidade horizontal.

Obter um commit ou uma referência do Pando exige reproduzir um conjunto de versões de ref (packfiles) no cliente, reconstruindo o estado do cliente que criou a versão de ref que está a ser obtida. Isto oferece uma forma de consistência mais fraca no conjunto do repositório, mas garante a serialização das atualizações de cada ref. Os clientes só entram em conflito quando fazem push para a mesma ref em simultâneo. Um cliente que leia vários ramos não pode assumir que essas leituras descrevem um único instante no conjunto do repositório.

À esquerda, três packs sucessivos de trunk contêm os commits A, B e C; D e E; e F. À direita, trunk e topic divergem em C. trunk avança para G, enquanto topic avança por D, E e F. O pack mais recente de topic contém D, E e F. Um contorno tracejado assinala que D e E também estão presentes no pack anterior de topic. A seta azul assinala a dependência do novo pack em relação a trunk em G. O commit pai de F no Git é E.

Um push para trunk acrescenta um pack incremental que contém um ou mais commits. Cada push para um ramo secundário volta a incluir os commits anteriores desse ramo, para que um cliente com a versão anunciada de trunk possa obter o ramo num único pack.

Esta é uma forma de consistência causal. Para obter uma referência, o cliente de leitura observa o histórico do cliente de escrita que a produziu. Durante um push, o Pando controla o histórico comum acordado entre cliente e servidor, garantindo que os clientes produzem packfiles mais fáceis de partilhar.

Consequentemente, o Pando não fornece o estado de todas as referências num dado instante durante operações Git, nem uma ordenação das atualizações de refs durante um clone ou fetch. Concretamente, suponhamos que os ramos foo e bar são ambos atualizados durante a negociação de um clone completo. Durante a negociação, um cliente pode observar foo na versão 1 e bar na versão 2, enquanto outro observa foo na versão 2 e bar na versão 1. Na prática, os utilizadores e os agentes não dependem da ordenação global das atualizações de refs, mas sim da ordem das atualizações de uma única referência.

Comparação de abordagens

Nesta simulação dos três modelos de consistência, configuramos 1000 clientes para que cada um selecione um ramo distinto e faça repetidamente push de commits únicos, medindo o tempo total decorrido e a latência entre push e fetch. Esta última é uma métrica importante para as cargas de trabalho, pois mede o tempo de ida e volta necessário para utilizadores e agentes colaborarem ou utilizarem outros sistemas que dependem do controlo de versões. A simulação modela hardware que vai de máquinas virtuais e sistemas de armazenamento de blobs correntes a opções de gama alta. Nesta simulação, flexibilizámos a consistência em todo o repositório para o protocolo de commit em três fases.

Simulating Git Server Implementations

Follow each writer through repeated pushes, and separately measure when an independent reader can check out each commit.

Three-phase commit

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Object-store WAL

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Pando

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Writer progress through verified fetches

  • Three-phase commit
  • Object-store WAL
  • Pando
05001,00000 s
  • Waiting
  • Pushing
  • All pushes acknowledged
0 sLater

Synthetic teaching models, not production benchmarks. Mean push to fetch covers topic pushes, from the first attempt through retries until an independent reader verifies that commit. It includes waiting for a reader slot and excludes builds and tests. With merges enabled, writer completion includes the required fetches and trunk merge.

Experiment settings

Preparing simulation…

Results

Play the comparison to see the recorded results.

Synthetic teaching models, not production benchmarks. Mean push to fetch covers topic pushes, from the first attempt through retries until an independent reader verifies that commit. It includes waiting for a reader slot and excludes builds and tests. With merges enabled, writer completion includes the required fetches and trunk merge.

Execute esta simulação que compara o Pando com duas outras abordagens. Escolha a infraestrutura, desde hardware corrente a opções de gama alta, o número de clientes e outras definições para comparar os resultados.

O sistema de commit em três fases modelado apresenta uma latência de escrita significativamente maior sob carga, à medida que os discos ficam saturados. Já os sistemas de registo antecipado de escrita apresentam uma latência de leitura significativamente maior pela mesma razão, pois os servidores têm dificuldade em acompanhar a leitura do registo. Tanto nos testes reais no nosso monorepositório como nesta simulação, o Pando suporta muitos milhares de clientes a fazer push em simultâneo sem afetar o desempenho sob carga.

Ao processar um push, o Pando valida o packfile recebido enquanto o transmite para o armazenamento de blobs, sem o desempacotar num repositório local. Quando um cliente inicia um fetch, o Pando seleciona os packs armazenados de que o cliente necessita, e este descarrega-os. Nenhum servidor precisa de aplicar packfiles a um repositório local ou de realizar operações dispendiosas de desempacotamento e empacotamento. Como os servidores Pando se ocupam sobretudo da negociação e da seleção de packfiles, podem funcionar em hardware corrente.

Isto permite ao Pando escalar até aos limites dos clientes, da rede entre os clientes e o Pando e do serviço de armazenamento de blobs. Com capacidade suficiente, cada cliente que trabalhe num ramo independente pode transferir packs à velocidade máxima da sua própria ligação. Para servir mais clientes a esse ritmo, podemos acrescentar servidores Pando e aumentar, em paralelo, a capacidade dos armazenamentos de metadados e de blobs.

Pushing and Fetching a Branch in Pando

Pando stores the writer’s packfile in blob storage and records the ref version in metadata service. On fetch, Pando selects the packs the reader needs, and Express serves them.

Stream the push

  • Packfile bytes
  • Metadata
  • Request / acknowledgment
  • Packfile URIs

Um agente edita ficheiros, cria um commit localmente e executa git push, que cria um packfile dessas alterações para atualizar um ramo. O Pando recebe o pack e transmite-o para o armazenamento de blobs. Depois de validar o push recebido com uma descodificação numa única passagem, o Pando utiliza uma transação com várias chaves para registar uma versão de ref no nosso armazenamento de metadados e atualizar atomicamente o ponteiro da ref. Isto permite ao Pando garantir a consistência de «leitura das próprias escritas». Toda esta operação de escrita exige O(1)O(1) leituras e escritas de metadados.

Quando outro cliente faz fetch do ramo, o Pando seleciona um conjunto de packs armazenados com base no histórico que o cliente já possui. Esta operação exige O(log⁡Δ)O(\log \Delta) leituras de metadados, sendo Δ\Delta a menor distância, em versões de ref, entre o que o cliente tem e o que pretende obter. O servidor devolve os URIs dos packfiles, e o cliente descarrega-os através de um serviço, o Express, que armazena packfiles em caches regionais.

Esta conceção assenta nos três serviços centrais da arquitetura do Pando: pushes, fetches e compactação.

Escalar os pushes

Quando um cliente faz push, um servidor Git convencional comporta-se como um jogador de póquer que mostra todas as cartas, revelando ao cliente todos os ramos que possui. O cliente reúne então os commits, as árvores e os blobs para criar um packfile que exclui os objetos que sabe que o servidor já possui.

Anunciar mais refs pode reduzir o tamanho do push. Também pode dificultar a reutilização do pack, porque outro cliente pode não ter os objetos que o cliente de escrita deixou de fora.

O Pando segue uma abordagem diferente. Em vez de mostrar todas as cartas, anuncia ao cliente apenas um conjunto selecionado de ramos. Fá-lo por dois motivos:

  1. Fornecer uma visão consistente de todas as refs é dispendioso. Com milhões de ramos, só o anúncio das refs pode ultrapassar os 100 MiB. Para o fornecer, cada servidor tem de manter um mapeamento atualizado, com um custo significativo de coordenação, ou executar uma consulta à base de dados cujo custo cresce com o número de refs.

  2. Ao receber apenas um subconjunto das refs, o cliente gera um pack que depende apenas daquilo que o servidor lhe permite explicitamente excluir. O acompanhamento das dependências permite ao Pando reutilizar o packfile que um cliente gera num push, fornecendo-o como resposta a um fetch posterior.

À medida que um servidor Git anuncia mais refs, os clientes que fazem push têm mais fontes a partir das quais podem excluir objetos, o que lhes permite produzir packs mais pequenos. No entanto, para reutilizar estes packfiles, o servidor tem de conseguir comprovar que os clientes que fazem fetch possuem todas as dependências. Caso contrário, o packfile pode omitir objetos de que o cliente necessita.

Por outro lado, se um servidor Git não anunciar nenhuma ref, o cliente gerará um packfile com todo o histórico do repositório. Esse packfile pode ser utilizado por qualquer outro cliente, mas com um custo significativo: fazer fetch de qualquer ramo equivale, na prática, a um clone completo.

Curva que mostra como as referências anunciadas afetam o tamanho do packfile: desde o histórico completo, quando nenhuma é anunciada, passando pelo anúncio de trunk, até apenas aos novos objetos, quando todas as referências são anunciadas.

Anunciar mais refs resulta em packfiles mais pequenos. A maior parte da redução de tamanho pode resultar do anúncio de apenas uma.

Por predefinição, quando um cliente faz push de um novo ramo de funcionalidade, o Pando anuncia como base apenas um ramo designado como trunk. Isto mantém o anúncio com o mesmo tamanho à medida que o repositório ganha ramos, mas tem contrapartidas. Quando um engenheiro faz push muitas vezes para um único ramo, ou cria um ramo de funcionalidade sobre outro, o cliente tem de reenviar tudo o que não está em trunk.

Isto faz com que os pushes sucessivos para o ramo sejam maiores, contenham objetos potencialmente redundantes e consumam mais largura de banda do cliente. No nosso monorepositório, o tamanho mediano dos packfiles produzidos desta forma era inferior a 20 KiB, e o percentil 99 era inferior a 3 MiB. Para nós, esta foi uma troca razoável, porque um cliente que já tenha o histórico anunciado de trunk pode obter o ramo com esse único packfile.

Depois de escolher o conjunto de refs a anunciar ao cliente, o Pando aceita o push e atualiza o seu armazenamento de metadados. O Pando divide a atualização dos metadados em partes síncronas e assíncronas. A atualização síncrona dos metadados garante que os clientes podem fazer fetch imediato do commit acabado de atualizar. Esta operação escreve uma nova versão de ref e atualiza o mapeamento do ponteiro da ref para essa versão, guardando a nova ponta do ramo, as dependências usadas para negociar o packfile e uma ligação para o packfile enviado no push.

Ao mesmo tempo, a mesma transação regista trabalho assíncrono, como o agendamento da compactação. Em pushes muito grandes, apenas um subconjunto dos commits é escrito de forma síncrona. Isto permite-nos exigir apenas um número constante de leituras e escritas no armazenamento de metadados, independentemente do tamanho do push.

É importante notar que os servidores Pando podem aceitar de forma independente pushes para refs sem relação entre si. A única chave disputada é o ponteiro da ref, que protegemos com uma operação atómica semelhante a compare-and-swap. Podemos aumentar a taxa de processamento das escritas acrescentando servidores Pando, unidades de escalabilidade do armazenamento de metadados ou armazenamentos de blobs.

Desempenho de fetch e clone à velocidade máxima da ligação

Quando um cliente faz fetch, fornece ao servidor dois conjuntos de IDs de commits, conhecidos no protocolo como have e want. Num servidor Git convencional, o servidor calcula a diferença entre o conjunto de objetos alcançáveis a partir dos wants e o dos alcançáveis a partir dos haves, comprime os objetos resultantes e envia-os ao cliente. Embora o servidor Git de código aberto disponha de vários índices locais para acelerar esta operação, gerar estes packfiles pode ser dispendioso e, em geral, é difícil tirar partido da cache para os reutilizar.

Em alternativa, o Pando utiliza os mapeamentos de commits registados nos pushes para consultar os have e want do cliente, convertendo-os num conjunto de versões de ref que contêm esses commits. Cada uma dessas versões de ref contém uma ligação para um packfile anteriormente recebido pelo Pando. O servidor usa esta informação para determinar o que falta ao cliente, selecionando os packfiles que abrangem o conjunto de objetos de que o cliente necessita.

Planning fetches in Pando

Pando compares what a client has with what it wants, then selects the stored packfiles needed—including any missing branch dependencies.

Already heldFetchMissing branch dependency

Fetch 27 trunk packs.

Select a branch to include its trunk dependencies.

O Pando mapeia os haves e wants do protocolo Git para uma sequência de versões de ref. Pedir um ramo secundário implica pedir também o histórico até às suas dependências em trunk.

O protocolo Git só permite enviar um único packfile na resposta HTTP de um clone ou fetch. No entanto, uma funcionalidade pouco valorizada do protocolo Git, packfile-uri, permite ao servidor devolver ligações para descarregar packs adicionais em separado. O Pando usa exclusivamente este mecanismo e transmite um packfile vazio no corpo da resposta. Para suportar esta funcionalidade, a OpenAI contribuiu com várias melhorias em packfile-uri para o projeto principal.

Os sistemas da OpenAI operam em muitas regiões geográficas, pelo que mantemos clusters Pando com caches locais em cada uma delas. Um serviço de distribuição de packs, o Express, mantém uma cache do nosso armazenamento de blobs, e um serviço separado mantém uma cache do armazenamento de metadados. Estas caches permitem ao Pando responder a pedidos de um ID de commit ou de uma leitura especial de uma ref inteiramente a partir do estado em cache nesse cluster. Usamos hashing consistente para encaminhar os pedidos de packfiles para os servidores Express, com taxas de acerto na cache superiores a 99%.

Este sistema de caches proporciona uma taxa de processamento extremamente elevada, capaz de esgotar a capacidade da rede entre os clientes e o Pando. Com estes URIs de packfiles e caches locais, confirmámos que as operações de clone e fetch saturam ligações de rede de 10 Gbps, utilizando apenas máquinas virtuais padrão de fornecedores de serviços na nuvem.

Compactação e tempo até ao primeiro byte

Na maioria dos servidores Git, o reempacotamento e a recolha de lixo são necessários para manter o desempenho de um repositório em crescimento. Ambas as etapas recuperam espaço, mas exigem muitos recursos de processamento e E/S.

Num repositório que evolui rapidamente, como o monorepositório da OpenAI, é inviável enviar um URI de packfile individual para cada push realizado desde o último fetch. Todos os dias, um cliente teria de descarregar dezenas de milhares de packfiles para atualizar o seu ramo trunk.

Em alternativa, o Pando combina packs em segundo plano para reduzir esta sobrecarga, adotando uma conceção semelhante à compactação em árvores LSM (log-structured merge-trees). Cada push produz aquilo a que o Pando chama um packfile «L0», que representa a menor unidade de dados que o Pando pode transmitir. Ao longo do tempo, processos em segundo plano reúnem estes packfiles em conjuntos de cobertura que abrangem uma sequência de versões de ref, crescendo geometricamente a partir de L0. Atualmente, o nosso monorepositório usa conjuntos de cobertura de packfiles até L4.

Estes conjuntos de cobertura têm uma dupla função. Quando um conjunto de cobertura está suficientemente cheio, um processo de compactação assume o seu processamento e produz um único packfile. Isto melhora a taxa de compressão, reorganizando os objetos dentro do packfile. Mas também melhora o desempenho da negociação de fetches, ao funcionar como uma lista de saltos sobre as versões de ref. Ao percorrer simultaneamente os índices de cada nível, o Pando consegue selecionar um conjunto de packs para enviar a um cliente em tempo logarítmico. Estes conjuntos de cobertura de packfiles, reempacotados ou não, permitem planear os fetches de forma mais eficiente, reduzindo os requisitos de taxa de processamento e de latência do Pando.

Compaction in Pando

Small packfiles are grouped into larger covers across levels, improving compression and speeding up fetches.

Compaction in progressPublished cover

A compactação combina, em segundo plano, packfiles de muitas versões de ref, permitindo fetches mais eficientes com menos ficheiros.

Ao contrário do que acontece em muitos servidores Git, os packfiles são mantidos em todos os níveis, sem serem descartados ou eliminados pela recolha de lixo. Os packs de todos os níveis são usados continuamente para servir os clientes, com base nos have e want que indicam num fetch. Para os clientes que usam o Pando como API, ler um objeto arbitrário de um packfile tende a exigir a leitura de menos dados de um L0 do que de um L4, porque é menos provável que esteja comprimido.

A compactação no Pando reduz significativamente o volume total de dados transferidos para os clientes e permite que o custo do planeamento dos packs cresça de forma sublinear em relação ao atraso do cliente. Isto é especialmente importante para repositórios grandes e em rápida evolução, que esperamos que se tornem a norma no desenvolvimento de software.

Criar o Pando

Criámos o Pando com o Codex, passando do arranque do projeto à produção em apenas 10 semanas. O núcleo do Pando foi concebido por um único engenheiro, implementado por equipas de agentes e colocado em produção por um pequeno grupo de engenheiros. O Pando começou a servir tráfego de produção em abril deste ano e faz parte de um sistema mais amplo de ferramentas de gestão do controlo de versões utilizado na OpenAI.

Usámos uma grande variedade de abordagens para validar a conceção do Pando. Aplicámos os nossos princípios para criar bases de software mais seguras⁠. Por se tratar de um projeto criado de raiz, optámos por implementar o Pando em Rust e sem dependências herdadas de sistemas anteriores. Escrevemos uma bateria de testes de conformidade para que os agentes pudessem testar as suas alterações face ao comportamento padrão do Git. Também desenvolvemos harnesses de testes de simulação determinística para verificar invariantes e propriedades do sistema, o que nos permitiu encontrar e mitigar condições de corrida subtis e regressões na negociação e no planeamento dos packs.

Muitos modelos contribuíram para este sistema; à medida que novos modelos ficaram disponíveis, usámo-los para melhorar o desempenho e a fiabilidade do Pando. Com o GPT‑6 Astra, os agentes escreveram código em Lean e TLA+ para formalizar uma «teoria do Pando», que usámos para encontrar e corrigir erros na implementação e demonstrar propriedades do sistema. Várias sessões contínuas decorreram durante centenas de horas, identificando e corrigindo casos-limite no Pando. Também mantemos agentes em funcionamento 24 horas por dia para realizar testes adversariais ao Git e ao Pando, utilizando modelos com capacidades Daybreak.

Colocar o Pando em produção

Inicialmente, colocámos o Pando em produção como um proxy que encaminhava as leituras e escritas para o nosso sistema de controlo de versões existente. Isto permitiu-nos manter o Pando e o sistema anterior sincronizados, para transferir o tráfego para o Pando em segurança e sem interrupções de serviço.

Como parte da sua arquitetura de cache, o Pando também pode eliminar pedidos duplicados e combinar pedidos antes de os encaminhar. Isto permitiu-nos manter as ferramentas existentes a funcionar bem enquanto migrávamos os sistemas para o Pando, um de cada vez. À medida que migrávamos mais cargas de trabalho para o Pando, os respetivos pushes preenchiam a cache do Pando e melhoravam a fiabilidade do backend anterior.

Para otimizar o desempenho ao utilizar o Pando, começámos a instalar a nossa distribuição pública do Git nas máquinas dos programadores e a incluí-la na aplicação ChatGPT para computador. Para migrar os clientes para o Pando por fases, usámos uma camada interna em torno da interface de linha de comandos do Git. Criámos muitas outras ferramentas para gerir pedidos de alteração e revisões de código, bem como APIs para automações. Juntamente com o tráfego Git, migrámos centenas de integrações que dependiam de APIs antigas, webhooks e transferências de repositórios.

O próprio Codex ajudou os programadores e as equipas a fazer a transição. Criámos competências para preparar as cópias de trabalho existentes e orientar os agentes na utilização do novo sistema. Assim, os programadores podiam simplesmente pedir ao Codex que migrasse os seus ambientes de desenvolvimento e serviços.

A mudança para o Pando melhorou a fiabilidade dos merges e reduziu a latência mediana dos pushes em quase 80%.

O Pando consegue fornecer um petabyte de dados Git por dia com apenas alguns núcleos de CPU. O Pando já forneceu mais de 57 terabytes de packs Git numa única hora, com mais de 99% desses bytes provenientes da cache local em disco.

A reutilização de packs permite a muitos clientes descarregar o mesmo histórico do repositório com pouco processamento nos nós que os servem. Este exemplo mostra um único repositório, mas o Pando foi concebido para suportar muitos mais. À medida que a procura cresce, podemos escalar a distribuição de dados e a negociação Git de forma independente.

Contribuir para o projeto Git

Criar o Pando com base no protocolo e no cliente Git ajudou-nos a identificar erros e oportunidades para melhorar o desempenho do próprio Git.

Por exemplo, o Pando fornece packs reutilizáveis diretamente aos clientes, pelo que descarregar e indexar esses packs se tornou essencial para tornar o sistema rápido e fiável. Esse trabalho revelou casos-limite nas transferências simultâneas, na limpeza após transferências interrompidas e na retoma de transferências que já tinham sido concluídas. Corrigimos esses percursos de execução, acrescentámos validações ao aceitar um pack existente para um fetch e contribuímos com esse trabalho para o projeto principal.

Também precisávamos de perceber onde eram gastos o tempo e a largura de banda, pelo que acrescentámos rastreio das transferências de packfile-uri e contagens de bytes na geração de packs. Isto facilitou perceber se um fetch lento demorava mais a gerar os packs ou a transferi-los.

Na versão do Git da OpenAI⁠(abre numa nova janela), o cliente pode descarregar e indexar packs independentes em paralelo. Acrescentámos o conceito de «haves explícitos» para que um servidor possa anunciar a ponta atual de um ramo separadamente do histórico que um cliente a fazer push pode deixar de fora do seu pack. No Pando, isto permite aos clientes usar --force-with-lease para verificar que um ramo de funcionalidade não mudou e criar o respetivo pack tendo apenas trunk como base. Qualquer cliente com esse histórico de trunk pode depois obter o ramo usando o mesmo pack.

Várias destas melhorias já estão no projeto principal do Git, e muitas outras estão no nosso repositório público. Continuamos a trabalhar com a comunidade Git para tornar estas melhorias úteis para além do Pando. Pode acompanhar esse trabalho no nosso sistema público de acompanhamento de patches⁠(abre numa nova janela).

Próximos passos

Ao repensar as ideias estabelecidas sobre como escalar repositórios Git, o Pando permite-nos aumentar a escala do nosso monorepositório sem alterar os fluxos de trabalho Git que os nossos engenheiros e agentes já utilizam. No nosso monorepositório, o Pando permitiu um maior grau de paralelismo, melhorou a fiabilidade dos merges e reduziu o tempo necessário para publicar alterações ao código. Além do monorepositório, estamos a explorar outras utilizações do Pando na OpenAI, incluindo cargas de trabalho em que um grande número de agentes precisa de colaborar num repositório Git partilhado.

À medida que engenheiros e agentes produzem mais software, mais equipas fora da OpenAI irão deparar-se com os limites que encontrámos no controlo de versões. Estamos atualmente a explorar formas de partilhar com os nossos utilizadores a tecnologia que torna o Pando possível.

O Pando assenta em duas décadas de experiência de utilizadores e operadores de repositórios Git. Continuaremos a desenvolver o Pando e a trabalhar com a comunidade de código aberto do Git para partilhar melhorias que beneficiem os utilizadores do Git em geral.

Autor

OpenAI