OpenAI

2 listopada 2026

Inżynieria

Skalowanie Gita w OpenAI

Tworzenie systemu kontroli wersji na skalę pracy agentów przy jednoczesnym inwestowaniu w ekosystem Gita.

Autorzy: Aaron Friel, Taylor Blau i Ted Nyman, członkowie zespołu technicznego

Ładowanie…

W ciągu ostatniego roku rozmiar naszego monorepo i stawiane mu wymagania rosły w zawrotnym tempie. Stało się jasne, że musimy przebudować system kontroli wersji, aby sprostać skali równoległej pracy, jaką umożliwia Codex.

Stworzyliśmy Pando — nowy backend dla repozytoriów Git — bez przerywania prac inżynierskich ani zmiany sposobu, w jaki nasz zespół korzysta z Gita. Pando obsługuje teraz wszystkie prace programistyczne w monorepo OpenAI. Pando zaprojektowano z myślą o dwóch ambitnych celach. Pierwszy to skalowanie zapisów w poziomie, zoptymalizowane pod kątem równoległych operacji push do wielu gałęzi. Drugi to maksymalizacja przepustowości odczytu, tak aby klienci mogli pobierać dane wystarczająco szybko, by w pełni wykorzystywać dostępną przepustowość sieci.

Osiągnęliśmy te cele, zachowując zgodność z protokołem i klientem Git, dzięki czemu zarówno nasi inżynierowie, jak i agenci mogli płynnie włączyć Pando do dotychczasowego sposobu pracy. W środowisku produkcyjnym Pando zmniejszyło medianę opóźnienia operacji push niemal pięciokrotnie. Obecnie dostarcza niemal petabajt danych Gita dziennie.

W tym artykule przedstawimy powody stworzenia nowego systemu kontroli wersji oraz wyjaśnimy, dlaczego zdecydowaliśmy się zachować zgodność z Gitem. Porównamy nasz system z popularnymi podejściami do skalowania Gita i opiszemy, dlaczego Pando wyjątkowo dobrze odpowiada na potrzeby OpenAI w zakresie skalowalności. Omówimy też dalsze plany oraz to, jak będziemy nadal rozwijać Gita, by wspierał pracę w skali, jaką umożliwiają agenci.

Dlaczego potrzebowaliśmy nowego systemu kontroli wersji

Codex zmienił skalę i tempo tworzenia oprogramowania w OpenAI, umożliwiając inżynierom wykonywanie znacznie większej ilości pracy równolegle. To oznacza więcej gałęzi, operacji push i scaleń oraz więcej pobrań i klonowań wykonywanych przez środowiska programistyczne, zadania badawcze i CI.

Te wymagania wzajemnie się potęgują. Jeden inżynier ma teraz jednocześnie kilka zmian gotowych do CI, a każda operacja push może uruchomić kilka zadań pobierających ten sam commit.

Mniej więcej pod koniec ubiegłego roku systemy obsługujące nasze monorepo zaczęły mieć trudności z nadążaniem za obciążeniem. Doświadczaliśmy powolnych operacji fetch i push oraz nieudanych scaleń, a niezawodność chwilami spadała poniżej 90%. Czy dotarliśmy do granic możliwości samego Gita, czy raczej sposobu, w jaki udostępnialiśmy nasze monorepo? Git wyrósł ze społeczności, która wymieniała się poprawkami do jądra systemu za pośrednictwem poczty elektronicznej. Przez ponad dwie dekady programiści dostosowywali narzędzia wokół Gita do zmieniającego się sposobu tworzenia oprogramowania. Gdy zaczęliśmy odczuwać trudności w naszym monorepo, zależało nam na tym, by móc dalej korzystać z tego dorobku.

Dostrzegliśmy sposób na zachowanie znanego modelu pracy przy jednoczesnej przebudowie backendu, tak aby równolegle obsługiwał operacje push do różnych gałęzi i udostępniał tę samą historię wielu klientom naraz. Pozostawienie Gita jako fundamentu naszej kontroli wersji oznaczało, że ulepszenia klienta mogły przydać się także poza OpenAI. Chcieliśmy przekazywać te ulepszenia z naszej kopii Gita⁠(otwiera nowe okno) do głównego projektu, dzieląc się nimi z szerszą społecznością.

Projektowanie Gita do pracy na dużą skalę

Skalowanie systemu kontroli wersji ma dwa kluczowe wymiary: wielu klientów wysyłających zmiany do różnych gałęzi i wielu klientów pobierających tę samą historię. Pando optymalizuje oba.

Po pierwsze, Pando koordynuje aktualizacje każdej gałęzi niezależnie. Aktualizacje niepowiązanych gałęzi mogą przebiegać równolegle, a klient pobierający gałąź otrzymuje historię potrzebną dla danej rewizji. Kompromis polega na tym, że klient odczytujący kilka gałęzi może widzieć ich stany z różnych momentów.

Po drugie, Pando ponownie wykorzystuje pliki paczek (format Gita do wspólnej kompresji wielu obiektów) przy kolejnych pobraniach, zamiast generować osobny plik dla każdego klienta. Klient może pobrać część obiektów, które już ma, ale serwer może udostępniać te same bajty wielu klientom przy niewielkim nakładzie obliczeń.

