Pesquisa

Git em qualquer escala

VM
Vicent Martí28 min de leitura

Hospedar repositórios Git em escala é um pesadelo. Quando Linus Torvalds projetou a primeira versão do gerenciador de informações infernal (que é, de fato, o slogan do Git, confira), ele tinha em mente um caso de uso muito específico: o seu próprio. Ele queria substituir o BitKeeper, o sistema de controle de versão distribuído usado para desenvolver o kernel do Linux. É claro que o substituto também precisava ser distribuído. O kernel é um projeto de software incomum: é extremamente descentralizado, com muitos mantenedores para seus diversos subsistemas. Um sistema de controle de versão distribuído é ideal para esse fluxo de trabalho.

Vinte anos depois, o Git se tornou um padrão do setor, mas a verdade é que sua natureza distribuída é mais um obstáculo do que uma vantagem. O projeto médio de software de código aberto não adota um fluxo de trabalho descentralizado. E a empresa média, muito menos. Eles aproveitam as muitas vantagens do modelo distribuído (como poder trabalhar offline e adiar pushes), mas dependem fortemente de um host centralizado. E hospedar um repositório Git, ao que parece, é incrivelmente difícil.

O que é difícil no Git?

O desafio de hospedar repositórios Git em escala é inerente ao próprio design do Git: por ser um sistema de controle de versão distribuído, todas as instâncias de um repositório são idênticas. Não há nada de especial em um repositório em um servidor Git que não se aplique a um repositório no laptop de um desenvolvedor. Embora, à primeira vista, isso pareça tornar simples a hospedagem de repositórios Git (basta colocar um daemon HTTP na frente de uma cópia do repositório em disco e pronto, você tem um servidor Git funcionando!), há diversos desafios de escalabilidade e confiabilidade que tornam a realidade exatamente oposta.

Em um repositório Git comum, seu código e metadados (arquivos, commits, árvores) são compactados e armazenados em packfiles — um formato simples de serialização binária, prático de manipular em uma máquina local, mas não ideal para gerenciar em escala em um servidor. Os packfiles são o componente fundamental do armazenamento e da comunicação em rede do Git. Quando você faz push ou fetch de dados de um repositório, eles são transferidos como um packfile.

É assim que o Git funciona por design, mas é razoável pensar que não precisaria ser dessa forma. Afinal, você não controla o cliente Git (pelo menos não sem incomodar seus usuários e adicionar muita fricção), mas, dentro do seu próprio servidor, pode fazer o que quiser. Nada obriga você a usar packfiles — Linus não vai aparecer para conferir. A única restrição é que você precisa receber e enviar packfiles pela rede em todas as operações Git.

Ao longo dos anos, empresas que tentaram hospedar repositórios Git em escala perceberam que esse design baseado em packfiles era uma grande limitação tanto para a disponibilidade quanto para a escalabilidade. Packfiles são grandes arquivos binários que precisam existir em um sistema de arquivos para que o Git possa acessá-los. A abordagem simples de ter um servidor HTTP na frente de um repositório em disco tem um limite muito baixo. Idealmente, o repositório deveria existir em vários discos e máquinas (isso permite executar muitas operações Git em paralelo e mantém o repositório disponível quando um servidor falha). Mas como fazer isso?

Há, em linhas gerais, três abordagens possíveis para isso, em ordem crescente de complexidade: distribuir o sistema de arquivos, distribuir os packfiles ou distribuir o próprio Git.

Git sem packfiles

O Git é um armazenamento de dados endereçável por conteúdo. Todos os objetos de um repositório Git (blobs, trees, commits etc.) são identificados pelo SHA-1 de seu conteúdo. Isso se encaixa intuitivamente muito bem em um armazenamento distribuído de chave-valor (a chave é o SHA-1; o valor é o próprio objeto) e poderia oferecer uma forma simples de escalar o armazenamento de um repositório. Mas, na prática, isso não funciona.

O problema é o seguinte: a estrutura real de um repositório Git é um grafo acíclico direcionado (DAG, na sigla em inglês). Você pode consultar qualquer objeto pelo SHA, mas, para executar até mesmo a operação mais trivial no repositório, precisa percorrer o DAG passo a passo.

COMMIT DAGTREE /main → c8f3?commit · c8f3NETWORK↓ OLDER COMMITSobjects 0/54 · round-trips 0key/value storeaa42a112c8f3e816f0214b70e8c487ab19b4d5c277b22dc83f7dc43040c22d6e21aa729a0db50f627cf191fe3e81b19092d080a5f31134e06c811f6e7a196e42e147b9086a70ce192ee45d83f7a9b02ec2a70c49b8e25ac074b1a93d47dd9b519d2a8c14d431dc31ef09e205

