OpenAI

2 de novembro de 2026

Engenharia

Escalando o Git na OpenAI

Criando um controle de versão para a escala do trabalho com agentes de IA e investindo no ecossistema Git.

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

Carregando…

Ao longo do último ano, o tamanho do nosso monorepo e as demandas sobre ele aumentaram em um ritmo impressionante. Estava claro que precisávamos reformular nosso sistema de controle de versão para acompanhar o volume de trabalho simultâneo que o Codex torna possível.

Criamos o Pando, um novo backend para repositórios Git, sem interromper nenhum trabalho de engenharia nem mudar a forma como nossa equipe usa o Git. Hoje, o Pando sustenta todo o desenvolvimento no monorepo da OpenAI. O Pando foi projetado para atender a dois objetivos ambiciosos: primeiro, gravações escaláveis horizontalmente, otimizadas para pushes simultâneos para muitos branches. Segundo, maximizar a taxa de processamento de leitura, para que os clientes possam baixar dados com velocidade suficiente para utilizar toda a largura de banda de rede disponível.

Alcançamos esses objetivos mantendo a compatibilidade com o protocolo e o cliente Git, o que permitiu que nossos engenheiros e agentes adotassem o Pando sem dificuldades em seus fluxos de trabalho existentes. Em produção, o Pando reduziu a latência mediana de push para quase um quinto do valor anterior. Hoje, ele entrega quase um petabyte de dados Git por dia.

Aqui, vamos abordar nossas motivações para criar um novo sistema de controle de versão e explicar por que escolhemos manter a compatibilidade com o Git. Vamos comparar nosso sistema com abordagens comuns para escalar o Git e explicar por que o Pando tem uma capacidade única de atender às necessidades de escalabilidade da OpenAI. Também discutiremos os próximos passos e como continuaremos contribuindo com o Git para dar suporte ao trabalho na escala dos agentes.

Por que precisávamos de um novo sistema de controle de versão

O Codex mudou a escala e o ritmo de desenvolvimento na OpenAI, permitindo que os engenheiros façam muito mais trabalho em paralelo. Isso significa mais branches e pushes, mais merges e mais fetches e clones vindos de ambientes de desenvolvimento, tarefas de pesquisa e CI.

Essas demandas se multiplicam. Agora, um único engenheiro tem várias alterações prontas para CI ao mesmo tempo, e cada push pode fazer com que várias tarefas busquem o mesmo commit.

Por volta do fim do ano passado, os sistemas que sustentavam nosso monorepo começaram a ter dificuldade para acompanhar essa demanda. Enfrentamos fetches e pushes lentos, além de falhas em merges, com a confiabilidade às vezes ficando abaixo de 90%. Tínhamos atingido um limite do próprio Git ou da forma como disponibilizávamos nosso monorepo? O Git surgiu de uma comunidade que trocava patches do kernel por e-mail. Há mais de duas décadas, os desenvolvedores adaptam suas ferramentas em torno do Git conforme a forma de criar software evolui. Quando começamos a encontrar dificuldades em nosso próprio monorepo, era importante poder continuar aproveitando esse trabalho.

Vimos uma forma de manter esse fluxo de trabalho familiar enquanto reconstruíamos o backend para processar pushes para diferentes branches em paralelo e fornecer o mesmo histórico a muitos clientes de leitura simultaneamente. Usar o Git como base do nosso fluxo de controle de versão 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 em uma nova janela) para o projeto original, compartilhando-as com toda a comunidade.

Engenharia para o Git em escala

Escalar um sistema de controle de versão envolve duas dimensões essenciais: muitos clientes fazendo push para diferentes branches e muitos clientes buscando o mesmo histórico. O Pando é otimizado para ambas.

Primeiro, o Pando coordena as atualizações de cada branch de forma independente. Atualizações de branches sem relação entre si podem ocorrer em paralelo, e um cliente que busca um branch recebe o histórico necessário para aquela revisão. A contrapartida é que um cliente que consulta vários branches pode vê-los em momentos diferentes.

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