Te decyzje pozwalają nam skalować zapisy w różnych gałęziach i obsługiwać odczyty z pamięci podręcznej. Aby zobaczyć, jak te rozwiązania się łączą, zacznijmy od sposobu przechowywania i przesyłania historii w Gicie.

Podstawy przechowywania obiektów w Gicie

Git musi utrzymywać synchronizację dwóch części repozytorium: obiektów i referencji.

Repozytorium Git zapisuje migawki plików i łączącą je historię w postaci grafu obiektów. Obiekt może być blobem, drzewem, commitem lub tagiem. Bloby przechowują zawartość pojedynczych plików, a drzewa wiążą nazwy z blobami lub innymi drzewami, tworząc strukturę katalogów. Commity wskazują na drzewo główne oraz na zero lub więcej commitów nadrzędnych, tworząc historię repozytorium.

Obiekty są identyfikowane przez skrót ich typu i zawartości. Obiekty są też niezmienne: zapisanie zmodyfikowanego pliku tworzy nowy blob, natomiast niezmienione pliki i katalogi mogą ponownie korzystać z istniejących obiektów.

Gałęzie i tagi to referencje, w skrócie „refy”, wskazujące na commity. Zatwierdzenie zmian na gałęzi tworzy nowy commit i aktualizuje referencję gałęzi, aby na niego wskazywała.

Gałąź refs/heads/main wskazuje na commit C2, którego rodzicem jest C1. C1 wskazuje na drzewo główne T1, w którym parser.py odwołuje się do bloba B1. C2 wskazuje na drzewo główne T2, w którym parser.py odwołuje się do bloba B2. W obu drzewach głównych docs/ odwołuje się do tego samego drzewa T3. Wpis README.md w tym drzewie wskazuje na blob B3, a usage.md — na blob B4.

Obiekty Gita odwołują się do siebie przez identyfikatory: commity do drzew, a drzewa do innych drzew i blobów. Gałąź refs/heads/main wskazuje na commit C2, którego rodzicem jest C1. C1 wskazuje na drzewo główne T1, w którym parser.py odwołuje się do bloba B1. C2 wskazuje na drzewo główne T2, w którym parser.py odwołuje się do bloba B2. W obu drzewach głównych docs/ odwołuje się do tego samego drzewa T3. Wpis README.md w tym drzewie wskazuje na blob B3, a usage.md — na blob B4.

Git przechowuje obiekty w stanie „luźnym” lub „spakowanym”. Luźne obiekty są przechowywane w osobnych plikach identyfikowanych przez identyfikator obiektu. Spakowane obiekty są przechowywane razem w pliku paczki, co pozwala zapisywać podobne obiekty jako różnice względem siebie.

W $GIT_DIR/objects znajdują się dwie wersje pliku parser.py. W postaci luźnych obiektów blob A zawiera parse(), a blob B zawiera zarówno parse(), jak i parse_file(). Plik paczki przechowuje A w całości, a B przedstawia jako instrukcje skopiowania całego A i dopisania parse_file(). Towarzyszący plik *.idx wskazuje położenie obu blobów w pliku paczki na podstawie identyfikatorów obiektów.

Git może przechowywać bloby jako osobne pliki lub pakować je razem, kodując jeden blob jako różnicę względem drugiego.

W Gicie obiekt jest osiągalny z commita, jeśli Git może go znaleźć, podążając od tego commita przez powiązania z jego rodzicami, drzewami i blobami. Podczas pobierania klient informuje serwer, których commitów żąda i które już ma. Serwer znajduje obiekty osiągalne z żądanych commitów, pomija te, o których wie, że klient już je ma, i wysyła pozostałe jako plik paczki w tym samym formacie, którego Git używa na dysku. Pełne klonowanie rozpoczyna się bez żadnych obiektów, więc wymaga wszystkich obiektów osiągalnych z klonowanej historii.

Operacja pushowania przesyła obiekty w przeciwnym kierunku, żądając od serwera aktualizacji co najmniej jednej referencji. Klient korzysta z zestawu referencji ogłaszanych przez serwer, aby nie wysyłać obiektów, które serwer już ma. Wysyłana paczka może więc zależeć od istniejących obiektów: odebranie samej paczki nie wystarczy do odtworzenia nowej historii.

Replikacja repozytorium Git oznacza utrzymywanie dostępności zarówno obiektów, jak i referencji na wielu serwerach. Zanim replika będzie mogła udostępnić nową wartość referencji, musi być w stanie dostarczyć obiekty osiągalne z tej wartości. Usługa musi też koordynować konkurujące aktualizacje referencji, czyli operacje pushowania próbujące przesunąć tę samą gałąź z tej samej starej końcówki do różnych nowych commitów. Systemy narzucające jedną kolejność wszystkich zapisów do repozytorium mogą sprawiać, że operacje pushowania do niepowiązanych gałęzi muszą na siebie czekać.

Istniejące wzorce rozproszonych usług Git

Projektując Pando, oceniliśmy tradycyjne podejścia do skalowania kontroli wersji opartej na Gicie. Jedno koordynuje aktualizacje referencji między replikami repozytorium za pomocą zatwierdzania trójfazowego (3PC — protokołu koordynującego zatwierdzanie transakcji w wielu węzłach). Drugie ustala kolejność aktualizacji przez wspólny dziennik zapisu wyprzedzającego (WAL — rejestr zmian zapisywanych przed ich zastosowaniem) w magazynie blobów.