Se quiser executar uma operação como listar as alterações recentes de um repositório, você precisa processar seus commits. Ao processar um commit, você obtém um ponteiro para a raiz de sua tree. A partir dessa tree, obtém ponteiros para cada arquivo e cada subtree. Do commit original, você obtém um ponteiro para seu parent (o que o precede no histórico). O ponto crucial é que, em cada etapa desse percurso, você só sabe o valor do próximo ponteiro depois de buscar o anterior. Se cada busca exigir uma ida e volta a um armazenamento distribuído, os custos aumentam muito rapidamente.

Essa abordagem de distribuir o Git no nível dos objetos já foi tentada muitas vezes e costuma falhar em escala. A implementação mais promissora foi desenvolvida pelo meu ex-mentor Shawn Pearce quando ele trabalhava na equipe de sistemas de controle de versão do Google. A abordagem dele consistia em armazenar os objetos em uma tabela hash distribuída. Isso só foi possível graças ao JGit, uma implementação personalizada do Git em Java. Como toda boa e velha biblioteca Java, o JGit fornece interfaces, fábricas e fábricas de interfaces suficientes para abstrair todos os detalhes de um repositório Git comum, incluindo a substituição dos packfiles em disco por uma DHT. Embora o sistema funcionasse e os resultados fossem bons o bastante para operações Git normais, as limitações do protocolo Git (que, novamente, exigem o envio de packfiles pela rede, independentemente de como os dados são armazenados no servidor) deixaram a performance de git clone ruim o bastante para que o design fosse descartado por completo.

GitHub e sistemas de arquivos

Alguns anos depois de o Git começar a sair de sua bolha do kernel Linux, uma startup descolada nasceu em San Francisco. O GitHub foi fundado em 2008 como uma plataforma social de programação com um slogan muito visionário: "Hospedagem de repositórios Git: não é mais um pé no saco." E não estou brincando: confira. Já em 2008, havia um amplo consenso de que, apesar de (ou talvez por causa de) seu design distribuído, o Git precisava de uma forma centralizada de hospedar repositórios Git para ser fácil de usar, e fazer isso era muito trabalhoso. O GitHub estava decidido a mudar isso.

Sua plataforma começou como (e, em grande parte, ainda é) um monólito Rails. As primeiras versões rodavam em uma única máquina, embora robusta, com um servidor Ruby e cópias dos repositórios no disco ao lado. Escalonar um app Rails é fácil: implante mais instâncias. Mas, nesse caso específico, como o Git está envolvido, eles logo se depararam com a questão recorrente que estamos tentando resolver aqui: se o app Rails precisa acessar os repositórios Git no disco, como implantar mais cópias dele?

Sendo um grupo econômico de desajustados, os primeiros engenheiros de sistemas do GitHub tentaram a abordagem mais simples que poderia resolver seus problemas de escalonamento. A ideia era que, se focassem em distribuir o sistema de arquivos (em vez dos packfiles ou do próprio Git), poderiam manter o app Rails inalterado e dedicar seu tempo a lançar mais recursos para a base de usuários em constante crescimento, em vez de fazer coisas estranhas com o Git. Muito pragmático. Não funcionou.

A equipe tentou várias abordagens de sistema de arquivos distribuído para os dados do Git: a mais óbvia, usar NFS para armazenar todos os repositórios em um servidor centralizado, foi rapidamente descartada. A implementação padrão do Git faz muitas suposições sobre a semântica do sistema de arquivos (bloqueio, fragmentação, leitura, sincronização...) que garantem uma performance razoável no sistema de arquivos local de um laptop lento de desenvolvedor, mas não levam em conta como elas se comportam em um sistema de arquivos de rede. Era lento e tinha bugs.

Outras tentativas foram feitas com tecnologias que, francamente, em retrospectiva, eram horríveis e replicavam o sistema de arquivos no nível de bloco. Uma implantação de curta duração com GFS. Uma implantação mais duradoura baseada em DRBD. Todas esbarraram em um limite. Eram terríveis de operar no dia a dia e não compensavam isso com boa performance. Tudo se resume ao design dos packfiles no disco.

Já vimos como as estruturas de dados em grafo do Git tornam idas e voltas proibitivamente caras. Infelizmente, um princípio muito semelhante também se aplica aos dados subjacentes no disco. Não há correlação entre o layout dos objetos no DAG e a forma como são posicionados em um packfile. A principal heurística usada ao gerar packfiles é minimizar seu tamanho; os objetos são posicionados aleatoriamente por todo o pack, são comprimidos e, principalmente, raramente são armazenados inteiros. A maioria dos objetos é armazenada como um delta sobre outro objeto no mesmo packfile. Ler um objeto individual, após seguir os muitos saltos lógicos na estrutura de dados em grafo, também envolve seguir saltos físicos no formato em disco.