Essas escolhas nos permitem escalar as gravações entre branches e atender às leituras a partir de caches. Para entender como elas se combinam, vamos começar pela forma como o Git armazena e transfere o histórico.

Introdução ao armazenamento de objetos do Git

O Git precisa manter duas partes de um repositório em sincronia: objetos e referências.

Um repositório Git registra snapshots de arquivos e o histórico que os conecta como um grafo de objetos. Um objeto pode ser um blob, uma árvore, um commit ou uma tag. Blobs contêm o conteúdo de um arquivo individual, e árvores associam nomes a blobs ou a outras árvores para formar uma estrutura de diretórios. Commits apontam para uma árvore raiz e zero ou mais commits pais, formando o histórico do repositório.

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

Branches e tags são referências, ou "refs", que apontam para commits. Fazer um commit em um branch cria um novo commit e atualiza a ref do branch para apontar para ele.

O branch refs/heads/main aponta para o commit C2, cujo pai é C1. C1 aponta para a árvore raiz T1, em que parser.py se refere ao blob B1. C2 aponta para a árvore raiz T2, em que parser.py se refere ao blob B2. Nas duas árvores raiz, docs/ se refere à mesma árvore, T3. Sua entrada README.md aponta para o blob B3, e usage.md aponta para o blob B4.

Os objetos do Git fazem referência uns aos outros por seus IDs: commits apontam para árvores, e árvores apontam para outras árvores e blobs. O branch refs/heads/main aponta para o commit C2, cujo pai é C1. C1 aponta para a árvore raiz T1, em que parser.py se refere ao blob B1. C2 aponta para a árvore raiz T2, em que parser.py se refere ao blob B2. Nas duas árvores raiz, docs/ se refere à mesma árvore, T3. Sua entrada README.md aponta para o blob B3, e usage.md aponta para o blob B4.

O Git armazena objetos no estado "solto" ou "empacotado". Objetos soltos são armazenados em arquivos individuais identificados pelo ID do objeto. Objetos empacotados são armazenados juntos em um packfile, permitindo que objetos semelhantes sejam armazenados como deltas em relação uns 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 armazena A na íntegra e representa B como instruções para copiar todo o conteúdo de A e acrescentar parse_file(). O arquivo *.idx correspondente localiza os dois blobs no packfile pelo ID do objeto.

O Git pode armazenar blobs como arquivos separados ou empacotá-los juntos, 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 consegue encontrá-lo seguindo os links desse commit por seus pais, árvores e blobs. Durante um fetch, o cliente informa ao servidor quais commits deseja e quais já tem. O servidor encontra os objetos alcançáveis a partir dos commits desejados, exclui os que sabe que o cliente já tem e envia o restante como um packfile no mesmo formato que o Git usa em disco. Um clone completo começa sem nenhum objeto, por isso precisa de todos os objetos alcançáveis a partir do histórico que está sendo clonado.

Um push transfere objetos no sentido oposto, solicitando ao servidor que atualize uma ou mais refs. O cliente usa o conjunto de refs anunciadas pelo servidor para evitar enviar objetos que ele já tem. Portanto, o pacote enviado pode depender de objetos existentes: receber apenas o pacote não basta para reconstruir o novo histórico.

Replicar um repositório Git significa manter tanto os objetos quanto as referências disponíveis em vários servidores. Antes de uma réplica disponibilizar o novo valor de uma referência, ela precisa ser capaz de fornecer os objetos alcançáveis a partir desse valor. O serviço também precisa coordenar atualizações de referências concorrentes, ou seja, pushes que tentam mover o mesmo branch do mesmo commit de ponta antigo para novos commits diferentes. Sistemas que impõem uma ordem única a todas as gravações em um repositório podem fazer com que pushes para branches sem relação entre si precisem esperar uns pelos outros.

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

