OpenAI

2. November 2026

Ingenieurwesen

Git bei OpenAI skalieren

Eine Versionsverwaltung für Arbeit im Maßstab von Agenten entwickeln und dabei in das Git-Ökosystem investieren.

Von Aaron Friel, Taylor Blau und Ted Nyman, Members of Technical Staff

Laden …

Im vergangenen Jahr sind die Größe unseres Monorepos und die Anforderungen daran rasant gewachsen. Es war klar, dass wir unsere Versionsverwaltung erneuern mussten, um sie auf das Maß an paralleler Arbeit zu skalieren, das Codex ermöglicht.

Wir haben Pando entwickelt, ein neues Backend für Git-Repositories, ohne die Entwicklungsarbeit zu unterbrechen oder die Git-Nutzung unseres Teams zu verändern. Pando unterstützt heute die gesamte Entwicklung im Monorepo von OpenAI. Pando verfolgt zwei anspruchsvolle Ziele: erstens horizontal skalierbare Schreibzugriffe, optimiert für gleichzeitige Pushes auf viele Branches. Und zweitens maximalen Lesedurchsatz, damit Clients Daten schnell genug herunterladen können, um ihre verfügbare Netzwerkbandbreite vollständig auszuschöpfen.

Wir haben diese Ziele erreicht und dabei die Kompatibilität mit dem Git-Protokoll und dem Git-Client erhalten. So konnten unsere Entwicklungsteams und Agenten Pando nahtlos in ihre bestehenden Workflows integrieren. Im Produktivbetrieb senkte Pando die mediane Push-Latenz auf fast ein Fünftel. Inzwischen liefert es fast ein Petabyte Git-Daten pro Tag aus.

Hier erläutern wir, warum wir eine neue Versionsverwaltung entwickelt und uns entschieden haben, die Git-Kompatibilität beizubehalten. Wir vergleichen unser System mit gängigen Ansätzen zur Skalierung von Git und erklären, warum Pando die Skalierungsanforderungen von OpenAI auf einzigartige Weise erfüllen kann. Außerdem geben wir einen Ausblick und erläutern, wie wir weiter zu Git beitragen werden, um Arbeit im Maßstab von Agenten zu unterstützen.

Warum wir eine neue Versionsverwaltung brauchten

Codex hat Umfang und Tempo der Entwicklung bei OpenAI verändert. Unsere Entwicklungsteams können damit deutlich mehr Arbeit parallel erledigen. Das bedeutet mehr Branches und Pushes, mehr Merges sowie mehr Fetch- und Clone-Vorgänge aus Entwicklungsumgebungen, Forschungsjobs und der CI.

Diese Anforderungen verstärken sich gegenseitig. Eine einzelne Person im Entwicklungsteam hat heute mehrere Änderungen gleichzeitig für die CI bereit. Jeder Push kann mehrere Jobs auslösen, die denselben Commit abrufen.

Etwa seit Ende letzten Jahres konnten die Systeme hinter unserem Monorepo kaum noch Schritt halten. Fetches und Pushes waren langsam, Merges schlugen fehl, und die Zuverlässigkeit sank zeitweise unter 90 %. Waren wir an eine Grenze von Git selbst gestoßen oder an eine Grenze der Art, wie wir unser Monorepo bereitstellten? Git entstand aus einer Community, die Kernel-Patches per E-Mail austauschte. Seit mehr als zwei Jahrzehnten passen Entwicklungsteams ihre Werkzeuge rund um Git an die veränderte Softwareentwicklung an. Als es in unserem eigenen Monorepo zu Problemen kam, war es uns wichtig, weiter auf dieser Arbeit aufbauen zu können.

Wir sahen eine Möglichkeit, den vertrauten Workflow beizubehalten und zugleich das Backend neu aufzubauen: Es sollte Pushes auf verschiedene Branches parallel verarbeiten und dieselbe Historie gleichzeitig an viele lesende Clients ausliefern können. Indem wir Git als Grundlage unseres Versionsverwaltungs-Workflows beibehielten, konnten Verbesserungen am Client auch außerhalb von OpenAI nützlich sein. Wir wollten diese Verbesserungen aus unserer Kopie von Git⁠(wird in einem neuen Fenster geöffnet) in das Upstream-Projekt einbringen und mit der breiteren Community teilen.

Git für große Lasten entwickeln

Bei der Skalierung einer Versionsverwaltung sind zwei Dimensionen entscheidend: viele schreibende Clients, die auf verschiedene Branches pushen, und viele lesende Clients, die dieselbe Historie abrufen. Pando ist für beides optimiert.

Erstens koordiniert Pando die Aktualisierungen jedes Branches unabhängig. Aktualisierungen unabhängiger Branches können parallel erfolgen. Ein lesender Client erhält beim Abrufen eines Branches die für diese Revision benötigte Historie. Der Kompromiss: Ein Client, der mehrere Branches liest, kann deren Zustände zu unterschiedlichen Zeitpunkten sehen.

Zweitens verwendet Pando Packdateien bei mehreren Fetches wieder, statt für jeden Client eine eigene zu erzeugen. Packdateien sind das Git-Format, mit dem mehrere Objekte gemeinsam komprimiert werden. Ein Client lädt dabei möglicherweise einige bereits vorhandene Objekte erneut herunter. Dafür kann der Server dieselben Bytes mit geringem Rechenaufwand an viele Clients ausliefern.

Diese Entscheidungen ermöglichen es uns, Schreibzugriffe über Branches hinweg zu skalieren und Lesezugriffe aus Caches zu bedienen. Um das Zusammenspiel zu verstehen, sehen wir uns zunächst an, wie Git die Historie speichert und überträgt.