COMMIT DAGTREE /HEAD·mergecommit · c8f3root /tree · f021server.tsblob · f7a9pack.tsblob · a112README.mdblob · c430Cargo.tomlblob · ef09main~1commit · 9d2aroot /tree · e8c4server.tsblob · d431pack.tsblob · b8e2README.mdblob · 21aaCargo.tomlblob · 92d0featurecommit · 74b1root /tree · 7a19server.tsblob · b02epack.tsblob · 7cf1README.mdblob · 5d83Cargo.tomlblob · e147merge basecommit · 5ac0root /tree · 2d6eserver.tsblob · 8c14pack.tsblob · 0f62README.mdblob · a93dCargo.tomlblob · 3e81refactorcommit · 3f7droot /tree · 6c81server.tsblob · e205pack.tsblob · 19b4README.mdblob · 6a70Cargo.tomlblob · d5c2parsercommit · aa42root /tree · 91feserver.tsblob · 4b70pack.tsblob · f311README.mdblob · 2dc8Cargo.tomlblob · 80a5docscommit · ce19root /tree · 0db5server.tsblob · 729apack.tsblob · 6e42README.mdblob · b190Cargo.tomlblob · 47ddbootstrapcommit · 87abroot /tree · 40c2server.tsblob · 1f6epack.tsblob · c2a7README.mdblob · 9b51Cargo.tomlblob · 34e0initialcommit · 2ee4root /tree · b908server.tsblob · dc31pack.tsblob · 77b2README.mdblob · e816Cargo.tomlblob · 0c49↓ OLDER COMMITSpack-7d9a.pack00005041434B00000002000000369667706B001087CB98F791BBB1A90DFB95AEA39C9096002008C2A7180F97ACF200DBBC9098D5BBD10030B5A94CB2E61004EC96B098E17C9BE5B700404A19E5C49FA44FE62B3225C446E744410050C5EB8990E84A607C5C5A3D9996324D5F0060633898968A2B4DF3066395013955B4960070A15A6898EA976BE046C3ACDE98A86F5E00803BE84A97E68450D47936985069F5B25F00902FBDE5D4C28ECFE63DB350B871E7F33A00A0BAF109D0E8F7FF556E74DAB7E6E2E71E00B058DD984BFA29344429F2E52F415382E600C0C71AD04782E703FECCE51940E862FA0600D004826A0596C94054F246984BD218C3AC00E04B2C9506ED4A02967A5071F17D9793A000F01B73670B98B79D58807A0B14E6DC5E5301002FEF9814BBB9A6E8617EE5D4B43D5AE601108478CF63F0E7F3D8BA541492E8E8FCF701205929E078E6629739693698F03127F80B0130295BE516B79BE8E68A9D1BCD23E744210140B1A5543CE88D7CD27941A14CE6C614EB0150F0E1982CE621266DD61CE5CECCF7DEE60160212997678DE7FD3C7B66EFF2E85F579E0170C2D51BAEE51554918799BF9B3F89732901808F7ED201FA87C2FD

Esse tipo de percurso aleatório por gigabytes de dados, que precisa acontecer para cada operação do Git realizada em um repositório, simplesmente não funciona bem com um sistema de arquivos de rede (seja ele replicado no nível de arquivo ou de bloco). A única forma de isso funcionar sem ficar extremamente lento é se for possível armazenar o arquivo inteiro em cache localmente. Mas, com centenas de milhares de repositórios no mesmo sistema de arquivos, o cache não é uma opção.

Por fim, os engenheiros de sistemas do GitHub aceitaram o inevitável e desistiram de distribuir o sistema de arquivos. Começaram a desenvolver um sistema RPC para que os repositórios pudessem ficar em servidores de arquivos dedicados e atualizaram o app Rails para realizar todas as operações remotamente. Isso proporcionou uma boa parcela de escalabilidade horizontal, mas não resolveu a disponibilidade nem a performance dos repositórios mais movimentados. Afinal, cada repositório ainda era armazenado em apenas uma máquina.

Spokes e consistência

O Spokes foi desenvolvido originalmente no GitHub por volta de 2013 e, desde então, tornou-se um padrão do setor. A maioria dos serviços de hospedagem Git usa uma variante da abordagem do Spokes (replicação de repositórios Git no nível da aplicação) em sua arquitetura. O principal motivo de o Spokes ter funcionado bem por tantos anos é que ele fez três escolhas fundamentais que, com o tempo, se provaram ideais:

  1. Ele não distribui o próprio Git; opera no nível do packfile.
  2. Ele armazena todos os dados como repositórios Git reais em discos NVMe locais.
  3. Ele replica os dados do Git, mas mantém todas as cópias consistentemente sincronizadas.