Ao projetar o Pando, avaliamos abordagens convencionais para escalar o controle de versão com Git: uma que coordena atualizações de refs entre réplicas do repositório usando commit em três fases (3PC, um protocolo para coordenar um commit entre vários nós) e outra que ordena as atualizações por meio de um log de gravação antecipada compartilhado (WAL, um registro de alterações gravado antes de serem aplicadas) no armazenamento de blobs.

Commit em três fases (3PC)

No projeto de commit em três fases que modelamos, 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 a todas as réplicas em paralelo. Para publicar o push, a maioria das réplicas precisa aceitar a atualização da ref. O servidor que gerencia esse processo é o coordenador, e o processo exige várias rodadas de comunicação entre ele e cada uma das outras réplicas.

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

Manter réplicas completas permite atender às leituras localmente e garante que os clientes tenham uma visão consistente de todas as referências, independentemente da réplica escolhida. Como consequência, as referências são atualizadas uma de cada vez, e cada push exige várias viagens 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 afetem diretamente o push. Conforme o volume de gravações cresce, as réplicas precisam acompanhar tanto os pacotes recebidos quanto o tráfego de leitura.

Log de gravação antecipada (WAL)

No projeto baseado em WAL que modelamos, o serviço Git usa um manifesto compartilhado no armazenamento de blobs para ordenar as atualizações em todo o repositório. Quando um servidor recebe um push, ele envia o packfile recebido a um armazenamento de blobs e prepara a atualização da ref em sua cópia local do repositório. Para publicar o push, ele adiciona ao manifesto compartilhado no armazenamento de blobs uma entrada que registra a atualização da ref e aponta para o packfile. O servidor Git então atualiza o manifesto usando uma operação compare-and-swap. Essa operação substitui o manifesto atomicamente ou rejeita a alteração. Se outro cliente de gravação o tiver atualizado nesse intervalo, o servidor lê as entradas adicionadas, verifica se sua atualização de ref ainda é válida e tenta novamente usando o packfile que já enviou.

Os clientes de gravação podem enviar seus packfiles em paralelo, mas até pushes para branches 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 essa etapa rápida. O processamento em lote permite que um manifesto atualize várias refs ao mesmo tempo, ao custo de enfileirar atualizações e aumentar a latência.

O armazenamento de blobs pode receber packfiles em paralelo, mas as atualizações do manifesto compartilhado continuam competindo entre si. Aumentar apenas a capacidade do armazenamento de blobs não resolve esse gargalo. Cada réplica ainda precisa ler e indexar os pacotes do armazenamento de blobs antes de poder fornecê-los localmente. Se as gravações recebidas superarem a velocidade desse trabalho, as réplicas ficam para trás, e os clientes esperam mais para buscar commits recém-enviados.

Projetando a arquitetura do Pando

É aí que entra o Pando. O Pando é uma nova arquitetura para disponibilizar repositórios Git, com o objetivo de escalar para muitos clientes fazendo push simultaneamente. Veja como funciona:

Repensando as restrições

Antes de desenvolver o Pando, suspeitávamos que seria possível atender aos nossos requisitos sem abrir mão do Git, mas precisávamos flexibilizar algumas premissas que a maioria dos sistemas Git trata como requisitos:

  • Consistência em todo o repositório: Fornecer uma visão consistente de todas as refs em um dado instante exige coordenação em todo o repositório. Em um sistema que ordena todas as atualizações de refs em conjunto, pushes para branches 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 buscarem o mesmo branch, o servidor talvez precise gerar um pacote diferente para cada um, evitando enviar objetos que eles já têm. Criar essas respostas consome CPU e E/S de disco, o que dificulta atender a um grande volume de clientes de leitura.

Em vez disso, o Pando oferece um modelo de consistência mais flexível (consistência causal) para escalar as gravações e reutiliza packfiles para escalar as leituras.

Consistência causal (CC)

