OpenAI

2 ноября 2026 г.

Инженерия

Масштабирование Git в OpenAI

Создаём систему контроля версий для работы в масштабе агентов и развиваем экосистему Git.

Авторы: Аарон Фрил, Тейлор Блау и Тед Найман, сотрудники технической команды

Загрузка…

За последний год размер нашего монорепозитория и нагрузка на него росли ошеломляющими темпами. Стало ясно: нам нужно перестроить систему контроля версий, чтобы она справлялась с масштабом параллельной работы, который делает возможным Codex.

Мы создали Pando — новый бэкенд для Git-репозиториев, — не прерывая разработку и не меняя привычный для команды способ работы с Git. Теперь вся разработка в монорепозитории OpenAI работает на Pando. Pando создана для достижения двух амбициозных целей. Первая — горизонтальное масштабирование записи с оптимизацией под одновременные операции push во множество веток. Вторая — максимальная пропускная способность при чтении, чтобы клиенты могли скачивать данные, полностью используя доступную полосу сети.

Мы достигли этих целей, сохранив совместимость с протоколом и клиентом Git. Благодаря этому инженеры и агенты смогли без затруднений перейти на Pando в рамках существующих рабочих процессов. В рабочей среде Pando сократила медианную задержку push почти в 5 раз. Теперь она передаёт почти петабайт данных Git в день.

Здесь мы расскажем, зачем нам понадобилась новая система контроля версий и почему мы решили сохранить совместимость с Git. Мы сравним нашу систему с распространёнными подходами к масштабированию Git и объясним, почему именно Pando способна удовлетворить потребности OpenAI в масштабировании. Также обсудим дальнейшие планы и наш вклад в развитие Git для поддержки работы в масштабе агентов.

Зачем нам понадобилась новая система контроля версий

Codex изменил масштаб и темп разработки в OpenAI, позволив инженерам выполнять гораздо больше работы параллельно. Это означает больше веток, операций push и слияний, а также больше запросов fetch и clone из сред разработки, исследовательских задач и CI.

Эта нагрузка нарастает. Теперь у одного инженера сразу несколько изменений готовы к CI, а каждый push может запустить несколько задач, скачивающих один и тот же коммит.

Примерно с конца прошлого года системы, обслуживающие наш монорепозиторий, начали с трудом справляться с нагрузкой. Операции fetch и push замедлялись, слияния завершались с ошибками, а показатель надёжности временами опускался ниже 90%. Мы упёрлись в ограничения самого Git или инфраструктуры, обслуживающей наш монорепозиторий? Git вырос из сообщества, которое обменивалось патчами к ядру по электронной почте. Более двух десятилетий разработчики адаптируют инструменты вокруг Git по мере изменения способов создания ПО. Когда работа с нашим монорепозиторием стала вызывать затруднения, для нас было важно продолжить опираться на этот опыт.

Мы увидели способ сохранить привычный рабочий процесс, перестроив бэкенд так, чтобы он параллельно обрабатывал push в разные ветки и одновременно передавал одну историю множеству читающих клиентов. Сохранив Git в основе контроля версий, мы могли сделать улучшения клиента полезными и за пределами OpenAI. Мы хотели перенести эти улучшения из нашей копии Git⁠(открывается в новом окне) в основной проект, чтобы ими могло пользоваться всё сообщество.

Инженерные решения для масштабирования Git

Масштабирование системы контроля версий имеет два ключевых направления: множество клиентов отправляют изменения в разные ветки, а множество других — получают одну и ту же историю. Pando оптимизирует оба направления.

Во-первых, Pando координирует обновления каждой ветки независимо. Независимые ветки могут обновляться параллельно, а клиент, запрашивающий ветку, получает историю, необходимую для этой ревизии. Компромисс в том, что клиент, читающий несколько веток, может увидеть их состояния на разные моменты времени.

Во-вторых, Pando повторно использует pack-файлы — формат Git для совместного сжатия нескольких объектов — при разных запросах fetch, вместо того чтобы создавать отдельный pack-файл для каждого клиента. Клиент может скачать некоторые уже имеющиеся объекты, зато сервер может передавать одни и те же байты множеству клиентов с минимальными вычислениями.

Эти решения позволяют масштабировать запись по веткам и обслуживать чтение из кешей. Чтобы понять, как они работают вместе, начнём с того, как Git хранит и передаёт историю.