Grundlagen der Objektspeicherung in Git

Git muss zwei Bestandteile eines Repositorys synchron halten können: Objekte und Referenzen.

Ein Git-Repository speichert Momentaufnahmen von Dateien und die verbindende Historie als Graphen aus Objekten. Ein Objekt kann ein Blob, ein Tree, ein Commit oder ein Tag sein. Blobs enthalten den Inhalt einer einzelnen Datei. Trees ordnen Namen Blobs oder anderen Trees zu und bilden so eine Verzeichnisstruktur. Commits verweisen auf einen Root-Tree und null oder mehr Eltern-Commits. So entsteht die Historie deines Repositorys.

Objekte werden durch einen Hash ihres Typs und Inhalts identifiziert. Objekte sind außerdem unveränderlich: Das Erfassen einer geänderten Datei erzeugt einen neuen Blob. Unveränderte Dateien und Verzeichnisse können bestehende Objekte wiederverwenden.

Branches und Tags sind Referenzen, kurz „Refs“, die auf Commits verweisen. Ein Commit-Vorgang auf einem Branch erzeugt einen neuen Commit und aktualisiert die Ref des Branches so, dass sie darauf verweist.

Der Branch refs/heads/main verweist auf Commit C2, dessen Eltern-Commit C1 ist. C1 verweist auf Root-Tree T1, in dem parser.py auf Blob B1 verweist. C2 verweist auf Root-Tree T2, in dem parser.py auf Blob B2 verweist. In beiden Root-Trees verweist docs/ auf denselben Tree T3. Dessen Eintrag README.md verweist auf Blob B3, usage.md auf Blob B4.

Git-Objekte verweisen anhand ihrer IDs aufeinander: Commits auf Trees und Trees auf andere Trees und Blobs. Der Branch refs/heads/main verweist auf Commit C2, dessen Eltern-Commit C1 ist. C1 verweist auf Root-Tree T1, in dem parser.py auf Blob B1 verweist. C2 verweist auf Root-Tree T2, in dem parser.py auf Blob B2 verweist. In beiden Root-Trees verweist docs/ auf denselben Tree T3. Dessen Eintrag README.md verweist auf Blob B3, usage.md auf Blob B4.

Git speichert Objekte entweder „lose“ oder „gepackt“. Lose Objekte werden in einzelnen Dateien gespeichert, die anhand ihrer Objekt-ID identifiziert werden. Gepackte Objekte werden gemeinsam in einer Packdatei gespeichert. Dadurch lassen sich ähnliche Objekte als Deltas zueinander ablegen.

Unter $GIT_DIR/objects sind zwei Versionen von parser.py dargestellt. Als lose Objekte enthält Blob A parse(), während Blob B sowohl parse() als auch parse_file() enthält. Die Packdatei speichert A vollständig und stellt B als Anweisungen dar, ganz A zu kopieren und parse_file() anzuhängen. Die zugehörige *.idx-Datei lokalisiert beide Blobs anhand ihrer Objekt-ID in der Packdatei.

Git kann Blobs als separate Dateien speichern oder gemeinsam packen und dabei einen Blob als Delta gegenüber einem anderen kodieren.

In Git ist ein Objekt von einem Commit aus erreichbar, wenn Git es findet, indem es den Verweisen dieses Commits durch dessen Eltern-Commits, Trees und Blobs folgt. Bei einem Fetch teilt der Client dem Server mit, welche Commits er benötigt und welche er bereits hat. Der Server ermittelt die von den gewünschten Commits aus erreichbaren Objekte, schließt die bekanntermaßen bereits beim Client vorhandenen aus und sendet den Rest als Packdatei. Das Format entspricht dem, das Git auf dem Datenträger verwendet. Ein vollständiger Klon beginnt ohne Objekte und benötigt daher alle Objekte, die von der zu klonenden Historie aus erreichbar sind.

Ein Push überträgt Objekte in die Gegenrichtung und fordert den Server auf, eine oder mehrere Refs zu aktualisieren. Der Client nutzt die vom Server bekannt gegebenen Refs, um bereits auf dem Server vorhandene Objekte nicht erneut zu senden. Das gesendete Pack kann daher von vorhandenen Objekten abhängen: Das Pack allein reicht nicht aus, um die neue Historie zu rekonstruieren.

Ein Git-Repository zu replizieren bedeutet, sowohl Objekte als auch Referenzen auf mehreren Servern verfügbar zu halten. Bevor ein Replikat den neuen Wert einer Referenz bereitstellen kann, muss es die von diesem Wert aus erreichbaren Objekte ausliefern können. Der Dienst muss außerdem konkurrierende Referenzaktualisierungen koordinieren, also Pushes, die denselben Branch von derselben bisherigen Spitze auf unterschiedliche neue Commits setzen wollen. Systeme, die alle Schreibzugriffe auf ein Repository in eine einzige Reihenfolge zwingen, können dazu führen, dass Pushes auf unabhängige Branches aufeinander warten müssen.

Bestehende Architekturmuster verteilter Git-Dienste

Beim Entwurf von Pando bewerteten wir herkömmliche Ansätze zur Skalierung der Versionsverwaltung mit Git: Einer koordiniert Ref-Aktualisierungen über Repository-Replikate hinweg mit Drei-Phasen-Commit (3PC, einem Protokoll zur Koordination eines Commits über mehrere Knoten). Ein anderer ordnet Aktualisierungen über ein gemeinsames Write-ahead-Log (WAL, ein vor der Anwendung geschriebenes Änderungsprotokoll) im Blob-Speicher.