Quando um cliente faz push para um branch, o Pando armazena o packfile recebido como um blob imutável em um armazenamento de blobs. A cada avanço do branch, o Pando registra no armazenamento de metadados uma versão de ref com o novo commit de ponta, o pacote que o forneceu e todas as versões anteriores de refs das quais o pacote depende. Tanto o armazenamento de blobs quanto o de metadados são escaláveis horizontalmente.

Buscar um commit ou uma referência no Pando exige reenviar ao cliente um conjunto de versões de refs (packfiles), reconstruindo o estado do cliente que criou a versão de ref buscada. Isso oferece uma forma mais fraca de consistência no repositório como um todo, mas garante serialização para atualizações de refs individuais. Os clientes só entram em conflito quando fazem push para a mesma ref simultaneamente. Um cliente que lê vários branches não pode presumir que essas leituras representam um único instante em todo o repositório.

À esquerda, três pacotes 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 até G, enquanto topic avança por D, E e F. O pacote mais recente de topic contém D, E e F. Um contorno tracejado indica que D e E também estão presentes no pacote anterior de topic. A seta azul indica a dependência do novo pacote em relação a trunk em G. O commit pai de F no Git é E.

Um push para o branch principal adiciona um pacote incremental com um ou mais commits. Cada push para um branch secundário inclui novamente os commits anteriores desse branch, para que um cliente com a versão anunciada do branch principal possa buscá-lo em um único pacote.

Essa é uma forma de consistência causal. Para buscar uma referência, o cliente de leitura observa o histórico do cliente de gravação que a produziu. Durante um push, o Pando controla qual histórico compartilhado é acordado entre cliente e servidor, garantindo que os clientes produzam packfiles mais fáceis de compartilhar.

Por isso, o Pando não fornece um estado de todas as referências em um dado instante durante operações Git, nem uma ordenação das atualizações de refs em um clone ou fetch. Por exemplo, suponha que os branches foo e bar sejam 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, usuários e 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.

Comparando abordagens

Nesta simulação dos três modelos de consistência, configuramos 1.000 clientes para que cada um selecione um branch distinto e faça pushes repetidos de commits exclusivos, medindo o tempo total decorrido e a latência entre push e fetch. Essa latência é uma métrica importante para as cargas de trabalho, pois mede o tempo de ida e volta para usuários e agentes colaborarem ou usarem outros sistemas que dependem do controle de versão. A simulação modela hardware que vai de máquinas virtuais e sistemas de armazenamento de blobs comuns a opções de alto desempenho. Nesta simulação, flexibilizamos a consistência em todo o repositório para o 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, de hardware comum a opções de alto desempenho, o número de clientes e outras configurações para ver como as abordagens se comparam.

O sistema de commit em três fases modelado apresenta latência de gravação significativamente maior sob carga à medida que seus discos saturam. Já os sistemas de log de gravação antecipada apresentam latência de leitura significativamente maior pelo mesmo motivo, pois os servidores têm dificuldade para acompanhar a leitura do log. Tanto nos testes reais em nosso monorepo quanto nesta simulação, o Pando suporta muitos milhares de clientes fazendo push simultaneamente 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 desempacotá-lo em um repositório local. Quando um cliente inicia um fetch, o Pando seleciona os pacotes armazenados de que ele precisa, e o cliente os baixa. Nenhum servidor precisa aplicar packfiles a um repositório local nem realizar operações custosas de desempacotamento e empacotamento. Como esses servidores se dedicam principalmente à negociação e à seleção de packfiles, os servidores Pando podem usar hardware comum.