Zatwierdzanie trójfazowe (3PC)

W modelowanym przez nas rozwiązaniu z zatwierdzaniem trójfazowym usługa Git utrzymuje pełną kopię repozytorium w kilku replikach. Gdy dowolny serwer odbiera żądanie pushowania, przychodzący plik paczki jest kopiowany i wysyłany równolegle do wszystkich replik. Aby opublikować zmiany z operacji pushowania, większość replik musi zaakceptować aktualizację referencji. Zarządzający tym serwer pełni rolę koordynatora, a proces wymaga kilku rund komunikacji między nim a każdą z pozostałych replik.

Aby zaakceptować aktualizację referencji, każda replika blokuje swoją kopię referencji i sprawdza, czy nadal wskazuje ona na commit oczekiwany przez klienta. Koordynator wysyła następnie komunikat wstępnego zatwierdzenia do replik, które zaakceptowały aktualizację. Gdy większość potwierdzi odbiór tego komunikatu, koordynator nakazuje im wykonanie aktualizacji. Każda aktualna replika może wtedy ogłaszać nową końcówkę gałęzi ze swojej lokalnej kopii repozytorium.

Utrzymywanie pełnych replik pozwala obsługiwać odczyty lokalnie i gwarantuje klientom spójny widok wszystkich referencji, niezależnie od wybranej repliki. W konsekwencji referencje są aktualizowane pojedynczo, a każda operacja pushowania wymaga kilku pełnych cykli komunikacji w celu skoordynowania aktualizacji referencji między replikami.

Oczekiwanie na kworum sprawia, że czas pushowania zależy od wydajności dysków replik i opóźnień komunikacji. W miarę wzrostu liczby zapisów repliki muszą nadążać zarówno z obsługą przychodzących paczek, jak i ruchu odczytowego.

Dziennik zapisu wyprzedzającego (WAL)

W modelowanym przez nas rozwiązaniu opartym na WAL usługa Git używa wspólnego manifestu w magazynie blobów do ustalania kolejności aktualizacji w całym repozytorium. Gdy serwer odbiera żądanie pushowania, przesyła przychodzący plik paczki do magazynu blobów i przygotowuje aktualizację referencji w swojej lokalnej kopii repozytorium. Aby opublikować zmiany, dodaje do wspólnego manifestu w magazynie blobów wpis, który rejestruje aktualizację referencji i wskazuje plik paczki. Następnie serwer Git aktualizuje manifest za pomocą operacji compare-and-swap. Operacja ta atomowo zastępuje manifest albo odrzuca zmianę. Jeśli inny klient zapisujący zaktualizował go w międzyczasie, serwer odczytuje dodane wpisy, sprawdza, czy jego aktualizacja referencji nadal jest poprawna, i ponawia próbę, używając już przesłanego pliku paczki.

Klienci zapisujący mogą przesyłać swoje pliki paczek równolegle, ale nawet operacje pushowania do niepowiązanych gałęzi wymagają aktualizacji tego samego manifestu. Czas pełnego cyklu komunikacji dla udanej operacji compare-and-swap ogranicza częstotliwość publikowania nowego manifestu repozytorium. Magazyn blobów o niskich opóźnieniach przyspiesza ten krok. Przetwarzanie wsadowe pozwala jednemu manifestowi aktualizować kilka referencji naraz, kosztem kolejkowania aktualizacji i większych opóźnień.

Magazyn blobów może odbierać pliki paczek równolegle, ale aktualizacje wspólnego manifestu nadal ze sobą konkurują. Samo zwiększenie zasobów magazynu blobów nie eliminuje tego wąskiego gardła. Każda replika nadal musi odczytać i zindeksować paczki z magazynu blobów, zanim będzie mogła udostępniać je lokalnie. Jeśli nowe zapisy napływają szybciej, niż repliki wykonują tę pracę, powstają zaległości, a klienci dłużej czekają na możliwość pobrania nowo wysłanych commitów.

Architektura Pando

To prowadzi nas do Pando. Pando to nowa architektura udostępniania repozytoriów Git, zaprojektowana do obsługi wielu klientów wykonujących jednoczesne operacje pushowania. Oto jak działa:

Nowe spojrzenie na ograniczenia

Zanim opracowaliśmy Pando, przeczuwaliśmy, że możemy spełnić nasze wymagania bez rezygnowania z Gita. Musieliśmy jednak pójść na kompromis w kwestii niektórych założeń, które większość systemów opartych na Gicie traktuje jako wymagania:

  • Spójność w całym repozytorium: Zapewnienie spójnej migawki wszystkich referencji z tego samego momentu wymaga koordynacji w całym repozytorium. W systemie, który ustala wspólną kolejność wszystkich aktualizacji referencji, operacje pushowania do niepowiązanych gałęzi rywalizują o dostęp do jednego punktu koordynacji.

  • Osobne pliki paczek dla każdego klienta: Jeśli dwóch klientów z różnymi lokalnymi historiami pobiera tę samą gałąź, serwer może potrzebować wygenerować inną paczkę dla każdego z nich, aby nie wysyłać obiektów, które już mają. Tworzenie tych odpowiedzi obciąża procesor i wymaga operacji wejścia/wyjścia na dysku, co utrudnia skalowanie do dużej liczby klientów odczytujących dane.