Devido aos padrões de leitura aleatória nos packfiles que acabamos de discutir, armazenar repositórios Git convencionais em unidades NVMe é praticamente um requisito para garantir que todas as operações básicas do Git continuem rápidas. Isso também mantém os clones eficientes, pois não é necessário transformar os dados no formato esperado pelo cliente Git. Além disso, permite que você se concentre em desenvolver um produto sobre o Git, em vez de manter um fork do Git capaz de operar nos seus repositórios incomuns.

Manter todas as cópias dos dados consistentemente sincronizadas também é, crucialmente, muito importante. Isso é algo que se aprende da pior maneira, mas o cliente Git realmente não lida bem com consistência eventual. Se o seu cliente Git local faz push de um commit e depois não consegue lê-lo imediatamente após um fetch, isso é uma péssima notícia. O Git fica muito confuso com isso. Se você executar seu pipeline de CI em cem runners e três deles não encontrarem o commit que deveriam testar após clonar seu repositório, isso também é uma péssima notícia. Além de proporcionar uma experiência de usuário muito ruim.

Trabalhar com uma visão eventualmente consistente de um repositório Git tem muitas armadilhas, seja no cliente ou no backend. Por isso, o Spokes assume um custo de complexidade muito alto para garantir que o sistema esteja sempre totalmente consistente. Vamos ver exatamente o que isso significa.

O Spokes é um sistema distribuído baseado em consenso. Ele funciona armazenando várias cópias do seu repositório Git em servidores diferentes. Sempre que você envia novos dados, um orquestrador distribui seu push para que cada instância do seu repositório receba uma cópia. Essa distribuição é sincronizada com um algoritmo clássico de consenso chamado 3PC (commit em três fases), de modo que um push só é aceito se a maioria dos nós o confirmar.

QUORUM · 4/5tx #42RESTORE ALLCOORDINATORcollecting votesPARTICIPANT 1waitingPARTICIPANT 2waitingPARTICIPANT 3offlinePARTICIPANT 4waitingPARTICIPANT 5waiting
1 · VOTING2 · PRE-COMMIT3 · DO COMMIT

O commit em três fases não tem nenhuma relação com o Git. É um algoritmo de consenso que garante que todos os nós de um sistema concordem em confirmar ou reverter uma transação; ele faz isso em três idas e vindas. É muito semelhante ao commit em duas fases, mas introduz uma fase adicional de “pré-commit” para que o sistema possa se recuperar se o coordenador ficar off-line no meio de uma transação.

Antes de podermos falar mais sobre como o Spokes usa o 3PC, precisamos entender como funciona um push do Git. Um push do Git tem dois componentes: um packfile e uma transação de referência. O packfile, sobre o qual já falamos, contém os objetos que você está enviando ao repositório (blobs, árvores e commits com suas alterações). A transação é o que efetivamente publica suas alterações no repositório, atualizando uma ou mais referências (por exemplo, a branch em que você está trabalhando) para apontar para os commits que você acabou de enviar.

Essa separação é muito conveniente aqui, pois um commit enviado não fica visível ("alcançável", na terminologia do Git) até que a referência que aponta para ele seja atualizada. Isso significa que podemos implementar consenso para nossos pushes distribuindo os packfiles para todos os hosts simultaneamente (não é necessário sincronizá-los nesse ponto) e então executando um commit em três fases para a transação de referência, que é muito menor e mais rápida de sincronizar do que o packfile. O próprio Git oferece suporte para preparar transações de referência: ele pode adquirir um bloqueio na referência, verificar se o valor existente é o esperado e manter o bloqueio até receber um comando para confirmar ou abortar a transação.

Playback speed0.010x
Replicas5
One-way latency20ms
elapsed 0msSPOKEScoordinatorneeds 5 / 5 acks

O Spokes distribui packs e depois executa um commit em três fases para a transação de cada push. Você pode aumentar o número de réplicas e a latência neste simulador para ver como isso afeta o throughput de commits.

Com esse design, garantimos que cada push seja totalmente sincronizado em todas as réplicas. As leituras (fetches, clones) podem então ser roteadas com segurança para qualquer réplica, porque todas as réplicas estão sempre atualizadas.

É basicamente assim que o Spokes funciona, e ele tem funcionado muito bem nos últimos 13 anos. É claro que o Spokes não é perfeito — nenhum sistema é. Em 2026, a forma como as pessoas usam repositórios Git mudou drasticamente, e aprendemos muitas lições importantes sobre a criação de sistemas distribuídos ao longo do caminho. O tempo e a experiência mostraram quais escolhas do Spokes acabaram sendo ideais e quais não foram.