Основы хранения объектов Git

Git должен поддерживать синхронизацию двух частей репозитория: объектов и ссылок.

Git-репозиторий хранит снимки файлов и связывающую их историю в виде графа объектов. Объект может быть blob-объектом, деревом, коммитом или тегом. Blob-объекты содержат данные отдельных файлов, а деревья связывают имена с blob-объектами или другими деревьями, формируя структуру каталогов. Коммиты указывают на корневое дерево и ноль или более родительских коммитов, формируя историю репозитория.

Идентификатор объекта — хеш его типа и содержимого. Объекты неизменяемы: при записи изменённого файла создаётся новый blob-объект, а для неизменившихся файлов и каталогов можно повторно использовать существующие объекты.

Ветки и теги — это ссылки (refs), указывающие на коммиты. При фиксации изменений в ветке создаётся новый коммит, а ссылка ветки обновляется и указывает на него.

Ветка refs/heads/main указывает на коммит C2, родитель которого — C1. C1 указывает на корневое дерево T1, где parser.py ссылается на blob-объект B1. C2 указывает на корневое дерево T2, где parser.py ссылается на blob-объект B2. В обоих корневых деревьях docs/ ссылается на одно и то же дерево T3. В нём запись README.md указывает на blob-объект B3, а usage.md — на blob-объект B4.

Объекты Git ссылаются друг на друга по идентификаторам: коммиты — на деревья, а деревья — на другие деревья и blob-объекты. Ветка refs/heads/main указывает на коммит C2, родитель которого — C1. C1 указывает на корневое дерево T1, где parser.py ссылается на blob-объект B1. C2 указывает на корневое дерево T2, где parser.py ссылается на blob-объект B2. В обоих корневых деревьях docs/ ссылается на одно и то же дерево T3. В нём запись README.md указывает на blob-объект B3, а usage.md — на blob-объект B4.

Git хранит объекты в «свободном» или «упакованном» виде. Свободные объекты хранятся в отдельных файлах, определяемых по идентификатору объекта. Упакованные объекты хранятся вместе в pack-файле, что позволяет представлять похожие объекты как дельты относительно друг друга.

Две версии parser.py находятся в $GIT_DIR/objects. В свободном виде blob-объект A содержит parse(), а blob-объект B — parse() и parse_file(). Pack-файл хранит A целиком, а B представляет как инструкции: скопировать весь A и добавить parse_file(). Сопутствующий файл *.idx позволяет найти оба blob-объекта в pack-файле по идентификаторам.

Git может хранить blob-объекты в отдельных файлах или упаковывать их вместе, кодируя один blob-объект как дельту относительно другого.

В Git объект достижим из коммита, если Git может найти его, следуя по ссылкам от этого коммита через родительские коммиты, деревья и blob-объекты. При fetch клиент сообщает серверу, какие коммиты ему нужны и какие у него уже есть. Сервер находит объекты, достижимые из нужных коммитов, исключает те, которые, насколько ему известно, уже есть у клиента, и отправляет остальные в pack-файле того же формата, который Git использует на диске. В начале полной операции clone у клиента нет никаких объектов, поэтому ему нужны все объекты, достижимые из истории, копируемой при clone.

Push передаёт объекты в обратном направлении, запрашивая у сервера обновление одной или нескольких ссылок. Клиент использует объявленный сервером набор ссылок, чтобы не отправлять объекты, которые уже есть на сервере. Поэтому отправляемый пакет может зависеть от существующих объектов: одного этого пакета недостаточно для восстановления новой истории.

Репликация Git-репозитория означает поддержание доступности объектов и ссылок на нескольких серверах. Прежде чем реплика сможет отдавать новое значение ссылки, она должна уметь предоставлять все объекты, достижимые из этого значения. Сервису также нужно координировать конкурирующие обновления ссылок — то есть операции push, которые пытаются перевести одну ветку с одной и той же прежней вершины на разные новые коммиты. В системах с единым порядком всех записей в репозиторий даже операции push в независимые ветки могут ждать друг друга.

Существующие схемы распределённых Git-сервисов