Isso permite que o Pando escale até os 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 trabalha em um branch independente pode transferir pacotes na velocidade máxima de sua própria conexão. Para atender a mais clientes nessa velocidade, podemos adicionar servidores Pando e ampliar os armazenamentos de metadados e blobs junto com eles.

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 arquivos, cria um commit localmente e executa git push, que cria um packfile com essas alterações para atualizar um branch. O Pando recebe o pacote e o transmite para o armazenamento de blobs. Depois de validar o push recebido com uma decodificação em passagem única, o Pando usa uma transação com várias chaves para registrar uma versão de ref no armazenamento de metadados e atualizar atomicamente o ponteiro da ref. Isso permite que o Pando ofereça consistência de "leitura das próprias gravações". Toda essa operação de gravação exige O(1)O(1) leituras e gravações de metadados.

Quando outro cliente busca o branch, o Pando seleciona um conjunto de pacotes armazenados usando o histórico que o cliente já tem. Essa operação exige O(log⁡Δ)O(\log \Delta) leituras de metadados, sendo Δ\Delta a menor distância em versões de refs entre o que o cliente tem e o que deseja. O servidor retorna URIs de packfiles, e o cliente os baixa por meio do Express, um serviço que armazena packfiles em caches regionais.

Esse projeto se apoia nos três serviços centrais da arquitetura do Pando: pushes, fetches e compactação.

Escalando os pushes

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

Anunciar mais refs pode reduzir o tamanho do push. Isso também pode dificultar a reutilização do pacote, porque outro cliente talvez não tenha objetos que o cliente de gravação deixou de fora.

O Pando adota uma abordagem diferente. Em vez de mostrar todas as cartas, ele anuncia ao cliente apenas um conjunto selecionado de branches. Isso é feito por dois motivos:

  1. Fornecer uma visão consistente de todas as refs tem um custo alto. Com milhões de branches, só o anúncio de refs pode ultrapassar 100 MiB. Para isso, cada servidor precisa manter um mapeamento atualizado, com um custo significativo de coordenação, ou fazer uma consulta ao banco de dados cujo custo cresce com o número de refs.

  2. Ao receber apenas um subconjunto das refs, o cliente gera um pacote que depende somente do que o servidor permite explicitamente que ele exclua. Rastrear as dependências permite que o Pando reutilize o packfile gerado por um cliente em um push, fornecendo-o como resposta a um fetch posterior.

Quanto mais refs um servidor Git anuncia, mais fontes os clientes que fazem push têm para excluir objetos, o que permite produzir pacotes menores. No entanto, para reutilizar esses packfiles, o servidor precisa comprovar que os clientes que fazem fetch possuem cada uma das dependências. Caso contrário, o packfile pode omitir objetos de que o cliente precisa.

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 usado por qualquer outro cliente, mas com um custo significativo: buscar qualquer branch se torna, na prática, um clone completo.

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

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

Por padrão, quando um cliente faz push de um novo branch de funcionalidade, o Pando anuncia apenas um branch principal designado como base. Isso mantém o anúncio com o mesmo tamanho à medida que o repositório ganha branches, mas traz contrapartidas. Quando um engenheiro faz push para o mesmo branch várias vezes ou empilha um branch de funcionalidade sobre outro, o cliente precisa reenviar tudo o que não está no branch principal.

Isso aumenta o tamanho dos pushes sucessivos para o branch, que passam a conter objetos potencialmente redundantes, e consome mais largura de banda do cliente. Em nosso monorepo, o tamanho mediano dos packfiles produzidos dessa forma ficou abaixo de 20 KiB, e o percentil 99 ficou abaixo de 3 MiB. Para nós, essa foi uma troca vantajosa, porque um cliente de leitura que já tem o histórico anunciado do branch principal pode buscar o branch com esse único packfile.

Depois de escolher o conjunto de refs a anunciar ao cliente, o Pando aceita o push e atualiza seu armazenamento de metadados. O Pando divide a atualização de metadados em partes síncronas e assíncronas. A atualização síncrona dos metadados garante que os clientes possam buscar imediatamente o commit recém-atualizado. Ela grava uma nova versão de ref e atualiza o mapeamento do ponteiro da ref para essa versão, armazenando o novo commit de ponta, as dependências usadas na negociação do packfile e um link para o packfile enviado no push.