Drei-Phasen-Commit (3PC)

Im von uns modellierten Drei-Phasen-Commit-Design hält der Git-Dienst auf mehreren Replikaten eine vollständige Kopie des Repositorys vor. Wenn ein Server einen Push empfängt, wird die eingehende Packdatei kopiert und parallel an alle Replikate gesendet. Um den Push zu veröffentlichen, muss eine Mehrheit der Replikate die Ref-Aktualisierung akzeptieren. Der zuständige Server ist der Koordinator. Dafür sind mehrere Kommunikationsrunden zwischen ihm und jedem anderen Replikat erforderlich.

Um eine Referenzaktualisierung zu akzeptieren, sperrt jedes Replikat seine Kopie der Ref und prüft, ob sie noch auf den vom Client erwarteten Commit verweist. Der Koordinator sendet dann eine Pre-Commit-Nachricht an die Replikate, die zugestimmt haben. Sobald eine Mehrheit die Pre-Commit-Nachricht bestätigt, weist der Koordinator diese Replikate an, die Aktualisierung durchzuführen. Jedes aktuelle Replikat kann anschließend die neue Branch-Spitze aus seiner lokalen Repository-Kopie bekannt geben.

Vollständige Replikate ermöglichen lokale Lesezugriffe und garantieren Clients eine konsistente Sicht auf alle Referenzen, unabhängig davon, welches Replikat sie wählen. Daher werden Referenzen nacheinander aktualisiert. Jeder Push erfordert mehrere Kommunikationsrunden, um die Ref-Aktualisierung zwischen den Replikaten zu koordinieren.

Durch das Warten auf ein Quorum hängen Pushes von der Datenträgerleistung der Replikate und der Kommunikationslatenz ab. Mit zunehmendem Schreibvolumen müssen die Replikate sowohl die eingehenden Packs als auch die Lesezugriffe bewältigen.

Write-ahead-Log (WAL)

Im von uns modellierten WAL-basierten Design nutzt der Git-Dienst ein gemeinsames Manifest im Blob-Speicher, um Aktualisierungen im gesamten Repository zu ordnen. Wenn ein Server einen Push empfängt, lädt er die eingehende Packdatei in einen Blob-Speicher hoch und bereitet die Ref-Aktualisierung in seiner lokalen Repository-Kopie vor. Um den Push zu veröffentlichen, fügt er dem gemeinsamen Manifest im Blob-Speicher einen Eintrag hinzu, der die Ref-Aktualisierung erfasst und auf die Packdatei verweist. Anschließend aktualisiert der Git-Server das Manifest mit einer Compare-and-Swap-Operation. Diese ersetzt das Manifest atomar oder lehnt die Änderung ab. Hat ein anderer schreibender Client es inzwischen aktualisiert, liest der Server die zwischenzeitlichen Einträge, prüft die Gültigkeit seiner Ref-Aktualisierung und versucht es mit der bereits hochgeladenen Packdatei erneut.

Schreibende Clients können ihre Packdateien parallel hochladen. Doch auch Pushes auf unabhängige Branches müssen dasselbe Manifest aktualisieren. Die Umlaufzeit einer erfolgreichen Compare-and-Swap-Operation begrenzt, wie oft das Repository ein neues Manifest veröffentlichen kann. Ein Blob-Speicher mit niedriger Latenz beschleunigt diesen Schritt. Durch Bündelung kann ein Manifest mehrere Refs gleichzeitig aktualisieren. Dafür müssen Aktualisierungen in einer Warteschlange gesammelt werden, was die Latenz erhöht.

Der Blob-Speicher kann Packdateien parallel empfangen. Aktualisierungen des gemeinsamen Manifests konkurrieren jedoch weiterhin miteinander. Mehr Kapazität im Blob-Speicher allein beseitigt diesen Engpass nicht. Jedes Replikat muss die Packs weiterhin aus dem Blob-Speicher lesen und indexieren, bevor es sie lokal ausliefern kann. Treffen Schreibzugriffe schneller ein, als diese Arbeit erledigt wird, geraten die Replikate in Rückstand. Clients müssen dann länger warten, bevor sie neu gepushte Commits abrufen können.

Die Architektur von Pando

Damit kommen wir zu Pando. Pando ist eine neue Architektur zur Bereitstellung von Git-Repositories. Sie soll auf viele gleichzeitig pushende Clients skalieren. So funktioniert es:

Anforderungen neu überdenken

Schon vor der Entwicklung von Pando vermuteten wir, dass wir unsere Anforderungen ohne Abstriche bei Git erfüllen könnten. Allerdings mussten wir bei einigen Punkten Kompromisse eingehen, die die meisten Git-Systeme als Anforderungen voraussetzen:

  • Repository-weite Konsistenz: Eine konsistente Momentaufnahme aller Refs erfordert eine Koordination über das gesamte Repository hinweg. In einem System, das alle Ref-Aktualisierungen in eine gemeinsame Reihenfolge bringt, konkurrieren Pushes auf unabhängige Branches um einen einzigen Koordinationspunkt.

  • Individuelle Packdateien für jeden Client: Wenn zwei Clients mit unterschiedlichen lokalen Historien denselben Branch abrufen, muss der Server möglicherweise für jeden ein anderes Pack erzeugen, um bereits vorhandene Objekte nicht erneut zu senden. Das Erstellen dieser Antworten beansprucht CPU-Leistung und Datenträger-I/O. Das erschwert die Skalierung auf viele lesende Clients.