При проектировании Pando мы оценили традиционные подходы к масштабированию контроля версий на базе Git. Первый координирует обновления ссылок между репликами репозитория с помощью трёхфазной фиксации (3PC — протокола согласования фиксации между несколькими узлами). Второй упорядочивает обновления через общий журнал упреждающей записи (WAL — журнал изменений, записываемых до их применения) в blob-хранилище.

Трёхфазная фиксация (3PC)

В смоделированной нами схеме с трёхфазной фиксацией Git-сервис хранит полную копию репозитория на нескольких репликах. Когда любой сервер получает push, входящий pack-файл копируется и параллельно отправляется всем репликам. Для публикации изменений большинство реплик должно принять обновление ссылки. Управляющий этим сервер называется координатором. Процесс требует нескольких раундов обмена сообщениями между ним и каждой из остальных реплик.

Чтобы принять обновление ссылки, каждая реплика блокирует свою копию ссылки и проверяет, что та всё ещё указывает на ожидаемый клиентом коммит. Затем координатор отправляет принявшим обновление репликам сообщение о предварительной фиксации. Когда большинство подтверждает это сообщение, координатор даёт им команду выполнить обновление. После этого любая актуальная реплика может объявлять новую вершину ветки из своей локальной копии репозитория.

Полные реплики позволяют обслуживать чтение локально и гарантируют клиентам согласованное представление всех ссылок независимо от выбранной реплики. В результате ссылки обновляются по одной, а каждый push требует нескольких циклов обмена сообщениями для согласования обновления между репликами.

Из-за ожидания кворума время выполнения push зависит от производительности дисков реплик и задержек связи. По мере роста объёма записи реплики должны успевать обрабатывать и входящие пакеты, и запросы чтения.

Журнал упреждающей записи (WAL)

В смоделированной нами схеме на базе WAL Git-сервис использует общий манифест в blob-хранилище для упорядочивания обновлений всего репозитория. Получив push, сервер загружает входящий pack-файл в blob-хранилище и подготавливает обновление ссылки в своей локальной копии репозитория. Для публикации изменений он добавляет в общий манифест в blob-хранилище запись об обновлении ссылки с указателем на pack-файл. Затем Git-сервер обновляет манифест операцией сравнения с обменом. Она либо атомарно заменяет манифест, либо отклоняет изменение. Если за это время другой отправитель обновил манифест, сервер читает появившиеся записи, проверяет, что его обновление ссылки всё ещё допустимо, и повторяет попытку с уже загруженным pack-файлом.

Отправители могут загружать pack-файлы параллельно, но даже push в независимые ветки требуют обновления одного и того же манифеста. Время полного цикла успешной операции сравнения с обменом ограничивает частоту публикации нового манифеста репозитория. Blob-хранилище с низкой задержкой ускоряет этот этап. Группировка позволяет одним манифестом обновлять сразу несколько ссылок, но требует накопления обновлений в очереди и увеличивает задержку.

Blob-хранилище может принимать pack-файлы параллельно, но обновления общего манифеста всё равно конкурируют друг с другом. Одного лишь наращивания ресурсов blob-хранилища недостаточно, чтобы устранить это узкое место. Каждой реплике по-прежнему нужно прочитать и проиндексировать пакеты из blob-хранилища, прежде чем отдавать их локально. Если входящие записи поступают быстрее, чем выполняется эта работа, реплики отстают, а клиенты дольше ждут возможности получить новые коммиты.

Архитектура Pando

Так мы подошли к Pando. Pando — новая архитектура обслуживания Git-репозиториев, рассчитанная на масштабирование при множестве клиентов, одновременно выполняющих push. Вот как она работает:

Переосмысление ограничений

Еще до разработки Pando мы предполагали, что сможем выполнить наши требования, не отказываясь от Git. Однако пришлось пересмотреть некоторые условия, которые большинство систем на базе Git считают обязательными:

  • Согласованность всего репозитория: Чтобы получить согласованный снимок всех ссылок на определенный момент времени, нужна координация в масштабе всего репозитория. В системе с единым порядком обновления всех ссылок операции push в независимые ветки конкурируют за одну точку координации.

  • Уникальные pack-файлы для каждого клиента: Если два клиента с разной локальной историей выполняют fetch одной и той же ветки, серверу может потребоваться создать отдельный pack-файл для каждого, чтобы не отправлять уже имеющиеся у них объекты. Подготовка таких ответов требует процессорного времени и дискового ввода-вывода, что затрудняет масштабирование при большом числе читающих клиентов.