Uma falha que se mostrou crítica é a escalabilidade horizontal limitada do 3PC. Quando o Spokes foi lançado, três réplicas por repositório eram o ponto ideal. Era possível atender um repositório médio a partir de três cópias com capacidade de sobra e redundância suficiente para continuar aceitando pushes mesmo que uma máquina caísse.

Em 2026, o cenário é muito diferente. O repositório médio de uma grande empresa agora é um monorepo gigantesco. Três réplicas não bastam para atender ao tráfego desses repositórios, especialmente em CI. É claro que nada impede o Spokes de operar com mais de três réplicas, exceto a temida cauda em escala. O commit em três fases se encaixa de forma muito elegante no modelo de transações do Git, mas, como algoritmo de consenso, tem limitações fundamentais: a latência de cada etapa é limitada pelo servidor mais lento do cluster. Quanto mais réplicas você adiciona a um cluster, pior fica a taxa de transferência de pushes.

Essa restrição de escalabilidade também se aplica no sentido inverso. Quando agentes trabalham com repositórios Git em escala, muitas vezes operam fora de um monorepo, criando um grande número de repositórios pequenos — muitos descartáveis e a maioria quase intocada. O Spokes tem dificuldades nesse cenário porque ainda exige três réplicas para cada um desses repositórios. Três réplicas quase sempre ociosas, que não podem ser reduzidas porque, nesse caso, o sistema deixaria de ser totalmente consistente e haveria risco de perda de dados. Com o commit em três fases, o piso é sempre alto demais, e o teto, baixo demais.

Outra falha, impossível de perceber de início, mas dolorosamente óbvia depois de passar por ela, é que o Spokes pode ser complicado de operar em escala. Como os repositórios em disco são sempre a fonte da verdade para o consenso, cada cópia de cada repositório é muito importante. É preciso tratar os repositórios como animais de estimação, não gado.

Isso significa, antes de tudo, que você precisa saber exatamente onde está cada repositório. Isso cria uma dependência — e um possível problema de disponibilidade — de um banco de dados externo que precisa manter uma tabela de roteamento enorme, mapeando cada repositório para cada máquina em que está replicado. Cada repositório também precisa ter somas de verificação calculadas e constantemente atualizadas nessa tabela para garantir que continue válido em disco. Assim que algo dá errado com o repositório (e, acredite, coisas ruins acontecem o tempo todo — na prática, o Git pode ser bastante instável), você precisa detectá-lo e agendar uma tarefa de reparo para restaurá-lo a um estado íntegro. E isso precisa ser feito muito rapidamente! Afinal, os repositórios em disco são a fonte da verdade. Uma cópia corrompida é tão ruim quanto uma cópia ausente. Se duas das três cópias estiverem corrompidas, o sistema não poderá mais aceitar pushes: não há quórum.

Continuity

Continuity (Cnt, abreviadamente) é o sistema de armazenamento Git que desenvolvemos na Cursor, com uma abordagem muito clara: aprender com tudo o que o Spokes fez bem e corrigir o que, após muitos anos, agora sabemos que são problemas.

Cnt é um sistema simples (um sistema não pode ser fácil de operar se não for simples). Seu primitivo central é um log de gravação antecipada, armazenado em um serviço de armazenamento de objetos compatível com S3. Em produção, executamos diretamente no S3, mas o projetamos para que possa ser implantado em qualquer nuvem.

Quando um repositório recebe um push, armazenamos esse push como uma entrada de WAL no S3. Nunca confirmamos um push até que ele tenha sido totalmente persistido. Cada push é armazenado como um objeto separado; gravamos o packfile enviado em disco e o carregamos no S3 simultaneamente. No entanto, carregar uma entrada de WAL não a publica. Um push só fica visível quando preparamos com sucesso sua transação de referência em uma cópia local do repositório e registramos um ponteiro para a entrada de WAL no arquivo de índice do WAL, que é um objeto próprio no armazenamento. Isso garante que todos os pushes sejam linearizáveis.

S3 · OBJECT STORE#1.wal#2.wal#19e37.wal#23c6e.walgitwal.pbetag e2GIT CLIENTgit pushWALGITreceivingBARE REPOidle

Tentamos não fazer uma única gravação no S3 por push porque, em repositórios movimentados, isso impõe um limite rígido à taxa de transferência de pushes, com base na latência da operação PUT do S3. Com uma implementação de agrupamento cuidadosamente ajustada e exigindo apenas a sincronização da transação de referência com um único repositório local, em vez de um quórum de réplicas, temos um sistema capaz de processar pushes tão rápido quanto o disco permite.