Ao mesmo tempo, tarefas assíncronas, como o agendamento da compactação, são registradas na mesma transação. Para pushes muito grandes, apenas um subconjunto dos commits é gravado de forma síncrona. Isso nos permite exigir apenas um número constante de leituras e gravações no armazenamento de metadados, independentemente do tamanho do push.

É importante destacar que os servidores Pando podem aceitar pushes para refs sem relação entre si de forma independente. A única chave sujeita a contenção é o ponteiro da ref, que protegemos com uma operação atômica semelhante a compare-and-swap. Podemos aumentar a taxa de processamento de gravação adicionando servidores Pando, unidades de escalabilidade ao armazenamento de metadados ou armazenamentos de blobs.

Fetch e clone na velocidade máxima da rede

Quando um cliente faz um fetch, ele fornece ao servidor dois conjuntos de IDs de commits, conhecidos no protocolo como have e want. Em um servidor Git convencional, o servidor calcula a diferença entre os conjuntos de objetos alcançáveis pelos haves e pelos wants, comprime os objetos necessários e envia o resultado ao cliente. Embora o servidor Git de código aberto tenha diversos índices locais para acelerar essa operação, gerar esses packfiles pode ser custoso, e eles geralmente são difíceis de reutilizar em cache.

Em vez disso, o Pando usa os mapeamentos de commits registrados nos pushes para consultar os have e want do cliente e associá-los a um conjunto de versões de refs que contêm esses commits. Cada uma dessas versões de refs aponta para um packfile recebido anteriormente pelo Pando. O servidor usa isso para determinar o que o cliente precisa, selecionando os packfiles que cobrem o conjunto de objetos necessários.

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 refs. Solicitar um branch secundário implica solicitar também o histórico até suas dependências no branch principal.

O protocolo Git permite enviar apenas um packfile na resposta HTTP de um clone ou fetch. No entanto, um recurso pouco valorizado do protocolo Git, packfile-uri, permite que o servidor retorne links para baixar pacotes adicionais separadamente. O Pando usa exclusivamente esse recurso e transmite um packfile vazio na própria resposta. Para dar suporte a esse recurso, a OpenAI contribuiu com várias melhorias em packfile-uri para o projeto original.

Os sistemas da OpenAI operam em várias regiões geográficas, por isso mantemos clusters Pando com caches locais em cada uma. O Express, um serviço de entrega de pacotes, mantém um cache do nosso armazenamento de blobs, e outro serviço mantém um cache do armazenamento de metadados. Esses caches permitem que o Pando atenda a solicitações de um ID de commit ou de uma leitura especial de uma ref usando apenas o estado em cache naquele cluster. Usamos hashing consistente para encaminhar solicitações de packfiles aos servidores Express, com taxas de acerto de cache superiores a 99%.

Esse sistema de caches oferece uma taxa de processamento extremamente alta, capaz de saturar a capacidade de rede entre os clientes e o Pando. Com essas URIs de packfiles e caches locais, validamos que operações de clone e fetch saturam links de rede de 10 Gbps usando apenas máquinas virtuais padrão de provedores de nuvem.

Compactação e tempo até o primeiro byte

Na maioria dos servidores Git, o reempacotamento e a coleta 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.

Em um repositório que muda rapidamente, como o monorepo da OpenAI, é inviável enviar uma URI de packfile individual para cada push ocorrido desde o último fetch. A cada dia, um cliente precisaria baixar dezenas de milhares de packfiles para atualizar seu branch principal.

Em vez disso, o Pando combina pacotes em segundo plano para reduzir esse custo, adotando um projeto semelhante à compactação em árvores LSM (log-structured merge-trees). Cada push produz o que o Pando chama de packfile "L0", que representa a menor unidade de dados que ele pode transmitir. Com o tempo, processos em segundo plano reúnem esses packfiles em coberturas que abrangem uma sequência de versões de refs, crescendo geometricamente a partir de L0. Hoje, nosso monorepo usa coberturas de packfiles de até L4.