Pando stosuje zamiast tego mniej rygorystyczny model spójności (spójność przyczynową), aby skalować zapisy, i ponownie wykorzystuje pliki paczek, aby skalować odczyty.

Spójność przyczynowa (CC)

Gdy klient wykonuje pushowanie do gałęzi, Pando zapisuje przychodzący plik paczki jako niezmienny blob w magazynie blobów. Za każdym razem, gdy zmienia się końcówka gałęzi, Pando zapisuje w magazynie metadanych wersję referencji zawierającą nową końcówkę, paczkę, z której ona pochodzi, oraz wszelkie wcześniejsze wersje referencji, od których zależy ta paczka. Zarówno magazyn blobów, jak i magazyn metadanych można skalować poziomo.

Pobranie commita lub referencji z Pando wymaga odtworzenia u klienta zestawu wersji referencji (plików paczek), tak aby zrekonstruować stan lokalnego repozytorium klienta, który utworzył pobieraną wersję referencji. Zapewnia to słabszą formę spójności w całym repozytorium, a jednocześnie szeregowalność aktualizacji poszczególnych referencji. Konflikty między klientami występują tylko wtedy, gdy jednocześnie wykonują operacje pushowania do tej samej referencji. Klient odczytujący kilka gałęzi nie może zakładać, że odczyty te opisują stan repozytorium z jednego momentu.

Po lewej trzy kolejne paczki gałęzi głównej zawierają commity A, B i C; D i E; oraz F. Po prawej gałąź główna i tematyczna rozchodzą się w punkcie C. Gałąź główna przechodzi do G, a tematyczna rozwija się przez D, E i F. Najnowsza paczka gałęzi tematycznej zawiera D, E i F. Przerywany obrys oznacza D i E jako obecne również we wcześniejszej paczce gałęzi tematycznej. Niebieska strzałka oznacza zależność nowej paczki od gałęzi głównej w punkcie G. Rodzicem F w Gicie jest E.

Operacja pushowania do gałęzi głównej dodaje paczkę przyrostową zawierającą co najmniej jeden commit. Każde pushowanie do gałęzi pobocznej ponownie uwzględnia jej wcześniejsze commity, dzięki czemu klient mający ogłoszoną wersję gałęzi głównej może pobrać wszystkie zmiany z gałęzi pobocznej w jednej paczce.

Jest to forma spójności przyczynowej. Aby pobrać referencję, klient odczytujący odtwarza historię klienta, który ją zapisał. Podczas pushowania Pando kontroluje, jaką wspólną historię uzgadniają klient i serwer, dzięki czemu klienci tworzą pliki paczek łatwiejsze do współdzielenia.

W konsekwencji Pando nie zapewnia stanu wszystkich referencji z jednego momentu podczas operacji Gita ani wspólnej kolejności aktualizacji referencji w ramach klonowania lub pobierania. Załóżmy na przykład, że gałęzie foo i bar są aktualizowane podczas negocjacji pełnego klonowania. Podczas negocjacji jeden klient może widzieć foo w wersji 1 i bar w wersji 2, a drugi — foo w wersji 2 i bar w wersji 1. W praktyce użytkownicy i agenty nie polegają na globalnej kolejności aktualizacji referencji, lecz na kolejności aktualizacji pojedynczej referencji.

Porównanie podejść

W tej symulacji trzech modeli spójności konfigurujemy 1000 klientów tak, aby każdy wybrał inną gałąź, a następnie wielokrotnie pushował unikatowe commity. Mierzymy całkowity czas trwania symulacji oraz opóźnienie od operacji pushowania do możliwości pobrania zmian. Ten drugi wskaźnik jest ważny w praktycznych zastosowaniach, ponieważ mierzy czas pełnego cyklu komunikacji podczas współpracy użytkowników i agentów lub korzystania z innych systemów opartych na kontroli wersji. Symulacja uwzględnia maszyny wirtualne i magazyny blobów o parametrach od standardowych po najwyższej klasy. W tej symulacji złagodziliśmy wymagania dotyczące spójności całego repozytorium dla zatwierdzania trójfazowego.

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.

Uruchom symulację porównującą Pando z dwoma innymi podejściami. Wybierz infrastrukturę — od standardowego sprzętu po najwyższej klasy — liczbę klientów i inne ustawienia, aby porównać wyniki.

Modelowany system zatwierdzania trójfazowego wykazuje znacznie większe opóźnienia zapisu pod obciążeniem, gdy dyski osiągają granice wydajności. Z tego samego powodu systemy oparte na dzienniku zapisu wyprzedzającego wykazują znacznie większe opóźnienia odczytu — serwery nie nadążają z odczytywaniem dziennika. Zarówno w testach na naszym rzeczywistym monorepo, jak i w tej symulacji Pando obsługuje wiele tysięcy klientów wykonujących równoległe operacje pushowania bez spadku wydajności pod obciążeniem.

Podczas obsługi operacji pushowania Pando weryfikuje przychodzący plik paczki, przesyłając go strumieniowo do magazynu blobów, bez rozpakowywania do lokalnego repozytorium. Gdy klient rozpoczyna pobieranie, Pando wybiera potrzebne mu zapisane paczki, a klient je pobiera. Żaden serwer nie musi stosować plików paczek w lokalnym repozytorium ani wykonywać kosztownych operacji rozpakowywania i pakowania. Ponieważ serwery Pando zajmują się głównie negocjacją w protokole Git i wyborem plików paczek, mogą działać na standardowym sprzęcie.