A cópia local do repositório é, naturalmente, um repositório Git comum armazenado em uma unidade NVMe muito rápida. Fazemos o mesmo que o Spokes, porque acho que o Spokes acertou em cheio nisso. Isso nos permite reutilizar todo o excelente trabalho de código aberto da comunidade Git, incluindo o cliente Git upstream e suas muitas otimizações de performance. Assim, podemos nos concentrar em entregar novos recursos, em vez de fazer coisas estranhas com o Git.

Consenso

Vimos que um dos fatores que tornam um cluster Spokes difícil de operar é a necessidade de acompanhar cuidadosamente a localização de cada repositório em cada servidor. O Cnt faz isso de uma forma bem diferente. Onde cada repositório reside? A resposta é: "em qualquer lugar". Não importa! Tratamos os repositórios como um cache aquecido em disco, mas a fonte da verdade é sempre o log de gravação antecipada no S3. O sistema não mantém estado, e não há tabelas de roteamento (nem banco de dados relacional para operar — hashtag abençoado). Se um repositório estiver ausente do disco local quando for acessado em um host, simplesmente o materializamos a partir do WAL. Podemos fazer isso com muita eficiência, mas, é claro, não queremos fazer isso o tempo todo, pois seria desperdício. Em produção, usamos hashing de rendezvous para mapear um ID de repositório para a lista de nós em que esperamos encontrá-lo. Todo o estado necessário para rotear repositórios consiste no ID do repositório e no conjunto atual de nós saudáveis em um cluster. Mas, se esse estado ficar fora de sincronia (por exemplo, se um nó ficar indisponível), tudo bem também. Simplesmente materializamos o repositório no próximo nó.

E quanto ao consenso? Eleições? Qual servidor é o primário de um determinado repositório? Também não importa! Não há estado nem consenso aqui. Qualquer servidor pode ser o primário. Todas as atualizações do log de gravação antecipada são sincronizadas com uma operação atômica de comparação e troca (CAS) no S3, portanto, é sempre seguro que qualquer instância de um repositório receba um push. Novamente, assim como no roteamento, permitir que um servidor arbitrário atue como primário não é o mais eficiente (isso resulta em retries de CAS, que podem atrasar pushes); por isso, na prática, sempre escolhemos o mesmo servidor como primário: o primeiro da lista ordenada pelo hashing de rendezvous. Mas, nos casos extremos — quando há uma implementação, um failover ou uma breve instabilidade de rede — simplesmente não nos importamos com qual servidor é exatamente o primário. O sistema foi projetado para estar sempre correto quando degradado e sempre rápido quando saudável.

S3 · OBJECT STORE4b1e#414b1e.walgitwal.pbetag e0WALGIT Aupload pack#414b1e.wal#429f4d.walgitwal.pbWALGIT Bupload pack#414b1e.wal#42b7e3.walgitwal.pb

Replicação

Ter um log de gravação antecipada no S3 abre um mundo de possibilidades em termos de escala. Podemos ter literalmente qualquer número de réplicas, porque a escalabilidade do S3 é incomparável e todas as réplicas se atualizam diretamente a partir dele. Fazemos replicação otimista enviando pacotes UDP de gossip pelo cluster. Os pacotes contêm todos os metadados necessários para que cada réplica se atualize diretamente do S3 após cada push. "Isso é insano", ouço você murmurar por trás da tela, através do tempo e do espaço. "UDP não é um transporte confiável." Claro que não é. Nada é confiável em um sistema distribuído! A rede não é confiável, o roteamento não é confiável e a topologia também não é confiável. Mas tudo bem: isso não importa. Cada réplica conhece o ETag da última versão do índice WAL com a qual se atualizou. Quando você realiza uma operação de leitura em uma réplica, fazemos um GET condicional no S3 com o ETag esperado. Uma resposta 304 sem corpo (convenientemente, uma operação quase instantânea — menos de 10 ms em média, pois é uma operação do S3 que envolve apenas metadados) significa que estamos atualizados e podemos atender ao fetch ou ao clone imediatamente. Uma resposta 200 vem com a versão mais recente do índice WAL, que usamos para nos atualizar antes de atender à leitura.

S3 · OBJECT STORE#40.wal#41.wal#40b8ab.wal#4156e2.walgitwal.pbetag e1GIT CLIENTgit pushGIT CLIENTgit fetchWALGIT · PRIMARYreceive-packWALGIT · REPLICAcurrent · e1#40b8ab.wal#4156e2.walgitwal.pbetag e1

Não importa se o pacote UDP de replicação se perde ou se chega ao servidor errado porque a topologia mudou. Todas as leituras em todas as réplicas são totalmente consistentes, pois são verificadas em relação à fonte da verdade, que é o S3. O sistema foi projetado para estar sempre correto quando degradado e sempre rápido quando saudável.