Pando verwendet stattdessen ein weniger strenges Konsistenzmodell (kausale Konsistenz), um Schreibzugriffe zu skalieren, und nutzt Packdateien mehrfach, um Lesezugriffe zu skalieren.

Kausale Konsistenz (CC)

Wenn ein Client auf einen Branch pusht, speichert Pando die eingehende Packdatei als unveränderlichen Blob in einem Blob-Speicher. Bei jeder Änderung des Branch-Zeigers legt Pando eine Ref-Version im Metadatenspeicher ab. Sie enthält die neue Branch-Spitze, das zugehörige Pack und alle früheren Ref-Versionen, von denen das Pack abhängt. Sowohl der Blob-Speicher als auch der Metadatenspeicher sind horizontal skalierbar.

Um einen Commit oder eine Referenz von Pando abzurufen, wird eine Reihe von Ref-Versionen (Packdateien) beim Client eingespielt. So wird der Zustand des Clients rekonstruiert, der die abgerufene Ref-Version erstellt hat. Das bietet eine schwächere Konsistenz im gesamten Repository, gewährleistet aber Serialisierbarkeit für Aktualisierungen einzelner Refs. Konflikte zwischen Clients entstehen nur, wenn sie gleichzeitig auf dieselbe Ref pushen. Ein Client, der mehrere Branches liest, kann nicht davon ausgehen, dass diese Lesezugriffe einen einheitlichen Zeitpunkt im gesamten Repository abbilden.

Links enthalten drei aufeinanderfolgende Trunk-Packs die Commits A, B und C; D und E; sowie F. Rechts verzweigen sich Trunk und Topic bei C. Trunk schreitet bis G fort, Topic dagegen über D und E bis F. Das neueste Pack von Topic enthält D, E und F. Eine gestrichelte Umrandung markiert D und E als bereits im früheren Pack von Topic enthalten. Der blaue Pfeil markiert die Abhängigkeit des neuen Packs von Trunk bei G. Der Eltern-Commit von F in Git ist E.

Ein Push auf den Trunk fügt ein inkrementelles Pack mit einem oder mehreren Commits hinzu. Jeder Push auf einen Nebenbranch enthält erneut dessen frühere Commits. So kann ein Client mit der vom Server bekannt gegebenen Trunk-Version den Themenbranch mit einem einzigen Pack abrufen.

Dies ist eine Form kausaler Konsistenz. Beim Abrufen einer Referenz sieht der lesende Client die Historie des schreibenden Clients, der sie erzeugt hat. Während eines Pushs steuert Pando, auf welche gemeinsame Historie sich Client und Server einigen. So erzeugen Clients Packdateien, die sich leichter gemeinsam nutzen lassen.

Daher stellt Pando bei Git-Operationen weder einen gemeinsamen Zeitpunkt für den Zustand aller Referenzen noch eine Reihenfolge der Ref-Aktualisierungen innerhalb eines Clone- oder Fetch-Vorgangs bereit. Nehmen wir konkret an, dass die Branches foo und bar während der Aushandlung eines vollständigen Klons aktualisiert werden. Während der Aushandlung kann ein Client foo in Version 1 und bar in Version 2 sehen, ein anderer dagegen foo in Version 2 und bar in Version 1. In der Praxis ist die globale Reihenfolge der Ref-Aktualisierungen für Menschen und Agenten unerheblich. Sie sind nur auf die Reihenfolge der Aktualisierungen einer einzelnen Referenz angewiesen.

Ansätze im Vergleich

In dieser Simulation der drei Konsistenzmodelle konfigurieren wir 1.000 Clients so, dass jeder einen eigenen Branch auswählt und wiederholt eindeutige Commits pusht. Dabei messen wir die gesamte verstrichene Zeit und die Latenz zwischen Push und Fetch. Letztere ist eine wichtige Kennzahl für Workloads: Sie misst die Umlaufzeit, die Menschen und Agenten für die Zusammenarbeit oder die Nutzung anderer Systeme benötigen, die auf Versionsverwaltung angewiesen sind. Die Simulation bildet Hardware von Standard- bis Premium-VMs sowie Blob-Speichersysteme ab. In dieser Simulation haben wir die Repository-weite Konsistenz für das Drei-Phasen-Commit-Verfahren gelockert.

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.

Starte diese Simulation, um Pando mit zwei anderen Ansätzen zu vergleichen. Wähle die Infrastruktur von Standard- bis Premium-Hardware, die Anzahl der Clients und weitere Einstellungen aus, um die Unterschiede zu sehen.

Das modellierte Drei-Phasen-Commit-System zeigt unter Last deutlich höhere Schreiblatenzen, weil seine Datenträger an ihre Leistungsgrenze stoßen. Systeme mit Write-ahead-Log zeigen aus demselben Grund deutlich höhere Leselatenzen: Die Server kommen mit dem Lesen des Journals kaum nach. Sowohl in Praxistests mit unserem Monorepo als auch in dieser Simulation unterstützt Pando viele Tausend Clients, die gleichzeitig pushen können, ohne die Leistung unter Last zu beeinträchtigen.

Beim Verarbeiten eines Pushs validiert Pando die eingehende Packdatei, während es sie in den Blob-Speicher streamt, ohne sie in ein lokales Repository zu entpacken. Wenn ein Client einen Fetch startet, wählt Pando die benötigten gespeicherten Packs aus, und der Client lädt sie herunter. Kein Server muss Packdateien in ein lokales Repository einspielen oder aufwendig entpacken und neu packen. Da die Server hauptsächlich die Aushandlung und Auswahl von Packdateien übernehmen, können Pando-Server auf Standardhardware laufen.