Вместо этого Pando использует ослабленную модель согласованности — причинную согласованность — для масштабирования записи и повторно использует pack-файлы для масштабирования чтения.

Причинная согласованность (CC)

Когда клиент выполняет push в ветку, Pando сохраняет входящий pack-файл как неизменяемый blob-объект в blob-хранилище. При каждом изменении ветки Pando записывает в хранилище метаданных версию ссылки, содержащую новую вершину ветки, pack-файл, из которого она получена, и все предыдущие версии ссылок, от которых зависит pack-файл. И blob-хранилище, и хранилище метаданных масштабируются горизонтально.

Чтобы выполнить fetch коммита или ссылки из Pando, клиент должен последовательно применить набор версий ссылок (pack-файлов), воссоздав состояние клиента, который создал версию ссылки, получаемую при fetch. Это дает более слабую согласованность в масштабе всего репозитория, но обеспечивает сериализуемость обновлений отдельных ссылок. Конфликты между клиентами возникают только при одновременном push в одну ссылку. Клиент, читающий несколько веток, не может считать, что полученные данные отражают состояние репозитория на один момент времени.

Слева три последовательных pack-файла основной ветки (trunk) содержат коммиты A, B и C; D и E; F. Справа основная ветка и тематическая ветка (topic) расходятся в точке C. Основная ветка продвигается к G, а тематическая — через D, E и F. Последний pack-файл тематической ветки содержит D, E и F. Пунктирная рамка отмечает D и E, которые также присутствуют в предыдущем pack-файле тематической ветки. Синяя стрелка обозначает зависимость нового pack-файла от основной ветки в точке G. Родительский коммит F в Git — E.

Push в основную ветку добавляет инкрементальный pack-файл с одним или несколькими коммитами. Каждый push в боковую ветку снова включает предыдущие коммиты этой тематической ветки, поэтому клиент с объявленной версией основной ветки может выполнить fetch тематической ветки, получив один pack-файл.

Это одна из форм причинной согласованности. При выполнении fetch ссылки читающий клиент видит историю клиента, который ее записал. При push Pando определяет, какую историю клиент и сервер считают общей, чтобы клиенты создавали pack-файлы, которые проще использовать повторно.

Поэтому Pando не предоставляет состояние всех ссылок на один момент времени при операциях Git и не гарантирует единый порядок обновлений ссылок в рамках clone или fetch. Например, допустим, что ветки foo и bar обновляются во время согласования полной операции clone. В ходе согласования один клиент может увидеть foo версии 1 и bar версии 2, а другой — foo версии 2 и bar версии 1. На практике пользователям и агентам не важен глобальный порядок обновления ссылок: они полагаются на порядок обновлений отдельной ссылки.

Сравнение подходов