As implicações disso são duas. Primeiro, como o sistema é sempre consistente, desenvolver infraestrutura sobre ele é trivial. Nós (nossos agentes, nossa interface web, nossos clientes) sempre vemos uma visão globalmente consistente do repositório. E, como o sistema escala em ambas as direções, cada repositório recebe exatamente o número ideal de réplicas. Um monorepo grande pode ser implantado em centenas de réplicas para atender a toda a carga dos seus jobs de CI. Milhões de repositórios pequenos criados por agentes podem ser atendidos por uma réplica cada; não precisamos de mais de uma para garantir disponibilidade, porque o S3 é a fonte da verdade. Na verdade, um repositório ocioso nem precisa disso: quando uma réplica não recebe tráfego há algum tempo, removemos seus dados do disco do nó e simplesmente a materializamos novamente a partir do WAL na próxima vez que chegar um fetch.

S3 · OBJECT STORE

Compactação

Logs de gravação antecipada exigem compactação periódica. Não é possível deixar o log crescer indefinidamente: uma restauração completa reproduz todas as entradas, portanto, quanto mais entradas houver, maior será o custo.

Coincidentemente, um repositório Git comum também exige compactação periódica, embora o Git não seja baseado em um WAL. Vimos que a unidade fundamental de armazenamento em um repositório Git é o packfile. Sempre que você faz push para uma cópia remota de um repositório ou fetch para sua cópia local, cria um novo packfile. Isso não escala indefinidamente: cada packfile tem seu próprio índice associado, que permite ao Git localizar com eficiência os objetos que contém, mas essa busca só é eficiente para cada packfile individualmente. Se você procura um objeto específico e seu repositório tem 100 packfiles, precisará abrir o índice de cada um e buscar o objeto até encontrá-lo em um dos packfiles. Uma operação eficiente deixa de ser eficiente quando precisa ser executada centenas ou milhares de vezes.

O Git moderno se tornou muito bom em contornar isso: agora oferece suporte a índices multi-pack e compactação geométrica incremental. Mas, em algum momento, é inevitável reempacotar o repositório Git no disco. Historicamente, isso tem sido um problema recorrente de disponibilidade para sistemas como o Spokes, porque o reempacotamento exige muito processamento, mesmo quando feito de forma incremental, e precisa ser executado em todas as réplicas do sistema. Acionar acidentalmente uma operação de manutenção em dois ou mais nós do Spokes para o mesmo repositório pode facilmente fazer o repositório realizar failover.

Aqui, amortizamos o custo da compactação. Apenas o primário realiza compactações, e o resultado se aplica tanto ao repositório no disco quanto ao WAL. Como todas as réplicas seguem o WAL, elas também seguem os eventos de compactação. As réplicas não reempacotam; elas simplesmente baixam do S3 os packs já compactados, trocando CPU por largura de banda.

WALGIT · LOCAL PACKSgeometric compaction0ec8f61e8f28979dbe0e6db3COMPACTION FRONTIERPACKS ARRIVES3 · SOURCE OF TRUTH5 packs#126db3.wal#40be0e.wal#970ec8.wal#98f61e.wal#998f28.walgitwal.pb

Escala

A replicação e a compactação são os dois principais fatores que determinam o desempenho de um sistema de armazenamento Git sob carga. Como vimos, elas estão intrinsecamente ligadas: quanto mais pushes por segundo um repositório recebe, mais o desempenho de leitura é prejudicado, pois os packfiles de cada push precisam ser compactados para que as operações Git permaneçam eficientes. Se esses pushes forem replicados, a compactação também precisará ser replicada ou executada de forma independente em cada réplica.

A arquitetura do Continuity baseada em WAL oferece escalabilidade horizontal totalmente consistente: é possível implantar um número arbitrário de réplicas, e a taxa de transferência das operações Git somente de leitura cresce linearmente com elas. Como todas as réplicas do cluster são totalmente consistentes, podemos escalar o protocolo Git (clones, fetches) e todas as operações RPC que o Origin executa sobre os repositórios (interações na interface web, API REST, todas as nossas interfaces agênticas etc.).

Executamos testes de estresse sintéticos com até 100 réplicas e observamos escalonamento linear consistente para leituras, sem regressões na taxa de transferência de pushes.