So kann Pando bis an die Leistungsgrenzen der Clients, des Netzwerks zwischen Clients und Pando sowie des Blob-Speicherdienstes skalieren. Bei ausreichender Kapazität können Clients auf unabhängigen Branches Packs jeweils so schnell übertragen, wie es ihre eigene Verbindung zulässt. Um mehr Clients mit dieser Geschwindigkeit zu bedienen, können wir Pando-Server hinzufügen und den Metadaten- und Blob-Speicher entsprechend skalieren.

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

Ein Agent bearbeitet Dateien, erstellt lokal einen Commit und führt git push aus. Dabei entsteht eine Packdatei mit diesen Änderungen, um einen Branch zu aktualisieren. Pando empfängt das Pack und streamt es in den Blob-Speicher. Nachdem Pando den eingehenden Push durch Dekodierung in einem einzigen Durchlauf validiert hat, nutzt es eine Transaktion über mehrere Schlüssel, um eine Ref-Version in unserem Metadatenspeicher abzulegen und den Ref-Zeiger atomar zu aktualisieren. So kann Pando eine Konsistenz gewährleisten, bei der Clients ihre eigenen Schreibvorgänge anschließend lesen können. Dieser gesamte Schreibvorgang erfordert O(1)O(1) Lese- und Schreibzugriffe auf Metadaten.

Wenn ein anderer Client den Branch abruft, wählt Pando anhand der bereits beim Client vorhandenen Historie eine Menge gespeicherter Packs aus. Dieser Vorgang erfordert O(log⁡Δ)O(\log \Delta) Metadaten-Lesezugriffe. Dabei ist Δ\Delta der kürzeste Abstand in Ref-Versionen zwischen dem vorhandenen und dem gewünschten Stand des Clients. Der Server gibt Packdatei-URIs zurück. Der Client lädt die Dateien über den Dienst Express herunter, der Packdateien in regionalen Caches speichert.

Dieses Design beruht auf den drei zentralen Diensten der Pando-Architektur: Pushes, Fetches und Kompaktierung.

Pushes skalieren

Wenn ein Client pusht, verhält sich ein herkömmlicher Git-Server wie jemand, der beim Poker alle Karten offenlegt: Er zeigt dem Client jeden vorhandenen Branch. Der Client sammelt dann Commits, Trees und Blobs, um eine Packdatei zu erstellen. Dabei schließt er Objekte aus, von denen er weiß, dass der Server sie bereits hat.

Mehr bekannt gegebene Refs können den Push verkleinern. Sie können aber auch die Wiederverwendung des Packs erschweren, weil einem anderen Client Objekte fehlen könnten, die der schreibende Client ausgelassen hat.

Pando verfolgt einen anderen Ansatz. Statt alle Karten offenzulegen, gibt es dem Client nur eine ausgewählte Menge von Branches bekannt. Das hat zwei Gründe:

  1. Eine konsistente Sicht auf alle Refs bereitzustellen, ist aufwendig. Bei Millionen von Branches kann allein die Bekanntgabe der Refs mehr als 100 MiB umfassen. Dafür muss jeder Server entweder eine aktuelle Zuordnung vorhalten, was erheblichen Koordinationsaufwand verursacht, oder eine Datenbankabfrage ausführen, deren Aufwand mit der Anzahl der Refs wächst.

  2. Erhält der Client nur eine Teilmenge der Refs, erzeugt er ein Pack, das ausschließlich von den Objekten abhängt, die der Server ausdrücklich zum Auslassen freigibt. Indem Pando diese Abhängigkeiten erfasst, kann es die bei einem Push erzeugte Packdatei wiederverwenden und als Antwort auf einen späteren Fetch ausliefern.

Je mehr Refs ein Git-Server bekannt gibt, desto mehr Quellen haben pushende Clients, aus denen sie Objekte ausschließen können. Dadurch können sie kleinere Packs erzeugen. Um diese Packdateien wiederzuverwenden, muss der Server jedoch nachweisen können, dass die abrufenden Clients über sämtliche Abhängigkeiten verfügen. Andernfalls fehlen in der Packdatei möglicherweise Objekte, die der Client benötigt.

Gibt ein Git-Server dagegen gar keine Refs bekannt, würde der Client eine Packdatei mit der vollständigen Historie des Repositorys erzeugen. Diese Packdatei kann jeder andere Client nutzen. Der erhebliche Nachteil: Das Abrufen eines beliebigen Branches entspricht dann praktisch einem vollständigen Klon.

Kurve zum Einfluss bekannt gegebener Referenzen auf die Packdateigröße: Ohne Referenzen wird die gesamte Historie übertragen, bei Bekanntgabe des Trunks weniger, bei allen Referenzen nur neue Objekte.

Je mehr Refs bekannt gegeben werden, desto kleiner werden die Packdateien. Den größten Teil der Verkleinerung kann bereits die Bekanntgabe einer einzigen Ref bewirken.

Wenn ein Client einen neuen Feature-Branch pusht, gibt Pando standardmäßig nur einen festgelegten Trunk-Branch als Basis bekannt. So bleibt die Bekanntgabe gleich groß, auch wenn das Repository mehr Branches erhält. Das erfordert allerdings Kompromisse. Wenn jemand aus dem Entwicklungsteam mehrfach auf denselben Branch pusht oder einen Feature-Branch auf einem anderen aufbaut, muss der Client alles außerhalb des Trunks erneut senden.