Pozwala to skalować Pando tak, aby w pełni wykorzystywać wydajność klientów, sieci łączącej ich z Pando oraz usługi przechowywania blobów. Przy wystarczających zasobach każdy klient pracujący na niezależnej gałęzi może przesyłać paczki tak szybko, jak pozwala na to jego łącze. Aby obsługiwać więcej klientów z taką szybkością, możemy dodawać serwery Pando i równolegle skalować magazyny metadanych i blobów.

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

Agent edytuje pliki, tworzy lokalnie commit i uruchamia git push, co tworzy plik paczki z tymi zmianami w celu aktualizacji gałęzi. Pando odbiera paczkę i przesyła ją strumieniowo do magazynu blobów. Po zweryfikowaniu przychodzącej operacji pushowania przez jednokrotne zdekodowanie danych Pando używa transakcji obejmującej wiele kluczy, aby zapisać wersję referencji w naszym magazynie metadanych i atomowo zaktualizować wskaźnik referencji. Dzięki temu Pando zapewnia spójność typu „odczyt własnego zapisu”. Cała ta operacja zapisu wymaga O(1)O(1) odczytów i zapisów metadanych.

Gdy inny klient pobiera gałąź, Pando wybiera zestaw zapisanych paczek na podstawie historii, którą klient już ma. Ta operacja wymaga O(log⁡Δ)O(\log \Delta) odczytów metadanych, gdzie Δ\Delta to najkrótsza odległość, liczona w wersjach referencji, między tym, co klient ma, a tym, czego żąda. Serwer zwraca adresy URI plików paczek, a klient pobiera je za pośrednictwem usługi Express, która przechowuje je w regionalnych pamięciach podręcznych.

Ten projekt opiera się na trzech głównych usługach architektury Pando: obsłudze pushowania, pobierania i kompaktowania.

Skalowanie operacji pushowania

Gdy klient wykonuje pushowanie, tradycyjny serwer Git zachowuje się jak pokerzysta odkrywający wszystkie karty: ujawnia klientowi każdą posiadaną gałąź. Klient zbiera następnie commity, drzewa i bloby, aby utworzyć plik paczki, który pomija obiekty, o których wie, że serwer już je ma.

Ogłaszanie większej liczby referencji może zmniejszyć ilość danych wysyłanych w operacji pushowania. Może też utrudnić ponowne wykorzystanie paczki, ponieważ innemu klientowi może brakować obiektów pominiętych przez klienta zapisującego.

Pando stosuje inne podejście. Zamiast odkrywać wszystkie karty, ogłasza klientowi tylko wybrany zestaw gałęzi. Robi to z dwóch powodów:

  1. Zapewnienie spójnego widoku wszystkich referencji jest kosztowne. Przy milionach gałęzi same ogłoszenia referencji mogą przekraczać 100 MiB. Wymaga to od każdego serwera utrzymywania aktualnego mapowania, co wiąże się ze znacznym narzutem koordynacji, albo wykonywania zapytania do bazy danych, którego koszt rośnie wraz z liczbą referencji.

  2. Gdy klient otrzymuje tylko podzbiór referencji, generuje paczkę zależną wyłącznie od tego, co serwer jawnie pozwala mu pominąć. Śledzenie zależności pozwala Pando ponownie wykorzystać plik paczki wygenerowany przez klienta w jednej operacji pushowania jako odpowiedź na późniejsze żądanie pobrania.

Im więcej referencji ogłasza serwer Git, tym więcej źródeł mogą wykorzystać klienci wykonujący pushowanie do wykluczania obiektów z paczki, co pozwala im tworzyć mniejsze paczki. Ponowne wykorzystanie tych plików paczek wymaga jednak, aby serwer mógł potwierdzić, że pobierający klienci mają wszystkie zależności. W przeciwnym razie plik paczki może pomijać obiekty potrzebne klientowi.

Z drugiej strony, gdyby serwer Git nie ogłaszał żadnych referencji, klient generowałby plik paczki zawierający pełną historię repozytorium. Taki plik mógłby wykorzystać każdy inny klient, ale kosztem tego, że pobranie dowolnej gałęzi w praktyce stawałoby się pełnym klonowaniem.

Krzywa pokazująca wpływ ogłaszanych referencji na rozmiar pliku paczki: od pełnej historii, gdy nie ogłasza się żadnej referencji, przez ogłaszanie gałęzi głównej, po same nowe obiekty przy ogłaszaniu wszystkich referencji.

Ogłaszanie większej liczby referencji skutkuje mniejszymi plikami paczek. Większość redukcji rozmiaru można uzyskać, ogłaszając tylko jedną.

Domyślnie, gdy klient pushuje nową gałąź funkcjonalną, Pando ogłasza jako bazę tylko wyznaczoną gałąź główną. Dzięki temu rozmiar ogłoszenia nie rośnie wraz z liczbą gałęzi w repozytorium, ale wymaga to kompromisów. Gdy inżynier wielokrotnie pushuje zmiany do jednej gałęzi lub tworzy gałąź funkcjonalną na bazie innej takiej gałęzi, klient musi ponownie wysłać wszystko, czego nie ma w gałęzi głównej.