Essas coberturas têm duas funções. Quando uma cobertura está suficientemente cheia, um processo de compactação assume seu processamento e produz um único packfile. Isso melhora a taxa de compressão ao reorganizar os objetos dentro do packfile. Mas também melhora o desempenho da negociação de fetch ao funcionar como uma lista de saltos sobre as versões de refs. Ao percorrer os índices de cada nível simultaneamente, o Pando consegue selecionar um conjunto de pacotes para enviar ao cliente em tempo logarítmico. Essas coberturas de packfiles, reempacotadas ou não, permitem uma solução mais eficiente para o planejamento de fetches, reduzindo os requisitos de taxa de processamento e 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 packfiles de várias versões de refs em segundo plano, permitindo fetches mais eficientes com menos arquivos.

Ao contrário do que ocorre em muitos servidores Git, os packfiles são mantidos em todos os níveis, sem serem descartados nem removidos pela coleta de lixo. Pacotes de todos os níveis são usados continuamente para atender aos clientes, com base nos have e want que eles informam em um fetch. E, para 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, pois a probabilidade de ele estar comprimido é menor.

A compactação no Pando reduz significativamente o volume total de dados transferidos aos clientes e permite que o planejamento de pacotes cresça de forma sublinear em relação ao quanto o cliente está desatualizado. Isso é especialmente importante para repositórios grandes e que mudam rapidamente, algo que esperamos se tornar o novo padrão no desenvolvimento de software.

Criando o Pando

Criamos o Pando com o Codex, indo do início do projeto à produção em apenas 10 semanas. O núcleo do Pando foi projetado por um único engenheiro, implementado por equipes de agentes e implantado por um pequeno grupo de engenheiros. O Pando começou a atender ao tráfego de produção em abril deste ano e faz parte de um sistema mais amplo de ferramentas de gestão de controle de versão usadas na OpenAI.

Usamos uma ampla variedade de abordagens para validar o projeto do Pando. Aplicamos nossos princípios para construir bases de software mais seguras⁠. Como o projeto começou do zero, optamos por implementar o Pando em Rust e sem dependências legadas. Criamos uma suíte de testes de conformidade para que os agentes pudessem testar suas alterações em relação 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 sutis e regressões na negociação e no planejamento de pacotes.

Muitos modelos contribuíram para esse sistema. À medida que novos modelos eram disponibilizados, nós os usávamos para melhorar o desempenho e a confiabilidade do Pando. Usando o GPT‑6 Astra, agentes escreveram código em Lean e TLA+ para formalizar uma "teoria do Pando", que usamos para encontrar e corrigir bugs na implementação e provar propriedades do sistema. Várias sessões contínuas duraram centenas de horas, encontrando e corrigindo casos extremos no Pando. Também mantemos agentes em execução 24 horas por dia para realizar testes adversariais no Git e no Pando com modelos dotados de recursos do Daybreak.

Implantando o Pando

Inicialmente, implantamos o Pando como um proxy que encaminhava leituras e gravações ao nosso sistema de controle de versão existente. Isso nos permitiu manter o Pando e o sistema anterior em sincronia, para transferir o tráfego ao Pando com segurança e sem indisponibilidade.

Como parte de seu projeto de cache, o Pando também pode deduplicar e combinar solicitações antes de encaminhá-las. Isso nos permitiu manter as ferramentas existentes funcionando bem enquanto migrávamos os sistemas para o Pando, um de cada vez. À medida que migrávamos mais cargas de trabalho para o Pando, seus pushes preenchiam o cache do Pando e melhoravam a confiabilidade do backend anterior.

Para otimizar o desempenho ao usar o Pando, começamos a disponibilizar nossa distribuição pública do Git nas máquinas dos desenvolvedores e no aplicativo do ChatGPT para desktop. Usamos uma camada interna em torno da CLI do Git para migrar os clientes para o Pando em fases. Criamos muitas outras ferramentas para lidar com solicitações de alteração, revisão de código e APIs para automações. Junto com o tráfego Git, migramos centenas de integrações que dependiam de APIs legadas, webhooks e downloads de repositórios.