Dadurch werden aufeinanderfolgende Pushes auf einen Branch größer, enthalten möglicherweise redundante Objekte und beanspruchen zusätzliche Bandbreite des Clients. In unserem Monorepo lag die mediane Größe der so erzeugten Packdateien unter 20 KiB, das 99. Perzentil unter 3 MiB. Für uns war das ein sinnvoller Kompromiss: Ein lesender Client, der die bekannt gegebene Trunk-Historie bereits besitzt, kann den Branch mit dieser einen Packdatei abrufen.

Nachdem Pando die Refs ausgewählt hat, die es dem Client bekannt gibt, nimmt es den Push an und aktualisiert seinen Metadatenspeicher. Pando teilt die Metadatenaktualisierung in synchrone und asynchrone Teile auf. Die synchrone Metadatenaktualisierung stellt sicher, dass Clients den neu aktualisierten Commit sofort abrufen können. Dabei wird eine neue Ref-Version geschrieben und die Zuordnung des Ref-Zeigers auf diese Version aktualisiert. Gespeichert werden die neue Branch-Spitze, die bei der Aushandlung der Packdatei verwendeten Abhängigkeiten und ein Verweis auf die gepushte Packdatei.

Zugleich werden in derselben Transaktion asynchrone Aufgaben erfasst, etwa das Einplanen der Kompaktierung. Bei sehr großen Pushes wird nur eine Teilmenge der Commits synchron geschrieben. So benötigen wir unabhängig von der Größe des Pushs nur eine konstante Anzahl von Lese- und Schreibzugriffen auf den Metadatenspeicher.

Entscheidend ist, dass Pando-Server Pushes auf unabhängige Refs unabhängig voneinander annehmen können. Der einzige Schlüssel, um den konkurriert wird, ist der Ref-Zeiger. Diesen schützen wir durch eine atomare Operation ähnlich Compare-and-Swap. Wir können den Schreibdurchsatz erhöhen, indem wir Pando-Server, Skalierungseinheiten für den Metadatenspeicher oder Blob-Speicher hinzufügen.

Fetches und Klone mit voller Leitungsgeschwindigkeit

Bei einem Fetch übermittelt der Client dem Server zwei Mengen von Commit-IDs, die im Protokoll als Have und Want bezeichnet werden. Ein herkömmlicher Git-Server berechnet die Mengendifferenz zwischen den über die Want-Angaben erreichbaren Objekten und den über die Have-Angaben erreichbaren Objekten, komprimiert sie und sendet das Ergebnis an den Client. Der Open-Source-Git-Server verfügt zwar über verschiedene lokale Indizes, um diesen Vorgang zu beschleunigen. Dennoch kann die Berechnung dieser Packdateien aufwendig sein, und sie lassen sich meist nur schlecht zwischenspeichern.

Pando nutzt stattdessen die bei Pushes erfassten Commit-Zuordnungen, um die Have- und Want-Angaben des Clients nachzuschlagen. Daraus ermittelt es eine Menge von Ref-Versionen, die diese Commits enthalten. Jede dieser Ref-Versionen verweist auf eine Packdatei, die Pando zuvor empfangen hat. Damit ermittelt der Server den Bedarf des Clients und wählt Packdateien aus, die die benötigte Objektmenge abdecken.

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 bildet die Have- un Want-Angaben des Git-Protokolls auf eine Folge von Ref-Versionen ab. Wer einen Nebenbranch anfordert, benötigt auch dessen Abhängigkeiten auf dem Trunk.

Das Git-Protokoll erlaubt bei einem Clone oder Fetch nur eine einzige Packdatei in der HTTP-Antwort. Eine wenig beachtete Funktion des Git-Protokolls, packfile-uri, ermöglicht es dem Server jedoch, Links zum separaten Herunterladen weiterer Packs zurückzugeben. Pando nutzt ausschließlich diese Funktion und überträgt direkt in der Antwort eine leere Packdatei. Zur Unterstützung dieser Funktion hat OpenAI mehrere Verbesserungen an packfile-uri in das Upstream-Projekt eingebracht.

Die Systeme von OpenAI laufen in vielen geografischen Regionen. Deshalb betreiben wir in jeder Region Pando-Cluster mit lokalen Caches. Der Pack-Auslieferungsdienst Express speichert Inhalte unseres Blob-Speichers zwischen. Ein separater Dienst übernimmt das Caching des Metadatenspeichers. Dank dieser Caches kann Pando Anfragen nach einer Commit-ID oder spezielle Lesezugriffe auf eine Ref vollständig aus dem zwischengespeicherten Zustand des jeweiligen Clusters bedienen. Wir leiten Anfragen nach einer Packdatei mithilfe von konsistentem Hashing an Express-Server weiter und erreichen dabei Cache-Trefferraten von über 99 %.

Dieses Cache-System bietet einen extrem hohen Durchsatz, der die Netzwerkbandbreite zwischen Clients und Pando vollständig ausschöpfen kann. Mit diesen Packdatei-URIs und lokalen Caches haben wir nachgewiesen, dass Clone- und Fetch-Vorgänge 10-Gbit/s-Netzwerkverbindungen vollständig auslasten können, und zwar allein mit Standard-VMs von Cloud-Anbietern.

Kompaktierung und Zeit bis zum ersten Byte

Bei den meisten Git-Servern sind erneutes Packen und Garbage Collection nötig, um die Leistung eines wachsenden Repositorys zu erhalten. Beide Schritte geben Speicherplatz frei, beanspruchen aber viel Rechenleistung und I/O.