W rezultacie kolejne operacje pushowania do gałęzi przesyłają więcej danych, zawierają potencjalnie nadmiarowe obiekty i zużywają dodatkową przepustowość łącza klienta. W naszym monorepo mediana rozmiaru pliku paczki utworzonego w ten sposób wynosiła poniżej 20 KiB, a 99. percentyl — poniżej 3 MiB. Dla nas był to rozsądny kompromis, ponieważ klient mający już ogłoszoną historię gałęzi głównej może pobrać gałąź za pomocą tego jednego pliku paczki.

Po wybraniu zestawu referencji do ogłoszenia klientowi Pando przyjmuje pushowanie i aktualizuje magazyn metadanych. Pando dzieli aktualizację metadanych na część synchroniczną i asynchroniczną. Synchroniczna aktualizacja metadanych zapewnia klientom możliwość natychmiastowego pobrania nowo zaktualizowanego commita. Zapisuje ona nową wersję referencji i aktualizuje mapowanie wskaźnika referencji na tę wersję, przechowując nową końcówkę, zależności użyte podczas negocjowania pliku paczki oraz łącze do przesłanego pliku paczki.

Jednocześnie w tej samej transakcji zapisywane są zadania asynchroniczne, takie jak zaplanowanie kompaktowania. W przypadku bardzo dużych operacji pushowania synchronicznie zapisywany jest tylko podzbiór commitów. Dzięki temu potrzebujemy jedynie stałej liczby odczytów i zapisów w magazynie metadanych, niezależnie od rozmiaru pushowania.

Co ważne, serwery Pando mogą niezależnie przyjmować operacje pushowania do niepowiązanych referencji. Jedynym kluczem, o który może wystąpić rywalizacja, jest wskaźnik referencji. Chronimy go operacją atomową podobną do compare-and-swap. Możemy zwiększać przepustowość zapisu, dodając serwery Pando, jednostki skalowania magazynu metadanych lub magazyny blobów.

Pobieranie i klonowanie z pełną szybkością łącza

Gdy klient wykonuje operację fetchowania, przekazuje serwerowi dwa zbiory identyfikatorów commitów, określane w protokole jako have i want. Tradycyjny serwer Git wyznacza różnicę między zbiorem obiektów osiągalnych z żądanych commitów a zbiorem obiektów osiągalnych z commitów posiadanych, kompresuje wynik i wysyła go klientowi. Choć serwer Git o otwartym kodzie źródłowym ma różne lokalne indeksy przyspieszające tę operację, generowanie tych plików paczek bywa kosztowne i na ogół trudno wykorzystać tu pamięć podręczną.

Zamiast tego Pando używa mapowań commitów zapisanych podczas operacji pushowania, aby wyszukać wpisy have i want klienta i przyporządkować je do zestawu wersji referencji zawierających te commity. Każda z tych wersji referencji wskazuje plik paczki odebrany wcześniej przez Pando. Serwer wykorzystuje te informacje, aby ustalić potrzeby klienta i wybrać pliki paczek obejmujące wszystkie potrzebne mu obiekty.

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.

Pando mapuje informacje protokołu Git o posiadanych i żądanych commitach na sekwencję wersji referencji. Żądanie gałęzi pobocznej obejmuje również jej zależności w gałęzi głównej.

Protokół Git pozwala wysłać tylko jeden plik paczki w odpowiedzi HTTP na klonowanie lub pobieranie. Jednak niedoceniana funkcja protokołu Git, packfile-uri, pozwala serwerowi zwrócić łącza do osobnego pobrania dodatkowych paczek. Pando korzysta wyłącznie z tego mechanizmu, a bezpośrednio w odpowiedzi przesyła pusty plik paczki. Aby wesprzeć tę funkcję, OpenAI wniosło szereg ulepszeń packfile-uri do głównego projektu Git.

Systemy OpenAI działają w wielu regionach geograficznych, dlatego w każdym z nich utrzymujemy klastry Pando z lokalnymi pamięciami podręcznymi. Usługa dostarczania paczek Express buforuje dane z magazynu blobów, a osobna usługa buforuje dane z magazynu metadanych. Te pamięci podręczne pozwalają Pando obsługiwać żądania dotyczące identyfikatora commita lub specjalnego odczytu referencji w całości na podstawie stanu buforowanego w danym klastrze. Używamy haszowania spójnego do kierowania żądań plików paczek do serwerów Express, osiągając odsetek trafień w pamięć podręczną powyżej 99%.

Ten system pamięci podręcznych zapewnia niezwykle wysoką przepustowość, wystarczającą do pełnego wykorzystania łączy między klientami a Pando. Potwierdziliśmy, że dzięki tym adresom URI plików paczek i lokalnym pamięciom podręcznym operacje klonowania i pobierania w pełni wykorzystują łącza 10 Gb/s, nawet przy użyciu wyłącznie standardowych maszyn wirtualnych dostawcy chmury.

Kompaktowanie i czas do pierwszego bajtu

W większości serwerów Git ponowne pakowanie i usuwanie nieużywanych obiektów są niezbędne do utrzymania wydajności rosnącego repozytorium. Obie operacje odzyskują miejsce, ale intensywnie wykorzystują zasoby obliczeniowe i operacje wejścia/wyjścia.