В этой симуляции трех моделей согласованности каждый из 1 000 клиентов выбирает отдельную ветку и затем многократно выполняет push уникальных коммитов. Мы измеряем общее время выполнения и задержку от push до доступности данных для fetch. По`следний показатель важен для рабочих нагрузок: он отражает время полного цикла взаимодействия пользователей и агентов при совместной работе или использовании других систем, зависящих от контроля версий. Симуляция моделирует инфраструктуру от стандартных до высокопроизводительных виртуальных машин и blob-хранилищ. В этой симуляции мы ослабили согласованность на уровне репозитория для трехфазной фиксации.

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.

Запустите симуляцию, чтобы сравнить Pando с двумя другими подходами. Выберите инфраструктуру — от стандартного до высокопроизводительного оборудования, — число клиентов и другие параметры, чтобы сравнить результаты.

Моделируемая система с трехфазной фиксацией показывает значительно большую задержку записи под нагрузкой, когда диски достигают предела производительности. По той же причине у систем с журналом упреждающей записи существенно растет задержка чтения: серверы не успевают считывать журнал. И в реальных тестах на нашем монорепозитории, и в этой симуляции Pando поддерживает многие тысячи клиентов, одновременно выполняющих push, без падения производительности под нагрузкой.

При обработке push Pando проверяет входящий pack-файл одновременно с его потоковой передачей в blob-хранилище, не распаковывая его в локальный репозиторий. Когда клиент начинает fetch, Pando выбирает нужные ему сохраненные pack-файлы, а клиент скачивает их. Ни одному серверу не нужно применять pack-файлы к локальному репозиторию или выполнять затратные операции распаковки и упаковки. Поскольку серверы Pando в основном выполняют согласование и выбор pack-файлов, они могут работать на стандартном оборудовании.

Это позволяет масштабировать Pando до пределов возможностей клиентов, сети между клиентами и Pando и сервиса blob-хранилища. При достаточных ресурсах каждый клиент, работающий с независимой веткой, может передавать pack-файлы с максимальной скоростью своего соединения. Чтобы обслуживать больше клиентов на такой скорости, мы можем добавлять серверы Pando и одновременно масштабировать хранилище метаданных и blob-хранилище.

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

Агент редактирует файлы, создает локальный коммит и запускает git push. Эта команда формирует pack-файл с изменениями для обновления ветки. Pando принимает pack-файл и передает его потоком в blob-хранилище. Проверив входящий push с помощью однопроходного декодирования, Pando выполняет транзакцию по нескольким ключам: записывает версию ссылки в хранилище метаданных и атомарно обновляет указатель ссылки. Так Pando обеспечивает согласованность по принципу «чтение собственных записей». Вся эта операция записи требует O(1)O(1) операций чтения и записи метаданных.

Когда другой клиент выполняет fetch ветки, Pando выбирает набор сохраненных pack-файлов с учетом уже имеющейся у него истории. Эта операция требует O(log⁡Δ)O(\log \Delta) чтений метаданных, где Δ\Delta — кратчайшее расстояние в версиях ссылок между тем, что у клиента есть, и тем, что он хочет получить. Сервер возвращает URI pack-файлов, а клиент скачивает их через сервис Express, который хранит pack-файлы в региональных кешах.

Эта схема опирается на три ключевых сервиса архитектуры Pando: push, fetch и compaction.

Масштабирование push

Когда клиент выполняет push, обычный Git-сервер ведет себя как игрок в покер, раскрывающий все карты: сообщает клиенту обо всех своих ветках. Затем клиент собирает коммиты, деревья и blob-объекты в pack-файл, исключая объекты, которые, насколько ему известно, уже есть на сервере.

Объявление большего числа ссылок может уменьшить объем push. Но это может затруднить повторное использование pack-файла: у другого клиента могут отсутствовать объекты, которые отправитель исключил.

Pando использует другой подход. Вместо того чтобы раскрывать все карты, она объявляет клиенту только выбранный набор веток. Для этого есть две причины:

  1. Предоставлять согласованное представление всех ссылок затратно. При миллионах веток одно лишь объявление ссылок может превышать 100 МиБ. Для этого каждый сервер должен либо поддерживать актуальное отображение ссылок с существенными затратами на координацию, либо выполнять запрос к базе данных, стоимость которого растет с числом ссылок.

  2. Если сообщать клиенту лишь часть ссылок, он создаст pack-файл, зависящий только от тех данных, которые сервер явно разрешил исключить. Отслеживая зависимости, Pando может повторно использовать pack-файл, созданный клиентом при push, и отправить его в ответ на последующий fetch.

Чем больше ссылок объявляет Git-сервер, тем больше у клиентов, выполняющих push, оснований исключать объекты и тем меньшие pack-файлы они могут создавать. Однако для повторного использования этих pack-файлов сервер должен убедиться, что у клиентов, выполняющих fetch, есть все зависимости. Иначе в pack-файле могут отсутствовать нужные клиенту объекты.

С другой стороны, если Git-сервер не объявляет ни одной ссылки, клиент создаст pack-файл с полной историей репозитория. Такой pack-файл подойдет любому другому клиенту, но цена высока: операция fetch любой ветки фактически превращается в полную операцию clone.

Кривая зависимости размера pack-файла от объявленных ссылок: от полной истории, если ссылки не объявлены, через объявление основной ветки до только новых объектов, если объявлены все ссылки.

Чем больше ссылок объявлено, тем меньше pack-файлы. При этом основное уменьшение размера может дать объявление всего одной ссылки.

По умолчанию, когда клиент выполняет push новой функциональной ветки, Pando объявляет в качестве базы только назначенную основную ветку. Так размер объявления остается неизменным по мере роста числа веток в репозитории, но у этого есть своя цена. Когда инженер многократно выполняет push в одну ветку или создает одну функциональную ветку поверх другой, клиенту приходится повторно отправлять все, чего нет в основной ветке.

Из-за этого последующие операции push в ветку становятся объемнее, могут содержать избыточные объекты и расходуют дополнительную полосу сети клиента. В нашем монорепозитории медианный размер созданного так pack-файла был менее 20 КиБ, а 99-й процентиль — менее 3 МиБ. Для нас это разумный компромисс: клиент, у которого уже есть объявленная история основной ветки, может выполнить fetch нужной ветки, получив один такой pack-файл.

Выбрав набор ссылок для объявления клиенту, Pando принимает push и обновляет хранилище метаданных. Pando делит обновление метаданных на синхронную и асинхронную части. Синхронное обновление метаданных гарантирует, что клиенты смогут сразу выполнить fetch нового коммита. При этом записывается новая версия ссылки и на нее переводится указатель ссылки. Сохраняются новая вершина ветки, зависимости, использованные при согласовании pack-файла, и ссылка на pack-файл, переданный при push.

Одновременно в той же транзакции фиксируется асинхронная работа, например постановка compaction в очередь. Для очень больших push синхронно записывается лишь часть коммитов. Благодаря этому требуется лишь постоянное число чтений и записей в хранилище метаданных независимо от размера push.

Важно, что серверы Pando могут независимо принимать push в несвязанные ссылки. Единственный ключ, за который возможна конкуренция, — указатель ссылки. Мы защищаем его атомарной операцией, аналогичной сравнению с обменом. Мы можем увеличивать пропускную способность записи, добавляя серверы Pando, узлы хранилища метаданных или blob-хранилища.

Fetch и clone на полной скорости канала

При выполнении fetch клиент передает серверу два набора идентификаторов коммитов, обозначаемых в протоколе как haves и wants. Обычный Git-сервер вычисляет разность множеств объектов, достижимых из want и из have, сжимает их и отправляет результат клиенту. Хотя открытый Git-сервер использует различные локальные индексы для ускорения этой операции, создание pack-файлов может быть затратным, а сами они обычно плохо поддаются кешированию.

Вместо этого Pando использует отображения коммитов, записанные при push, чтобы найти указанные клиентом haves и wants и сопоставить их с набором версий ссылок, содержащих эти коммиты. Каждая из этих версий ссылок указывает на pack-файл, ранее полученный Pando. На этой основе сервер определяет потребности клиента и выбирает pack-файлы, покрывающие нужный ему набор объектов.

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 сопоставляет have и want протокола Git с последовательностью версий ссылок. Запрос боковой ветки подразумевает запрос истории вплоть до ее зависимостей от основной ветки.

Протокол Git разрешает отправлять только один pack-файл в HTTP-ответе на clone или fetch. Однако недооцененная возможность протокола Git — packfile-uri — позволяет серверу возвращать ссылки для отдельного скачивания дополнительных pack-файлов. Pando использует только этот механизм, а в самом ответе передает пустой pack-файл. Для поддержки этой возможности OpenAI внесла ряд улучшений packfile-uri в основной проект Git.

Системы OpenAI работают во многих географических регионах, поэтому в каждом из них мы поддерживаем кластеры Pando с локальными кешами. Сервис доставки pack-файлов Express кеширует данные blob-хранилища, а отдельный сервис — данные хранилища метаданных. Эти кеши позволяют Pando обслуживать запросы по идентификатору коммита или специальные операции чтения ссылки целиком из кешированного состояния данного кластера. Мы направляем запросы pack-файлов на серверы Express с помощью консистентного хеширования; доля попаданий в кеш превышает 99%.

Эта система кешей обеспечивает чрезвычайно высокую пропускную способность, позволяя полностью загрузить сетевой канал между клиентами и Pando. Мы проверили: благодаря URI pack-файлов и локальным кешам операции clone и fetch полностью загружают каналы на 10 Гбит/с даже при использовании только стандартных виртуальных машин облачного провайдера.

Compaction и время до первого байта

Большинству Git-серверов для поддержания производительности растущего репозитория нужны переупаковка и сборка мусора. Обе операции освобождают место, но требуют значительных вычислительных ресурсов и ввода-вывода.

В быстро меняющемся репозитории, таком как монорепозиторий OpenAI, нецелесообразно отправлять отдельный URI pack-файла для каждого push, выполненного с момента последнего fetch. Каждый день клиенту пришлось бы скачивать десятки тысяч pack-файлов, чтобы обновить основную ветку.

Вместо этого Pando объединяет pack-файлы в фоновом режиме, снижая накладные расходы. Этот подход похож на compaction в LSM-деревьях. Каждый push создает pack-файл, который в Pando называется «L0» и представляет собой минимальную единицу передаваемых данных. Со временем фоновые обработчики собирают эти pack-файлы в покрытия последовательностей версий ссылок. Размер покрытий растет в геометрической прогрессии начиная с L0. Сейчас наш монорепозиторий использует покрытия pack-файлов вплоть до L4.

Эти покрытия выполняют две задачи. Когда покрытие достаточно заполнено, обработчик compaction берет его в работу и создает единый pack-файл. Реорганизация объектов внутри pack-файла повышает степень сжатия. Кроме того, покрытия ускоряют согласование fetch, работая как список с пропусками по версиям ссылок. Одновременно просматривая индексы на каждом уровне, Pando может выбрать набор pack-файлов для отправки клиенту за логарифмическое время. Независимо от того, переупакованы ли они, покрытия pack-файлов делают планирование fetch эффективнее, снижая требования к пропускной способности и задержкам Pando.

Compaction in Pando

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

Compaction in progressPublished cover

Уплотнение объединяет pack-файлы множества версий ссылок в фоновом режиме, сокращая число файлов и повышая эффективность fetch.

В отличие от многих Git-серверов, Pando сохраняет pack-файлы на всех уровнях, не удаляя их и не очищая сборкой мусора. Pack-файлы всех уровней постоянно используются для обслуживания клиентов в зависимости от haves и wants, указанных в запросе fetch. А для клиентов, использующих Pando через API, чтение произвольного объекта из pack-файла обычно требует прочитать меньше данных из L0, чем из L4, поскольку вероятность сжатия объекта там ниже.

Уплотнение в Pando существенно сокращает общий объём передаваемых клиентам данных. Кроме того, затраты на планирование пакетов растут сублинейно по мере увеличения отставания клиента. Это особенно важно для больших и быстро меняющихся репозиториев, которые, как мы ожидаем, станут новой нормой в разработке ПО.

Создание Pando

Мы создали Pando с помощью Codex, пройдя путь от старта проекта до запуска в рабочей среде всего за 10 недель. Ядро Pando спроектировал один инженер, реализовали команды агентов, а развернули несколько инженеров. Pando начала обслуживать рабочий трафик в апреле этого года и стала частью более широкой системы инструментов контроля версий в OpenAI.

Для проверки архитектуры Pando мы использовали множество подходов. Мы применили свои принципы создания более безопасной программной основы⁠. Поскольку проект создавался с нуля, мы решили реализовать Pando на Rust, без устаревших зависимостей. Мы написали набор тестов на соответствие, чтобы агенты могли проверять свои изменения на совместимость со стандартным поведением Git. Мы также разработали обвязки для детерминированного симуляционного тестирования, позволяющие проверять инварианты и свойства системы. Это помогло выявлять и устранять трудноуловимые состояния гонки и регрессии в согласовании и планировании pack-файлов.

В создании системы участвовало много моделей. По мере появления новых мы использовали их для повышения производительности и надёжности Pando. С помощью GPT‑6 Astra агенты написали код на Lean и TLA+, формализовав «теорию Pando». Мы использовали её, чтобы находить и исправлять ошибки реализации и доказывать свойства системы. Несколько непрерывных сеансов длились сотни часов: в них находили и исправляли ошибки Pando в граничных случаях. Мы также круглосуточно запускаем агентов на моделях с возможностями Daybreak для проверки безопасности Git и Pando методом red teaming.

Развёртывание Pando

Сначала мы развернули Pando как прокси для чтения и записи перед существующей системой контроля версий. Это позволяло поддерживать синхронизацию Pando с прежней системой и безопасно переводить трафик на Pando без простоев.

В рамках механизма кеширования Pando также умеет устранять дубликаты и объединять запросы перед проксированием. Благодаря этому существующие инструменты продолжали стабильно работать, пока мы поочерёдно переводили системы на Pando. По мере переноса новых рабочих нагрузок на Pando их операции push наполняли её кеш и повышали надёжность прежнего бэкенда.

Чтобы оптимизировать производительность Pando, мы начали поставлять нашу общедоступную сборку Git на компьютеры разработчиков и в составе настольного приложения ChatGPT. Для поэтапного перевода клиентов на Pando мы использовали внутреннюю обёртку над Git CLI. Мы создали множество других инструментов для обработки запросов на изменение и ревью кода, а также API для автоматизации. Вместе с трафиком Git мы перенесли сотни интеграций, зависевших от старых API, вебхуков и скачивания репозиториев.

Сам Codex помогал разработчикам и командам перейти на новую систему. Мы создали навыки для подготовки существующих рабочих копий и обучения агентов работе с новой системой. В результате разработчики могли просто попросить Codex перенести их среды разработки и сервисы.

Переход на Pando повысил надёжность слияний и сократил медианную задержку push почти на 80%.

Pando может передавать петабайт данных Git в день, используя всего несколько процессорных ядер. За один час Pando передала более 57 терабайт пакетов Git, причём свыше 99% этих байтов — из локального дискового кеша.

Повторное использование пакетов позволяет множеству клиентов скачивать одну и ту же историю репозитория с минимальными вычислениями на обслуживающих узлах. Этот пример касается одного репозитория, но Pando рассчитана на поддержку гораздо большего их числа. По мере роста спроса мы можем независимо масштабировать доставку данных и согласование Git.

Вклад в проект Git

Создание Pando на базе протокола и клиента Git помогло нам выявить ошибки и возможности повысить производительность самого Git.

Например, Pando передаёт повторно используемые пакеты напрямую клиентам, поэтому их скачивание и индексирование стали ключевыми для скорости и надёжности системы. Эта работа выявила граничные случаи при параллельном скачивании, очистке после прерванной передачи и возобновлении уже завершённых загрузок. Мы исправили обработку этих случаев, добавили проверки при принятии существующего пакета для fetch и внесли изменения в основной проект.

Нам также нужно было понять, на что расходуются время и полоса сети. Мы добавили трассировку загрузок packfile-uri и подсчёт байтов при создании пакетов, чтобы проще определять, что замедляет fetch: формирование пакетов или их передача.

В сборке Git от OpenAI⁠(открывается в новом окне) клиент может параллельно скачивать и индексировать независимые pack-файлы. Мы добавили понятие «explicit haves», чтобы сервер мог объявлять текущую вершину ветки отдельно от истории, которую клиент, выполняющий push, вправе исключить из pack-файла. В Pando это позволяет клиентам с помощью --force-with-lease проверять, что функциональная ветка не изменилась, и формировать pack-файл только относительно основной ветки. Затем любой клиент с этой историей основной ветки может выполнить fetch функциональной ветки с помощью того же pack-файла.

Некоторые из этих улучшений уже вошли в основной Git, а многие другие доступны в нашем публичном репозитории. Мы продолжаем работать с сообществом Git, чтобы эти улучшения приносили пользу и за пределами Pando. Следить за этой работой можно в нашем публичном трекере патчей⁠(открывается в новом окне).

Что дальше

Переосмыслив привычные подходы к масштабированию Git-репозиториев, мы создали Pando, которая позволяет масштабировать наш монорепозиторий, не меняя знакомые инженерам и агентам процессы работы с Git. В нашем монорепозитории Pando позволила повысить параллелизм, улучшить надежность слияний и сократить время публикации изменений кода. Помимо монорепозитория, мы изучаем и другие применения Pando внутри OpenAI, в том числе для задач, где множеству агентов нужно совместно работать в общем Git-репозитории.

По мере того как инженеры и агенты создают все больше программного обеспечения, все больше команд за пределами OpenAI будут сталкиваться с теми же ограничениями контроля версий, что и мы. Сейчас мы изучаем способы поделиться с пользователями технологией, лежащей в основе Pando.

Pando опирается на двадцатилетний опыт пользователей и администраторов Git-репозиториев. Мы продолжим развивать Pando и сотрудничать с сообществом открытого проекта Git, чтобы делиться улучшениями, полезными широкому кругу пользователей Git.

Автор

OpenAI