A taxa de transferência de pushes de um cluster depende da latência com que conseguimos atualizar nosso WAL no S3. Com o S3 Standard, conseguimos sustentar até 120 pushes/s enquanto compactamos e replicamos os dados compactados para todos os demais nós. Também implantamos clusters de alto desempenho no S3 Express One Zone, que apresenta latência muito menor para operações PUT. Nesse caso, conseguimos receber mais de 300 pushes/s e ficamos efetivamente limitados pela velocidade com que o Git consegue compactar os dados em disco. Estamos trabalhando em formas inovadoras de organizar esses dados em disco para reduzir o impacto da compactação: nosso objetivo é continuar otimizando a velocidade com que um repositório Git consegue receber código sem flexibilizar nossas rigorosas garantias de durabilidade e consistência.

  • S3 Standard
  • S3 Express One Zone

Taxa de transferência de pushes/clones para everysphere, o monorepo do Cursor.
Todos os pushes são linearizáveis e persistidos em armazenamento externo antes da confirmação.
Todos os clones são totalmente consistentes.

WAL como fonte da verdade

O S3 é uma ótima tecnologia. Todo o conceito de armazenamento de blobs, introduzido pela API do S3, acabou se tornando um componente fundamental muito poderoso para sistemas de armazenamento de grandes volumes de dados, e isso certamente também se aplica à hospedagem de repositórios Git. O design apresentado aqui é inovador em muitos aspectos, mas não é o primeiro a armazenar packfiles como blobs. O Azure DevOps (o concorrente da própria Microsoft para o GitHub da própria Microsoft) tem um sistema de armazenamento Git muito bem-sucedido que armazena packfiles em armazenamento de blobs e suas referências em um banco de dados relacional (MS SQL Server). Há muitas concessões em um sistema como esse. Um banco de dados relacional escala bem com grandes transações de referências. Mas, nesse caso, é preciso operar um banco de dados relacional. Acreditamos firmemente que a consistência dos dados Git é mais importante do que qualquer outra consideração. Foi isso que nos levou a projetar um sistema baseado em WAL que não depende de bancos de dados externos.

Há muitas coisas que podem dar errado com um repositório Git em produção: corrupção de dados em repouso, bugs durante o reempacotamento e condições de corrida durante pushes. É uma grande coleção de casos extremos. A maioria deles foi corrigida no Git upstream, mas não todos. Nenhum sistema está livre de bugs, nem mesmo os de código aberto e amplamente implantados. Nosso modelo de consistência garante que rastreamos todas as operações fundamentais realizadas em um repositório. Nunca confirmamos um push até que ele tenha sido totalmente persistido no WAL. Linearizamos todos os pushes. Todas as visualizações de todos os repositórios que acessamos são sempre totalmente consistentes. Como cada push está no WAL, podemos examinar todos os estados pelos quais um repositório já passou. Temos dados completos de procedência para todos os pushes e reempacotamentos. Podemos retroceder e avançar rapidamente cada réplica. Não precisamos sincronizar nenhum estado com nenhum banco de dados externo, seja ele um banco que armazena apenas referências ou todos os dados de objetos. Quando — e não se — encontrarmos um bug no Git, poderemos identificar exatamente o que aconteceu e revertê-lo. Além dos bugs que já existem no Git, introduzimos pouquíssimos novos, pois, durante todo esse processo, todas as operações Git são realizadas em um repositório Git comum em disco, usando ferramentas padrão.

Origem

Temos plena consciência da importância de hospedar o código-fonte de alguém. Acredito que todos que leem e entendem esta postagem do blog tenham a mesma percepção. Uma empresa pode ficar paralisada se seus desenvolvedores não conseguirem fazer push ou pull em seus repositórios Git. É difícil quantificar em dólares o custo de produtividade de cinco minutos de indisponibilidade no sistema de CI, mas, sob qualquer ótica, trata-se de um valor gigantesco.

Os agentes mudaram fundamentalmente a forma como trabalhamos com software e, em muitos aspectos, pioraram essa situação. Mais código, mais PRs, mais execuções de CI. O controle de versão está no centro de tudo isso e talvez seja a coisa mais difícil de mudar da noite para o dia.

Enfrentamos essas dificuldades internamente no Cursor há muitos meses e dedicamos muita reflexão e cuidado à criação de uma plataforma que resolva esses problemas para nós e que, esperamos, também possa resolvê-los para nossos clientes. Nosso foco agora é oferecer a transição mais tranquila possível para maior confiabilidade, melhor performance e mais escala, tornando a migração o menos dolorosa possível.

Origin não é um experimento; é o resultado de muitas décadas de experiência na criação desses mesmos sistemas por pessoas que entendem profundamente a magnitude dos desafios envolvidos. Temos uma filosofia de engenharia e operações que comprovadamente funciona e um forte compromisso de continuar a aprimorá-la à medida que o cenário do controle de versão evolui.

Esperamos que você deposite sua confiança em nós e em nossa plataforma.

Publicado em: Pesquisa

Autor: Vicent Martí