W szybko zmieniającym się repozytorium, takim jak monorepo OpenAI, wysyłanie osobnego adresu URI pliku paczki dla każdej operacji pushowania od ostatniego pobrania jest niepraktyczne. Każdego dnia klient musiałby pobierać dziesiątki tysięcy plików paczek, aby zaktualizować gałąź główną.

Zamiast tego Pando łączy paczki w tle, aby zmniejszyć ten narzut, stosując rozwiązanie podobne do kompaktowania w drzewach LSM. Każde pushowanie tworzy plik paczki określany w Pando jako „L0”, reprezentujący najmniejszą jednostkę danych, jaką Pando może przesłać. Z czasem procesy działające w tle zbierają te pliki paczek w grupy pokrywające sekwencje wersji referencji, których rozmiar rośnie geometrycznie od poziomu L0. Obecnie nasze monorepo używa takich grup plików paczek aż do poziomu L4.

Grupy te pełnią podwójną funkcję. Gdy grupa jest wystarczająco zapełniona, proces kompaktowania przejmuje ją i tworzy pojedynczy plik paczki. Poprawia to stopień kompresji dzięki reorganizacji obiektów wewnątrz pliku paczki. Przyspiesza też negocjację pobierania, ponieważ grupy działają jak lista z przeskokami obejmująca wersje referencji. Przeszukując równocześnie indeksy na każdym poziomie, Pando może w czasie logarytmicznym wybrać zestaw paczek do wysłania klientowi. Te grupy plików paczek, niezależnie od tego, czy zostały ponownie spakowane, umożliwiają wydajniejsze planowanie pobierania, łagodząc wymagania dotyczące zarówno przepustowości, jak i opóźnień w Pando.

Compaction in Pando

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

Compaction in progressPublished cover

Kompaktowanie łączy w tle pliki paczek z wielu wersji referencji, umożliwiając wydajniejsze pobieranie mniejszej liczby plików.

W odróżnieniu od wielu serwerów Git pliki paczek są zachowywane na każdym poziomie — nie są odrzucane ani usuwane podczas porządkowania. Paczki ze wszystkich poziomów są stale używane do obsługi klientów na podstawie wpisów have i want przekazywanych w żądaniu pobrania. W przypadku klientów korzystających z Pando jako API odczyt dowolnego obiektu z pliku paczki zwykle wymaga odczytania mniejszej ilości danych z L0 niż z L4, ponieważ obiekt rzadziej jest skompresowany.

Kompaktowanie w Pando znacznie zmniejsza całkowitą ilość danych wysyłanych klientom, a koszt planowania paczek rośnie wolniej niż liniowo wraz z zaległością klienta. Jest to szczególnie ważne dla dużych, szybko zmieniających się repozytoriów, które naszym zdaniem staną się nową normą w tworzeniu oprogramowania.

Tworzenie Pando

Zbudowaliśmy Pando z pomocą Codex — od rozpoczęcia projektu do wdrożenia produkcyjnego minęło zaledwie 10 tygodni. Rdzeń Pando zaprojektował jeden inżynier, zaimplementowały go zespoły agentów, a wdrożyła niewielka grupa inżynierów. Pando zaczęło obsługiwać ruch produkcyjny w kwietniu tego roku i jest częścią większego zestawu narzędzi do zarządzania kontrolą wersji używanych w OpenAI.

Weryfikując projekt Pando, zastosowaliśmy wiele różnych podejść. Zastosowaliśmy nasze zasady budowania bezpieczniejszych fundamentów oprogramowania⁠. Ponieważ tworzyliśmy Pando od podstaw, zdecydowaliśmy się zaimplementować je w języku Rust, bez zależności od starszych rozwiązań. Napisaliśmy zestaw testów zgodności, aby agenty mogły sprawdzać swoje zmiany pod kątem standardowego zachowania Gita. Opracowaliśmy również otoczenia operacyjne do deterministycznych testów symulacyjnych, które pozwalają sprawdzać niezmienniki i właściwości systemu. Dzięki temu mogliśmy wykrywać i ograniczać subtelne wyścigi oraz regresje w negocjacji i planowaniu paczek.

W tworzeniu tego systemu uczestniczyło wiele modeli. Gdy pojawiały się nowe, wykorzystywaliśmy je do poprawy wydajności i niezawodności Pando. Korzystając z GPT‑6 Astra, agenty pisały kod w Lean i TLA+, aby sformalizować „teorię Pando”, która posłużyła nam do wykrywania i naprawiania błędów implementacji oraz dowodzenia właściwości systemu. Wiele nieprzerwanych sesji trwało setki godzin, podczas których wykrywano i naprawiano przypadki brzegowe w Pando. Przez całą dobę uruchamiamy też agenty prowadzące testy bezpieczeństwa Gita i Pando metodą red teamingu z użyciem modeli obsługujących Daybreak.

Wdrażanie Pando

Początkowo wdrożyliśmy Pando jako warstwę proxy przekazującą odczyty i zapisy do istniejącego systemu kontroli wersji. Pozwoliło nam to utrzymywać synchronizację Pando z poprzednim systemem, dzięki czemu mogliśmy bezpiecznie przenosić ruch do Pando bez przestojów.