In einem Repository mit schnellen Änderungen wie dem Monorepo von OpenAI ist es nicht praktikabel, für jeden Push seit dem letzten Fetch eine eigene Packdatei-URI zu senden. Ein Client müsste täglich Zehntausende Packdateien herunterladen, um seinen Trunk auf den aktuellen Stand zu bringen.

Stattdessen fasst Pando Packs im Hintergrund zusammen, um diesen Aufwand zu senken. Das Design ähnelt der Kompaktierung in Log-Structured Merge-Trees. Jeder Push erzeugt eine von Pando als „L0“ bezeichnete Packdatei. Sie ist die kleinste Dateneinheit, die Pando übertragen kann. Im Laufe der Zeit sammeln Hintergrundprozesse diese Packdateien in Bündeln, die eine Folge von Ref-Versionen abdecken und ausgehend von L0 geometrisch wachsen. Heute verwendet unser Monorepo Packdatei-Bündel bis zur Ebene L4.

Diese Bündel erfüllen zwei Zwecke. Sobald ein Bündel ausreichend gefüllt ist, übernimmt es ein Kompaktierungsprozess und erzeugt daraus eine einzige Packdatei. Durch die Neuanordnung der Objekte innerhalb der Packdatei verbessert sich das Kompressionsverhältnis. Zugleich beschleunigt das die Fetch-Aushandlung, da die Bündel als Skip-Liste über Ref-Versionen dienen. Indem Pando die Indizes auf allen Ebenen gleichzeitig durchsucht, kann es in logarithmischer Zeit eine Menge von Packs auswählen, die an einen Client gesendet werden. Diese Packdatei-Bündel ermöglichen eine effizientere Fetch-Planung, unabhängig davon, ob sie bereits neu gepackt wurden. Das verringert sowohl die Durchsatz- als auch die Latenzanforderungen an Pando.

Compaction in Pando

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

Compaction in progressPublished cover

Die Kompaktierung fasst Packdateien vieler Ref-Versionen im Hintergrund zusammen. Das ermöglicht effizientere Fetches mit weniger Dateien.

Anders als bei vielen Git-Servern bleiben Packdateien auf jeder Ebene erhalten und werden weder verworfen noch durch Garbage Collection entfernt. Packs aller Ebenen werden fortlaufend an Clients ausgeliefert, abhängig von deren Have- und Want-Angaben bei einem Fetch. Für Clients, die Pando als API nutzen, erfordert das Lesen eines beliebigen Objekts aus einer Packdatei zudem meist weniger Daten aus einer L0- als aus einer L4-Datei, weil das Objekt mit geringerer Wahrscheinlichkeit komprimiert ist.

Die Kompaktierung in Pando reduziert die insgesamt an Clients übertragene Datenmenge erheblich. Zudem wächst der Aufwand der Pack-Planung nur sublinear mit dem Rückstand des Clients. Das ist besonders wichtig für große Repositories mit schnellen Änderungen, die unserer Erwartung nach in der Softwareentwicklung zur neuen Normalität werden.

Die Entwicklung von Pando

Wir haben Pando mit Codex entwickelt: vom Projektstart bis zum Produktivbetrieb in nur 10 Wochen. Den Kern von Pando entwarf eine einzige Person aus dem Entwicklungsteam. Teams aus Agenten setzten ihn um, und eine Handvoll Personen brachte ihn in den Betrieb. Seit April dieses Jahres verarbeitet Pando produktiven Datenverkehr. Es ist Teil eines größeren Systems von Werkzeugen zur Versionsverwaltung bei OpenAI.

Zur Validierung des Pando-Designs nutzten wir eine Vielzahl von Ansätzen. Wir wandten unsere Grundsätze für die Entwicklung sichererer Softwaregrundlagen⁠ an. Da wir bei Null begannen, entschieden wir uns, Pando in Rust und ohne Altlasten bei den Abhängigkeiten zu implementieren. Wir schrieben eine Konformitätstestsuite, mit der Agenten ihre Änderungen gegen das Standardverhalten von Git prüfen konnten. Außerdem entwickelten wir Harnesses für deterministische Simulationstests, um Invarianten und Systemeigenschaften zu prüfen. Damit konnten wir schwer erkennbare Race Conditions sowie Regressionen bei der Aushandlung und Pack-Planung finden und entschärfen.

Viele Modelle trugen zu diesem System bei. Sobald neue Modelle verfügbar waren, nutzten wir sie, um die Leistung und Zuverlässigkeit von Pando zu verbessern. Mit GPT‑6 Astra schrieben Agenten Code in Lean und TLA+, um eine „Theorie von Pando“ zu formalisieren. Damit fanden und behoben wir Implementierungsfehler und bewiesen Eigenschaften des Systems. Mehrere ununterbrochene Sitzungen liefen Hunderte Stunden lang, um Grenzfälle in Pando zu finden und zu beheben. Außerdem setzen wir rund um die Uhr Agenten mit Daybreak-fähigen Modellen ein, um Git und Pando durch Red-Teaming auf Schwachstellen zu prüfen.

Die Einführung von Pando

Zunächst setzten wir Pando als Read-through- und Write-through-Proxy vor unserer bestehenden Versionsverwaltung ein. So konnten wir Pando und das bisherige System synchron halten und den Datenverkehr sicher und ohne Ausfallzeit auf Pando umstellen.

Im Rahmen seines Caching-Designs kann Pando Anfragen vor der Weiterleitung auch deduplizieren und zusammenfassen. Dadurch blieben unsere bestehenden Werkzeuge stabil, während wir die Systeme einzeln auf Pando umstellten. Mit jedem weiteren auf Pando migrierten Workload füllten dessen Pushes den Cache von Pando und verbesserten die Zuverlässigkeit unseres bisherigen Backends.