O próprio Codex ajudou desenvolvedores e equipes a fazer a transição. Criamos skills para preparar os checkouts existentes e orientar os agentes a usar o novo sistema. Com isso, os desenvolvedores podiam simplesmente pedir ao Codex que migrasse seus ambientes de desenvolvimento e serviços.

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

O Pando consegue entregar um petabyte de dados Git por dia com apenas alguns núcleos de CPU. O Pando já entregou mais de 57 terabytes de pacotes Git em uma única hora, com mais de 99% desses bytes vindos do cache em disco local.

A reutilização de pacotes permite que muitos clientes baixem o mesmo histórico de repositório com pouco processamento nos nós que os fornecem. Esse exemplo mostra um único repositório, mas o Pando foi projetado para dar suporte a muitos outros. Conforme a demanda cresce, podemos escalar a entrega de dados e a negociação Git de forma independente.

Retribuindo ao projeto Git

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

Por exemplo, o Pando fornece pacotes reutilizáveis diretamente aos clientes, de modo que baixar e indexar esses pacotes se tornou essencial para tornar o sistema rápido e confiável. Esse trabalho revelou casos extremos em downloads simultâneos, na limpeza após transferências interrompidas e na retomada de downloads que já haviam sido concluídos. Corrigimos esses fluxos de execução, adicionamos validações ao aceitar um pacote existente para um fetch e contribuímos com esse trabalho para o projeto original.

Também precisávamos entender onde o tempo e a largura de banda estavam sendo consumidos. Por isso, adicionamos rastreamento aos downloads de packfile-uri e contagens de bytes à geração de pacotes, facilitando identificar se um fetch lento gastava tempo gerando ou transferindo pacotes.

Na versão do Git da OpenAI⁠(abre em uma nova janela), o cliente pode baixar e indexar pacotes independentes em paralelo. Adicionamos o conceito de "haves explícitos" para que um servidor possa anunciar o commit de ponta atual de um branch separadamente do histórico que um cliente pode omitir do pacote ao fazer push. No Pando, isso permite que os clientes usem --force-with-lease para verificar se um branch de funcionalidade não mudou e criar seu pacote usando apenas o branch principal como base. Qualquer cliente com esse histórico do branch principal pode então buscar o branch usando o mesmo pacote.

Várias dessas melhorias já estão no projeto original do Git, e muitas outras estão em nosso repositório público. Continuamos trabalhando com a comunidade Git para tornar essas melhorias úteis também fora do Pando. Você pode acompanhar esse trabalho em nosso rastreador público de patches⁠(abre em uma nova janela).

Próximos passos

Ao repensar as práticas convencionais para escalar repositórios Git, o Pando nos permite ampliar nosso monorepo sem mudar os fluxos de trabalho do Git que nossos engenheiros e agentes já usam. Em nosso monorepo, o Pando permitiu um grau maior de paralelismo, melhorou a confiabilidade dos merges e reduziu o tempo necessário para publicar alterações de código. Além do monorepo, também estamos explorando outros usos do Pando na OpenAI, incluindo cargas de trabalho em que um grande número de agentes precisa colaborar em um repositório Git compartilhado.

À medida que engenheiros e agentes produzem mais software, mais equipes fora da OpenAI encontrarão os limites que atingimos no controle de versão. Estamos explorando maneiras de compartilhar com nossos usuários a tecnologia que torna o Pando possível.

O Pando se apoia em duas décadas de experiência de usuários e operadores de repositórios Git. Continuaremos a desenvolver o Pando enquanto trabalhamos com a comunidade de código aberto do Git para compartilhar melhorias que beneficiem os usuários do Git de forma mais ampla.

Autor

OpenAI