W ramach mechanizmu pamięci podręcznej Pando potrafi też usuwać duplikaty żądań i łączyć je przed przekazaniem dalej. Dzięki temu nasze dotychczasowe narzędzia działały sprawnie, gdy kolejno przenosiliśmy systemy do Pando. W miarę przenoszenia kolejnych zadań do Pando ich operacje pushowania zapełniały pamięć podręczną Pando i poprawiały niezawodność poprzedniego backendu.

Aby zoptymalizować wydajność korzystania z Pando, zaczęliśmy dostarczać naszą publiczną dystrybucję Gita na komputery programistów i w aplikacji komputerowej ChatGPT. Do etapowej migracji klientów do Pando wykorzystaliśmy wewnętrzną nakładkę na interfejs wiersza poleceń Gita. Stworzyliśmy też wiele innych narzędzi do obsługi zgłoszeń zmian i przeglądów kodu oraz interfejsy API do automatyzacji. Wraz z ruchem Gita przenieśliśmy setki integracji zależnych od starszych interfejsów API, webhooków i pobierania repozytoriów.

Sam Codex pomagał programistom i zespołom w migracji. Stworzyliśmy umiejętności służące do przygotowywania istniejących kopii roboczych i instruowania agentów, jak korzystać z nowego systemu. W rezultacie programiści mogli po prostu poprosić Codex o migrację swoich środowisk programistycznych i usług.

Przejście na Pando poprawiło niezawodność scaleń i zmniejszyło medianę opóźnienia operacji pushowania o niemal 80%.

Pando może dostarczać petabajt danych Gita dziennie, korzystając z zaledwie kilku rdzeni procesora. W ciągu jednej godziny Pando dostarczyło ponad 57 terabajtów paczek Gita, przy czym ponad 99% tych bajtów pochodziło z lokalnej dyskowej pamięci podręcznej.

Ponowne wykorzystanie paczek pozwala wielu klientom pobierać tę samą historię repozytorium przy niewielkim nakładzie obliczeń na obsługujących ich węzłach. Ten przykład dotyczy jednego repozytorium, ale Pando zaprojektowano z myślą o obsłudze znacznie większej ich liczby. W miarę wzrostu zapotrzebowania możemy niezależnie skalować dostarczanie danych i negocjację Gita.

Wkład w projekt Git

Oparcie Pando na protokole i kliencie Git pomogło nam wykryć błędy oraz możliwości poprawy wydajności samego Gita.

Na przykład Pando udostępnia klientom bezpośrednio paczki wielokrotnego użytku, więc ich pobieranie i indeksowanie stało się kluczowe dla szybkości i niezawodności systemu. Prace te ujawniły przypadki brzegowe związane z równoległym pobieraniem, sprzątaniem po przerwanych transferach oraz wznawianiem pobrań, które już się zakończyły. Naprawiliśmy te ścieżki kodu, dodaliśmy weryfikację przy akceptowaniu istniejącej paczki do pobrania i przekazaliśmy te zmiany do głównego projektu.

Musieliśmy też wiedzieć, na co zużywane są czas i przepustowość, dlatego dodaliśmy śledzenie pobrań packfile-uri oraz liczniki bajtów przy generowaniu paczek. Ułatwia to ustalenie, czy powolne pobieranie wynika z czasu generowania paczek, czy ich przesyłania.

W wersji Gita od OpenAI⁠(otwiera nowe okno) klient może równolegle pobierać i indeksować niezależne paczki. Dodaliśmy pojęcie „jawnych informacji o posiadanych commitach”, aby serwer mógł ogłaszać aktualną końcówkę gałęzi oddzielnie od historii, którą klient wykonujący pushowanie może pominąć w swojej paczce. W Pando pozwala to klientom używać --force-with-lease, aby sprawdzić, czy końcówka gałęzi funkcjonalnej się nie zmieniła, i tworzyć paczkę wyłącznie względem gałęzi głównej. Każdy, kto ma tę historię gałęzi głównej, może następnie pobrać gałąź przy użyciu tej samej paczki.

Kilka z tych ulepszeń jest już w głównym projekcie Git, a wiele innych — w naszym publicznym repozytorium. Nadal współpracujemy ze społecznością Gita, aby te ulepszenia były przydatne również poza Pando. Postępy tych prac można śledzić w naszym publicznym rejestrze poprawek⁠(otwiera nowe okno).

Co dalej

Dzięki odejściu od utartych założeń dotyczących skalowania repozytoriów Git Pando pozwala nam skalować monorepo bez zmiany sposobu pracy z Gitem, z którego już korzystają nasi inżynierowie i agenty. W naszym monorepo Pando umożliwiło większą równoległość pracy, zwiększyło niezawodność scaleń i skróciło czas publikowania zmian w kodzie. Poza monorepo badamy też inne zastosowania Pando w OpenAI, w tym zadania, w których duża liczba agentów musi współpracować we wspólnym repozytorium Git.

W miarę jak inżynierowie i agenty tworzą coraz więcej oprogramowania, kolejne zespoły spoza OpenAI będą napotykać ograniczenia kontroli wersji, na które my już natrafiliśmy. Obecnie szukamy sposobów udostępnienia naszym użytkownikom technologii, na której opiera się Pando.

Pando czerpie z dwóch dekad doświadczeń użytkowników i administratorów repozytoriów Git. Będziemy dalej rozwijać Pando i współpracować ze społecznością open source Gita, dzieląc się ulepszeniami przydatnymi dla szerszego grona użytkowników.

Autorzy

OpenAI