Um die Leistung mit Pando zu optimieren, begannen wir, unsere öffentliche Git-Distribution auf Entwicklungsrechnern und in der ChatGPT‑Desktop‑App auszuliefern. Für die schrittweise Migration der Clients auf Pando nutzten wir einen internen Wrapper um die Git-CLI. Wir entwickelten zahlreiche weitere Werkzeuge für Änderungsanträge und Code-Reviews sowie APIs für Automatisierungen. Zusammen mit dem Git-Datenverkehr migrierten wir Hunderte Integrationen, die von älteren APIs, Webhooks und Repository-Downloads abhingen.

Codex selbst unterstützte einzelne Entwicklungsfachkräfte und ganze Teams bei der Umstellung. Wir erstellten Skills, um bestehende Checkouts vorzubereiten und Agenten bei der Nutzung des neuen Systems anzuleiten. So konnten die Entwicklungsteams Codex einfach bitten, ihre Entwicklungsumgebungen und Dienste zu migrieren.

Der Wechsel zu Pando verbesserte die Zuverlässigkeit von Merges und senkte die mediane Push-Latenz um fast 80 %.

Pando kann mit nur einer Handvoll CPU-Kernen ein Petabyte Git-Daten pro Tag ausliefern. Pando hat in einer einzigen Stunde mehr als 57 Terabyte Git-Packs ausgeliefert. Über 99 % dieser Bytes kamen aus dem lokalen Datenträger-Cache.

Durch die Wiederverwendung von Packs können viele Clients dieselbe Repository-Historie herunterladen, während auf den ausliefernden Knoten nur wenig Rechenaufwand entsteht. Dieses Beispiel zeigt ein einzelnes Repository, doch Pando ist für die Unterstützung vieler weiterer ausgelegt. Bei wachsendem Bedarf können wir die Datenauslieferung und die Git-Aushandlung unabhängig voneinander skalieren.

Zum Git-Projekt beitragen

Pando auf dem Git-Protokoll und dem Git-Client aufzubauen, half uns, Fehler und Möglichkeiten zur Leistungsverbesserung in Git selbst zu erkennen.

Pando liefert zum Beispiel wiederverwendbare Packs direkt an Clients aus. Deshalb wurde das Herunterladen und Indexieren dieser Packs zu einem zentralen Faktor für die Geschwindigkeit und Zuverlässigkeit des Systems. Dabei zeigten sich Grenzfälle bei parallelen Downloads, beim Aufräumen nach unterbrochenen Übertragungen und beim Fortsetzen bereits abgeschlossener Downloads. Wir korrigierten diese Codepfade, ergänzten Validierungen bei der Übernahme eines vorhandenen Packs für einen Fetch und brachten diese Arbeit in das Upstream-Projekt ein.

Außerdem mussten wir erkennen können, wofür Zeit und Bandbreite benötigt wurden. Deshalb ergänzten wir Tracing für Packfile-URI-Downloads und Byte-Zähler bei der Pack-Erzeugung. So lässt sich leichter feststellen, ob ein langsamer Fetch Zeit für das Erzeugen oder das Übertragen von Packs benötigt.

Im Git-Build von OpenAI⁠(wird in einem neuen Fenster geöffnet) kann der Client unabhängige Packs parallel herunterladen und indexieren. Wir führten das Konzept der „expliziten Have-Angaben“ ein. Damit kann ein Server die aktuelle Spitze eines Branches getrennt von der Historie bekannt geben, die ein pushender Client aus seinem Pack auslassen darf. Bei Pando können Clients dadurch mit --force-with-lease prüfen, ob sich ein Feature-Branch nicht verändert hat, und ihr Pack allein auf Basis des Trunks erstellen. Jeder Client mit dieser Trunk-Historie kann den Branch dann mit demselben Pack abrufen.

Mehrere dieser Verbesserungen sind bereits im Upstream-Git enthalten, viele weitere in unserem öffentlichen Repository. Wir arbeiten weiter mit der Git-Community daran, diese Verbesserungen auch über Pando hinaus nutzbar zu machen. Du kannst diese Arbeit in unserem öffentlichen Patch-Tracker⁠(wird in einem neuen Fenster geöffnet) verfolgen.

Wie es weitergeht

Für Pando haben wir gängige Annahmen zur Skalierung von Git-Repositories hinterfragt. So können wir unser Monorepo skalieren, ohne die Git-Workflows zu ändern, die unsere Entwicklungsteams und Agenten bereits nutzen. In unserem Monorepo ermöglicht Pando mehr Parallelität, zuverlässigere Merges und eine schnellere Veröffentlichung von Codeänderungen. Über unser Monorepo hinaus prüfen wir weitere Einsatzmöglichkeiten für Pando bei OpenAI. Dazu gehören Workloads, bei denen viele Agenten in einem gemeinsamen Git-Repository zusammenarbeiten müssen.

Da Entwicklungsteams und Agenten immer mehr Software erstellen, werden auch mehr Teams außerhalb von OpenAI an dieselben Grenzen der Versionsverwaltung stoßen wie wir. Wir prüfen derzeit, wie wir die Technologie hinter Pando den Menschen zugänglich machen können, die unsere Produkte nutzen.

Pando baut auf zwei Jahrzehnten Erfahrung im Betrieb und in der Nutzung von Git-Repositories auf. Wir werden Pando weiterentwickeln und mit der Open-Source-Community von Git zusammenarbeiten, um Verbesserungen zu teilen, die auch anderen zugutekommen, die Git nutzen.

Autor:in

OpenAI