지난 1년간 모노리포의 규모와 이를 향한 수요는 놀라운 속도로 증가했습니다. Codex가 가능하게 하는 동시 작업 규모를 감당하려면 소스 관리 시스템을 개편해야 한다는 점이 분명해졌습니다.
저희는 엔지니어링 작업을 중단하거나 팀의 Git 사용 방식을 바꾸지 않고, Git 리포지터리용 새 백엔드인 Pando를 구축했습니다. 이제 OpenAI 모노리포의 모든 개발은 Pando를 기반으로 이루어집니다. Pando는 두 가지 도전적인 목표를 달성하도록 설계되었습니다. 첫째는 여러 브랜치에 대한 동시 푸시에 최적화된, 수평 확장 가능한 쓰기입니다. 둘째는 읽기 처리량을 극대화하여 클라이언트가 가용 네트워크 대역폭을 모두 활용할 만큼 빠르게 데이터를 다운로드할 수 있게 하는 것입니다.
Git 프로토콜 및 클라이언트와의 호환성을 유지하면서 이 목표를 달성했기에, 엔지니어와 에이전트 모두 기존 워크플로에 Pando를 원활하게 도입할 수 있었습니다. 프로덕션 환경에서 Pando는 푸시 지연 시간의 중앙값을 거의 5분의 1로 줄였습니다. 현재 하루에 약 1페타바이트의 Git 데이터를 제공합니다.
이 글에서는 새로운 소스 관리 시스템을 구축하게 된 배경과 Git과의 호환성을 유지하기로 한 이유를 살펴보겠습니다. 저희 시스템을 일반적인 Git 확장 방식과 비교하고, Pando가 OpenAI의 확장성 요구를 충족하는 데 특히 적합한 이유를 설명하겠습니다. 앞으로의 계획과 에이전트 규모의 작업을 지원하기 위해 Git에 계속 기여할 방법도 다룹니다.
Codex는 엔지니어가 훨씬 더 많은 작업을 병렬로 수행할 수 있게 하며 OpenAI의 개발 규모와 속도를 바꾸었습니다. 이는 더 많은 브랜치와 푸시, 더 많은 병합, 그리고 개발 환경과 연구 작업, CI에서 발생하는 더 많은 페치와 클론으로 이어집니다.
이런 수요는 서로 맞물려 더욱 커집니다. 이제 엔지니어 한 명이 여러 변경 사항을 동시에 CI에 넘길 수 있고, 각 푸시는 여러 작업이 같은 커밋을 가져오게 할 수 있습니다.
지난해 말 무렵부터 모노리포를 지원하는 시스템이 이 속도를 따라가기 어려워졌습니다. 페치와 푸시가 느려지고 병합이 실패했으며, 신뢰성이 90% 아래로 떨어지는 때도 있었습니다. Git 자체의 한계에 도달한 것일까요, 아니면 모노리포를 서비스하는 방식의 한계였을까요? Git은 이메일로 커널 패치를 주고받던 커뮤니티에서 탄생했습니다. 20년 넘게 개발자들은 소프트웨어를 만드는 방식이 변할 때마다 Git을 중심으로 도구를 발전시켜 왔습니다. 저희 모노리포에서도 불편이 나타나기 시작했을 때, 그동안 쌓인 성과를 계속 활용할 수 있다는 점이 중요했습니다.
익숙한 워크플로는 유지하되, 서로 다른 브랜치에 대한 푸시를 병렬 처리하고 여러 읽기 클라이언트에 같은 이력을 동시에 제공하도록 백엔드를 재구축할 길이 보였습니다. 소스 관리 워크플로를 Git 위에 구축하면 클라이언트 개선 사항이 OpenAI 외부에도 도움이 될 수 있습니다. 저희는 자체 Git 사본(새 창에서 열기)에 적용한 개선 사항을 업스트림에 기여해 더 넓은 커뮤니티와 공유하고자 했습니다.
소스 관리 시스템의 확장에는 두 가지 핵심 축이 있습니다. 많은 쓰기 클라이언트가 서로 다른 브랜치에 푸시하는 것, 그리고 많은 읽기 클라이언트가 같은 이력을 가져오는 것입니다. Pando는 두 가지 모두에 최적화되어 있습니다.
첫째, Pando는 각 브랜치의 업데이트를 독립적으로 조율합니다. 서로 무관한 브랜치의 업데이트는 병렬로 진행할 수 있으며, 브랜치를 가져오는 클라이언트는 해당 리비전에 필요한 이력을 받습니다. 대신 여러 브랜치를 조회하는 클라이언트는 각기 다른 시점의 상태를 볼 수 있습니다.
둘째, Pando는 클라이언트별로 팩파일을 생성하는 대신 여러 페치에서 팩파일(여러 객체를 함께 압축하는 Git 형식)을 재사용합니다. 클라이언트가 이미 가진 객체를 일부 다운로드할 수는 있지만, 서버는 적은 연산으로 여러 클라이언트에 동일한 바이트를 제공할 수 있습니다.
이러한 선택 덕분에 여러 브랜치에 걸쳐 쓰기를 확장하고 캐시에서 읽기 요청을 처리할 수 있습니다. 이 두 방식이 어떻게 맞물리는지 이해하기 위해, 먼저 Git이 이력을 저장하고 전송하는 방식을 살펴보겠습니다.
Git은 리포지터리의 두 구성 요소인 객체와 참조를 동기화할 수 있어야 합니다.
Git 리포지터리는 파일 스냅샷과 이를 연결하는 이력을 객체 그래프로 기록합니다. 객체는 블롭, 트리, 커밋, 태그 중 하나입니다. 블롭은 개별 파일의 내용을 담고, 트리는 이름을 블롭이나 다른 트리와 연결해 디렉터리 구조를 형성합니다. 커밋은 루트 트리와 0개 이상의 부모 커밋을 가리키며 리포지터리의 이력을 형성합니다.
객체는 유형과 내용의 해시로 식별됩니다. 객체는 변경할 수 없습니다. 변경된 파일을 기록하면 새 블롭이 생성되고, 변경되지 않은 파일과 디렉터리는 기존 객체를 재사용할 수 있습니다.
브랜치와 태그는 커밋을 가리키는 참조, 즉 ‘ref’입니다. 브랜치에서 커밋하면 새 커밋이 생성되고 브랜치의 참조가 이를 가리키도록 업데이트됩니다.
Git 객체는 ID로 서로를 참조합니다. 커밋은 트리를, 트리는 다른 트리와 블롭을 참조합니다. refs/heads/main 브랜치는 커밋 C2를 가리키며, C2의 부모는 C1입니다. C1은 루트 트리 T1을 가리키며, T1의 parser.py는 블롭 B1을 참조합니다. C2는 루트 트리 T2를 가리키며, T2의 parser.py는 블롭 B2를 참조합니다. 두 루트 트리에서 docs/는 같은 트리인 T3를 참조합니다. T3의 README.md 항목은 블롭 B3를, usage.md는 블롭 B4를 가리킵니다.
Git은 객체를 ‘개별 저장’ 또는 ‘팩 저장’ 상태로 보관합니다. 개별 저장 객체는 객체 ID로 식별되는 별도 파일에 저장됩니다. 팩 저장 객체는 팩파일에 함께 저장되므로, 유사한 객체를 서로에 대한 델타로 저장할 수 있습니다.
Git은 블롭을 별도 파일로 저장하거나, 한 블롭을 다른 블롭에 대한 델타로 인코딩하여 함께 패킹할 수 있습니다.
Git에서 커밋의 부모, 트리, 블롭으로 이어지는 링크를 따라가 객체를 찾을 수 있다면, 그 객체는 해당 커밋에서 도달 가능하다고 합니다. 페치 중에 클라이언트는 원하는 커밋과 이미 가진 커밋을 서버에 알립니다. 서버는 원하는 커밋에서 도달 가능한 객체를 찾고, 클라이언트가 이미 갖고 있다고 아는 객체를 제외한 뒤, 나머지를 Git이 디스크에서 사용하는 것과 같은 형식의 팩파일로 보냅니다. 전체 클론은 객체가 전혀 없는 상태에서 시작하므로, 복제할 이력에서 도달 가능한 모든 객체가 필요합니다.
푸시는 서버에 하나 이상의 참조를 업데이트하도록 요청하면서 반대 방향으로 객체를 전송합니다. 클라이언트는 서버가 알린 참조 집합을 활용해 서버가 이미 가진 객체를 보내지 않습니다. 따라서 전송되는 팩은 기존 객체에 의존할 수 있으며, 팩만 수신해서는 새 이력을 재구성할 수 없습니다.
Git 리포지터리를 복제한다는 것은 여러 서버에서 객체와 참조를 모두 사용할 수 있게 유지한다는 뜻입니다. 복제본이 참조의 새 값을 제공하려면, 먼저 그 값에서 도달 가능한 객체를 제공할 수 있어야 합니다. 서비스는 경합하는 참조 업데이트도 조율해야 합니다. 즉, 같은 브랜치를 동일한 이전 끝 커밋에서 서로 다른 새 커밋으로 옮기려는 푸시를 조율해야 합니다. 리포지터리의 모든 쓰기에 단일 순서를 강제하는 시스템에서는 서로 무관한 브랜치의 푸시도 서로를 기다리게 될 수 있습니다.
Pando를 설계하면서 Git 기반 소스 관리의 일반적인 확장 방식을 검토했습니다. 하나는 3단계 커밋(3PC, 여러 노드에 걸쳐 커밋을 조율하는 프로토콜)으로 리포지터리 복제본 간의 참조 업데이트를 조율하는 방식이고, 다른 하나는 블롭 스토리지의 공유 선행 기록 로그(WAL, 변경 사항을 적용하기 전에 남기는 기록)로 업데이트 순서를 정하는 방식입니다.
저희가 모델링한 3단계 커밋 설계에서는 Git 서비스가 여러 복제본에 리포지터리의 완전한 사본을 유지합니다. 어느 서버든 푸시를 수신하면, 수신한 팩파일을 복사해 모든 복제본에 병렬로 전송합니다. 푸시를 반영하려면 과반수의 복제본이 참조 업데이트를 수락해야 합니다. 이를 관리하는 서버를 코디네이터라고 하며, 코디네이터와 각 복제본 사이에 여러 차례 통신이 필요합니다.
각 복제본은 참조 업데이트를 수락하기 위해 자신의 참조 사본을 잠그고, 여전히 클라이언트가 예상하는 커밋을 가리키는지 확인합니다. 그런 다음 코디네이터는 수락한 복제본에 사전 커밋 메시지를 보냅니다. 과반수가 사전 커밋 메시지를 확인하면, 코디네이터는 업데이트를 수행하도록 지시합니다. 이후 최신 상태인 복제본은 자신의 로컬 리포지터리 사본에서 브랜치의 새 끝 커밋을 알릴 수 있습니다.
완전한 복제본을 유지하면 로컬에서 읽기 요청을 처리할 수 있고, 클라이언트가 어느 복제본을 선택하든 모든 참조의 일관된 상태를 받도록 보장할 수 있습니다. 그 결과 참조는 한 번에 하나씩 업데이트되며, 각 푸시에서 복제본 간 참조 업데이트를 조율하려면 여러 차례의 왕복 통신이 필요합니다.
정족수의 응답을 기다려야 하므로, 복제본의 디스크 성능과 통신 지연 시간이 푸시 처리 시간에 영향을 미칩니다. 쓰기량이 증가하면 복제본은 읽기 트래픽뿐 아니라 유입되는 팩도 제때 처리해야 합니다.
저희가 모델링한 WAL 기반 설계에서는 Git 서비스가 블롭 스토리지의 공유 매니페스트를 사용해 리포지터리 전체의 업데이트 순서를 정합니다. 서버가 푸시를 수신하면 팩파일을 블롭 저장소에 업로드하고, 로컬 리포지터리 사본에서 참조 업데이트를 준비합니다. 푸시를 반영하려면 블롭 저장소의 공유 매니페스트에 참조 업데이트를 기록하고 팩파일을 가리키는 항목을 추가합니다. 그런 다음 Git 서버는 비교 후 교환 연산으로 매니페스트를 업데이트합니다. 이 연산은 매니페스트를 원자적으로 교체하거나 변경을 거부합니다. 그사이 다른 쓰기 클라이언트가 매니페스트를 업데이트했다면, 서버는 중간에 추가된 항목을 읽고 자신의 참조 업데이트가 여전히 유효한지 확인한 뒤 이미 업로드한 팩파일로 재시도합니다.
쓰기 클라이언트들은 팩파일을 병렬로 업로드할 수 있지만, 서로 무관한 브랜치에 푸시하더라도 같은 매니페스트를 업데이트해야 합니다. 비교 후 교환 연산이 성공하는 데 걸리는 왕복 시간은 리포지터리가 새 매니페스트를 게시할 수 있는 빈도를 제한합니다. 지연 시간이 짧은 블롭 저장소를 사용하면 이 단계를 빠르게 처리할 수 있습니다. 일괄 처리를 이용하면 매니페스트 하나로 여러 참조를 동시에 업데이트할 수 있지만, 업데이트를 대기열에 쌓아야 하므로 지연 시간이 늘어납니다.
블롭 저장소는 팩파일을 병렬로 수신할 수 있지만, 공유 매니페스트의 업데이트는 여전히 서로 경합합니다. 블롭 저장소의 용량만 늘려서는 이 병목을 해소할 수 없습니다. 각 복제본은 로컬에서 팩을 제공하기 전에 여전히 블롭 저장소에서 팩을 읽고 인덱싱해야 합니다. 쓰기 유입 속도가 이 작업 속도를 앞서면 복제본이 뒤처지고, 클라이언트는 새로 푸시된 커밋을 가져오기까지 더 오래 기다려야 합니다.
바로 여기서 Pando가 등장합니다. Pando는 많은 클라이언트의 동시 푸시를 수용하도록 확장하는 것을 목표로 하는, Git 리포지터리 서비스용 새 아키텍처입니다. 작동 방식은 다음과 같습니다.
Pando를 개발하기 전부터, Git을 그대로 유지하면서도 요구 사항을 충족할 수 있으리라는 예감이 있었습니다. 다만 대부분의 Git 시스템이 필수로 여기는 몇 가지 조건은 양보해야 했습니다.
리포지터리 전체의 일관성: 모든 참조에 대해 특정 시점의 일관된 스냅샷을 제공하려면 리포지터리 전반에 걸친 조율이 필요합니다. 모든 참조 업데이트를 하나의 순서로 처리하는 시스템에서는 서로 무관한 브랜치에 대한 푸시도 단일 조율 지점에서 경합합니다.
클라이언트별 전용 팩파일: 서로 다른 로컬 이력을 가진 두 클라이언트가 같은 브랜치를 가져올 때, 이미 보유한 객체를 보내지 않으려면 서버가 각각 다른 팩을 생성해야 할 수 있습니다. 이런 응답을 생성하려면 CPU와 디스크 I/O 자원이 필요하므로, 대규모 읽기 클라이언트를 수용하도록 확장하기 어렵습니다.
대신 Pando는 완화된 일관성 모델인 인과적 일관성으로 쓰기를 확장하고, 팩파일을 재사용해 읽기를 확장합니다.
클라이언트가 브랜치에 푸시하면 Pando는 수신한 팩파일을 블롭 저장소에 불변 블롭으로 저장합니다. 브랜치가 이동할 때마다 Pando는 새 끝 커밋, 해당 커밋을 제공한 팩, 그 팩이 의존하는 이전 참조 버전을 담은 참조 버전을 메타데이터 저장소에 기록합니다. 블롭 저장소와 메타데이터 저장소는 모두 수평 확장이 가능합니다.
Pando에서 커밋이나 참조를 가져올 때는 관련 참조 버전(팩파일)을 순서대로 클라이언트에 전달하여, 해당 참조 버전을 생성한 클라이언트의 상태를 재구성해야 합니다. 이 방식은 리포지터리 전체에는 더 약한 형태의 일관성을 제공하면서도, 개별 참조의 업데이트에는 직렬 가능성을 보장합니다. 클라이언트 간 충돌은 같은 참조에 동시에 푸시할 때만 발생합니다. 여러 브랜치를 읽는 클라이언트는 그 결과가 리포지터리 전체의 동일 시점을 나타낸다고 가정할 수 없습니다.
트렁크에 푸시하면 하나 이상의 커밋이 담긴 증분 팩이 추가됩니다. 곁가지 브랜치에 푸시할 때마다 해당 토픽의 이전 커밋도 다시 포함하므로, 서버가 알린 트렁크 버전을 가진 클라이언트는 팩 하나로 토픽을 가져올 수 있습니다.
이는 인과적 일관성의 한 형태입니다. 참조를 가져오려면 읽기 클라이언트는 해당 참조를 생성한 쓰기 클라이언트의 이력을 보게 됩니다. 푸시 중에 Pando는 클라이언트와 서버가 합의하는 공통 이력을 제어하여, 클라이언트가 더 쉽게 공유할 수 있는 팩파일을 생성하도록 합니다.
따라서 Pando는 Git 작업 중 모든 참조의 특정 시점 상태나, 클론 또는 페치 내에서 참조 업데이트의 순서를 제공하지 않습니다. 구체적으로, 전체 클론의 협상 과정에서 foo와 bar 브랜치가 모두 업데이트된다고 가정해 보겠습니다. 협상 중에 한 클라이언트는 foo의 버전 1과 bar의 버전 2를 보는 반면, 다른 클라이언트는 foo의 버전 2와 bar의 버전 1을 볼 수 있습니다. 실제로 사용자와 에이전트는 참조 업데이트의 전역 순서에 의존하지 않으며, 개별 참조의 업데이트 순서에 의존합니다.
세 가지 일관성 모델을 비교하는 이 시뮬레이션에서는 클라이언트 1,000개가 각각 서로 다른 브랜치를 선택해 고유한 커밋을 반복해서 푸시하도록 구성하고, 전체 경과 시간과 푸시 후 페치까지의 지연 시간을 측정합니다. 후자는 사용자와 에이전트가 협업하거나 버전 관리에 의존하는 다른 시스템을 사용할 때의 왕복 시간을 측정하므로 워크로드에 중요한 지표입니다. 시뮬레이션은 범용부터 고급 사양까지 다양한 가상 머신과 블롭 스토리지 시스템을 모델링합니다. 이 시뮬레이션에서는 3단계 커밋의 리포지터리 전체 일관성 조건을 완화했습니다.
Simulating Git Server Implementations
Follow each writer through repeated pushes, and separately measure when an independent reader can check out each commit.
Pando를 다른 두 가지 방식과 비교하는 시뮬레이션을 실행해 보세요. 범용부터 고급 하드웨어까지 인프라와 클라이언트 수 등 여러 설정을 선택해 결과를 비교해 보세요.
모델링한 3단계 커밋 시스템은 부하가 걸리면 디스크가 포화되어 쓰기 지연 시간이 크게 늘어납니다. 한편 선행 기록 로그 시스템은 같은 이유로 서버의 저널 읽기가 작업량을 따라가지 못해 읽기 지연 시간이 크게 늘어납니다. 모노리포를 대상으로 한 실제 테스트와 이 시뮬레이션 모두에서, Pando는 부하 상황에서도 성능 저하 없이 수천 개가 넘는 클라이언트의 동시 푸시를 지원합니다.
Pando는 푸시를 처리할 때 수신한 팩파일을 로컬 리포지터리에 풀지 않고, 블롭 스토리지로 스트리밍하면서 검증합니다. 클라이언트가 페치를 시작하면 Pando가 저장된 팩 중 필요한 팩을 선택하고, 클라이언트가 이를 다운로드합니다. 어떤 서버도 팩파일을 로컬 리포지터리에 적용하거나 비용이 큰 언패킹 및 패킹 작업을 수행할 필요가 없습니다. Pando 서버는 주로 협상과 팩파일 선택을 수행하므로 범용 하드웨어에서 실행할 수 있습니다.
덕분에 Pando는 클라이언트, 클라이언트와 Pando 사이의 네트워크, 블롭 스토리지 서비스가 허용하는 한계까지 확장할 수 있습니다. 용량이 충분하다면 서로 독립적인 브랜치에서 작업하는 클라이언트는 각자의 연결이 허용하는 최대 속도로 팩을 전송할 수 있습니다. 더 많은 클라이언트에 같은 속도를 제공하려면 Pando 서버를 추가하고 메타데이터 저장소와 블롭 저장소도 함께 확장하면 됩니다.
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.
에이전트가 로컬에서 파일을 수정하고 커밋을 생성한 뒤 git push를 실행하면, 브랜치를 업데이트하기 위해 변경 사항을 담은 팩파일이 생성됩니다. Pando는 팩을 수신하고 블롭 스토리지로 스트리밍합니다. Pando는 한 차례의 디코딩으로 수신한 푸시를 검증한 뒤, 다중 키 트랜잭션을 사용해 메타데이터 저장소에 참조 버전을 기록하고 참조 포인터를 원자적으로 업데이트합니다. 이를 통해 Pando는 ‘자신이 쓴 내용은 즉시 읽을 수 있는’ 일관성을 제공합니다. 이 전체 쓰기 작업에 필요한 메타데이터 읽기와 쓰기 횟수는 입니다.
다른 클라이언트가 브랜치를 가져오면 Pando는 해당 클라이언트가 이미 가진 이력을 기준으로 저장된 팩 집합을 선택합니다. 이 작업에는 번의 메타데이터 읽기가 필요하며, 는 클라이언트가 가진 상태와 원하는 상태 사이의 최단 거리를 참조 버전 수로 나타낸 값입니다. 서버는 팩파일 URI를 반환하고, 클라이언트는 팩파일을 지역별 캐시에 저장하는 Express 서비스를 통해 이를 다운로드합니다.
이 설계는 Pando 아키텍처의 세 가지 핵심 서비스인 푸시, 페치, 팩 병합을 기반으로 합니다.
클라이언트가 푸시할 때 기존 Git 서버는 패를 모두 보여주는 포커 플레이어처럼, 보유한 모든 브랜치를 클라이언트에 공개합니다. 그러면 클라이언트는 커밋, 트리, 블롭을 모아 서버가 이미 갖고 있다고 아는 객체를 제외한 팩파일을 생성합니다.
더 많은 참조를 알리면 푸시 크기를 줄일 수 있습니다. 하지만 다른 클라이언트에는 쓰기 클라이언트가 제외한 객체가 없을 수 있으므로, 팩을 재사용하기가 더 어려워질 수도 있습니다.
Pando는 다른 방식을 택합니다. 패를 전부 공개하는 대신, 선택한 브랜치만 클라이언트에 알립니다. 그 이유는 두 가지입니다.
모든 참조의 일관된 상태를 제공하려면 비용이 많이 듭니다. 브랜치가 수백만 개에 이르면 참조를 알리는 데이터만 100 MiB를 넘을 수 있습니다. 이를 제공하려면 각 서버가 상당한 조율 오버헤드를 감수하며 최신 매핑을 유지하거나, 참조 수에 비례하는 비용의 데이터베이스 쿼리를 실행해야 합니다.
서버가 일부 참조만 클라이언트에 알려주면, 클라이언트는 서버가 명시적으로 제외해도 된다고 허용한 객체만 팩파일에서 제외합니다. 따라서 생성된 팩파일은 해당 객체에만 의존하게 됩니다. Pando는 이 의존성을 추적하므로, 클라이언트가 푸시에서 생성한 팩파일을 이후 페치의 응답으로 제공해 재사용할 수 있습니다.
Git 서버가 더 많은 참조를 알릴수록 푸시하는 클라이언트는 객체를 제외할 근거가 늘어나 더 작은 팩을 생성할 수 있습니다. 하지만 이런 팩파일을 재사용하려면 페치하는 클라이언트가 각 의존 객체를 보유하고 있음을 서버가 확인할 수 있어야 합니다. 그렇지 않으면 팩파일에서 클라이언트에 필요한 객체가 빠질 수 있습니다.
반대로 Git 서버가 참조를 하나도 알리지 않으면 클라이언트는 리포지터리의 전체 이력이 담긴 팩파일을 생성합니다. 이 팩파일은 다른 모든 클라이언트가 사용할 수 있지만, 어떤 브랜치를 가져오든 사실상 전체 클론을 수행하게 된다는 큰 비용이 따릅니다.
더 많은 참조를 알릴수록 팩파일이 작아집니다. 참조 하나만 알려도 크기 감소 효과의 대부분을 얻을 수 있습니다.
기본적으로 클라이언트가 새 기능 브랜치를 푸시하면 Pando는 지정된 트렁크 브랜치 하나만 기준으로 알립니다. 이렇게 하면 리포지터리의 브랜치가 늘어나도 알림 데이터의 크기는 일정하게 유지되지만, 그에 따른 대가도 있습니다. 엔지니어가 하나의 브랜치에 여러 번 푸시하거나 기능 브랜치 위에 또 다른 기능 브랜치를 쌓으면, 클라이언트는 트렁크에 없는 모든 내용을 다시 보내야 합니다.
그 결과 브랜치에 연속해서 푸시할수록 중복될 수 있는 객체가 포함되어 크기가 커지고, 클라이언트 대역폭도 더 많이 사용됩니다. 저희 모노리포에서 이 방식으로 생성한 팩파일 크기의 중앙값은 20 KiB 미만이었고, 99번째 백분위수는 3 MiB 미만이었습니다. 서버가 알린 트렁크 이력을 이미 가진 읽기 클라이언트라면 팩파일 하나로 브랜치를 가져올 수 있으므로, 저희에게는 합리적인 절충안이었습니다.
Pando는 클라이언트에 알릴 참조 집합을 선택한 뒤 푸시를 수락하고 메타데이터 저장소를 업데이트합니다. Pando는 메타데이터 업데이트를 동기 부분과 비동기 부분으로 나눕니다. 동기 메타데이터 업데이트는 클라이언트가 새로 업데이트된 커밋을 즉시 가져올 수 있도록 보장합니다. 이 단계에서는 새 끝 커밋, 팩파일 협상에 사용한 의존성, 푸시된 팩파일의 링크를 저장하는 새 참조 버전을 기록하고, 참조 포인터가 새 버전을 가리키도록 매핑을 업데이트합니다.
동시에 팩 병합 예약과 같은 비동기 작업도 같은 트랜잭션에 기록합니다. 매우 큰 푸시의 경우 커밋의 일부만 동기적으로 기록합니다. 덕분에 푸시 크기와 관계없이 메타데이터 저장소의 읽기와 쓰기 횟수를 상수로 유지할 수 있습니다.
중요한 점은 Pando 서버가 서로 무관한 참조에 대한 푸시를 독립적으로 수락할 수 있다는 것입니다. 경합이 발생하는 키는 참조 포인터뿐이며, 원자적인 비교 후 교환과 유사한 연산으로 이를 보호합니다. Pando 서버, 메타데이터 저장소의 확장 단위, 또는 블롭 저장소를 추가해 쓰기 처리량을 늘릴 수 있습니다.
클라이언트가 페치를 수행할 때 서버에 커밋 ID 집합 두 개를 제공합니다. 프로토콜에서는 이를 have와 want라고 합니다. 기존 Git 서버는 have에서 도달할 수 있는 객체와 want에서 도달할 수 있는 객체 간의 차집합을 계산한 뒤 압축하여 클라이언트에 보냅니다. 오픈 소스 Git 서버에는 이 작업을 가속하는 다양한 로컬 인덱스가 있지만, 이러한 팩파일은 생성에 많은 연산이 필요할 수 있고 대체로 캐싱하기도 어렵습니다.
대신 Pando는 푸시할 때 기록한 커밋 매핑으로 클라이언트의 have와 want를 조회하고, 해당 커밋이 포함된 참조 버전 집합을 찾습니다. 각 참조 버전은 Pando가 이전에 수신한 팩파일과 연결되어 있습니다. 서버는 이를 바탕으로 클라이언트에 필요한 객체 집합을 포괄하는 팩파일을 선택합니다.
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.
Fetch 27 trunk packs.
Select a branch to include its trunk dependencies.
Pando는 Git 프로토콜의 have와 want를 일련의 참조 버전에 매핑합니다. 곁가지 브랜치를 요청한다는 것은 해당 브랜치가 의존하는 트렁크 이력까지 요청한다는 뜻입니다.
Git 프로토콜에서는 클론이나 페치의 HTTP 응답으로 팩파일 하나만 보낼 수 있습니다. 하지만 비교적 덜 알려진 Git 프로토콜 기능인 packfile-uri를 사용하면, 서버가 추가 팩을 별도로 다운로드할 수 있는 링크를 반환할 수 있습니다. Pando는 이 방식만 사용하며, 인라인으로는 빈 팩파일을 전송합니다. 이 기능을 지원하기 위해 OpenAI는 packfile-uri의 여러 개선 사항을 업스트림에 기여했습니다.
OpenAI 시스템은 여러 지역에서 운영되므로, 각 지역에 로컬 캐시를 갖춘 Pando 클러스터를 유지합니다. 팩 전송 서비스인 Express는 블롭 저장소를 캐싱하고, 별도 서비스가 메타데이터 저장소를 캐싱합니다. 이 캐시들 덕분에 Pando는 커밋 ID 요청이나 특수한 참조 읽기 요청을 해당 클러스터에 캐싱된 상태만으로 처리할 수 있습니다. 일관된 해싱을 사용해 팩파일 요청을 Express 서버로 라우팅하며, 캐시 적중률은 99%를 넘습니다.
이 캐시 시스템은 클라이언트와 Pando 사이의 네트워크 한계까지 활용할 수 있을 만큼 매우 높은 처리량을 제공합니다. 이러한 팩파일 URI와 로컬 캐시를 활용하면 클라우드 제공업체의 표준 가상 머신만으로도 클론과 페치 작업이 10 Gbps 네트워크 링크를 최대한 활용할 수 있음을 확인했습니다.
대부분의 Git 서버에서는 커지는 리포지터리의 성능을 유지하기 위해 리패킹과 가비지 컬렉션이 필요합니다. 두 작업 모두 공간을 회수하지만 연산과 I/O 부하가 큽니다.
OpenAI의 모노리포처럼 빠르게 변하는 리포지터리에서는 마지막 페치 이후 발생한 모든 푸시에 대해 개별 팩파일 URI를 보내는 것이 현실적으로 어렵습니다. 클라이언트가 트렁크를 최신 상태로 유지하려면 매일 수만 개의 팩파일을 다운로드해야 하기 때문입니다.
대신 Pando는 로그 구조 병합 트리의 병합 압축과 유사한 설계를 채택해, 백그라운드에서 팩을 합쳐 이 오버헤드를 줄입니다. 각 푸시는 Pando에서 ‘L0’라고 부르는 팩파일을 생성하며, 이는 Pando가 전송할 수 있는 가장 작은 데이터 단위입니다. 시간이 지나면서 백그라운드 워커는 이 팩파일들을 연속된 참조 버전 범위를 포괄하는 커버로 모읍니다. 커버의 규모는 L0부터 기하급수적으로 커집니다. 현재 저희 모노리포는 최대 L4 팩파일 커버를 사용합니다.
이 커버는 두 가지 역할을 합니다. 커버가 충분히 차면 병합 워커가 이를 맡아 하나의 팩파일을 생성합니다. 팩파일 내부의 객체를 재배치하여 압축률을 높입니다. 동시에 참조 버전에 대한 스킵 리스트 역할을 해 페치 협상 성능도 개선합니다. Pando는 각 레벨의 인덱스를 동시에 탐색하여 로그 시간 안에 클라이언트에 보낼 팩 집합을 선택할 수 있습니다. 이 팩파일 커버는 리패킹 여부와 관계없이 더 효율적인 페치 계획을 가능하게 하여, Pando에 요구되는 처리량과 지연 시간의 부담을 모두 줄입니다.
Compaction in Pando
Small packfiles are grouped into larger covers across levels, improving compression and speeding up fetches.
팩 병합은 여러 참조 버전의 팩파일을 백그라운드에서 합쳐, 더 적은 파일로 더 효율적인 페치를 가능하게 합니다.
많은 Git 서버와 달리, 모든 레벨의 팩파일을 유지하며 폐기하거나 가비지 컬렉션으로 제거하지 않습니다. 클라이언트가 페치에서 요청한 have와 want에 따라 모든 레벨의 팩이 지속적으로 클라이언트에 제공됩니다. 또한 Pando를 API로 사용하는 클라이언트가 팩파일에서 임의의 객체를 읽을 때는 L4보다 L0에서 읽어야 할 데이터가 더 적은 경향이 있습니다. L0에서는 해당 객체가 압축되어 있을 가능성이 더 낮기 때문입니다.
Pando의 팩 병합은 클라이언트에 전송하는 전체 데이터량을 크게 줄입니다. 또한 클라이언트가 최신 이력에서 많이 뒤처져 있더라도, 전송할 팩파일을 선택하는 데 드는 비용이 뒤처진 정도에 비례해서 늘어나지 않도록 합니다. 이는 규모가 크고 빠르게 변하는 리포지터리에서 특히 중요하며, 저희는 이런 리포지터리가 소프트웨어 개발의 새로운 표준이 될 것으로 예상합니다.
저희는 Codex로 Pando를 구축해, 착수부터 프로덕션 배포까지 단 10주 만에 마쳤습니다. Pando의 핵심은 엔지니어 한 명이 설계하고, 에이전트 팀들이 구현했으며, 소수의 엔지니어가 배포했습니다. Pando는 올해 4월부터 프로덕션 트래픽을 처리하기 시작했으며, OpenAI에서 사용하는 더 큰 소스 관리 도구 체계의 일부입니다.
Pando의 설계를 검증할 때는 매우 다양한 접근 방식을 활용했습니다. 저희는 더 안전한 소프트웨어 기반 구축 원칙을 적용했습니다. 처음부터 새로 시작하는 프로젝트였기에, 레거시 의존성 없이 Rust로 Pando를 구현하기로 했습니다. 에이전트가 변경 사항을 표준 Git 동작과 비교해 테스트할 수 있도록 적합성 테스트 모음을 작성했습니다. 또한 시스템의 불변 조건과 속성을 검사할 수 있는 결정론적 시뮬레이션 테스트 하네스를 개발해, 협상과 팩 계획 과정의 미묘한 경쟁 상태와 회귀 문제를 찾아 완화할 수 있었습니다.
많은 모델이 이 시스템에 기여했으며, 새로운 모델이 나올 때마다 이를 활용해 Pando의 성능과 신뢰성을 높였습니다. 에이전트는 GPT‑6 Astra를 사용해 Lean과 TLA+로 ‘Pando 이론’을 형식화했습니다. 저희는 이를 활용해 구현상의 버그를 찾아 수정하고 시스템의 속성을 증명했습니다. 여러 세션이 수백 시간 동안 연속으로 실행되며 Pando의 예외적인 상황을 찾아 수정했습니다. 또한 Daybreak 기능을 갖춘 모델로 에이전트를 24시간 가동해 Git과 Pando의 레드팀 테스트를 수행합니다.
처음에는 Pando를 기존 소스 관리 시스템에 읽기와 쓰기를 전달하는 프록시로 배포했습니다. 덕분에 Pando와 이전 시스템을 동기화한 상태로 유지하며, 중단 없이 트래픽을 Pando로 안전하게 옮길 수 있었습니다.
Pando는 캐싱 설계의 일부로, 요청을 프록시로 전달하기 전에 중복을 제거하고 합칠 수도 있습니다. 이를 통해 시스템을 하나씩 Pando로 옮기는 동안에도 기존 도구를 안정적으로 운영할 수 있었습니다. 더 많은 워크로드를 Pando로 옮길수록 해당 워크로드의 푸시가 Pando의 캐시를 채웠고, 이전 백엔드의 신뢰성도 높아졌습니다.
Pando를 사용할 때의 성능을 최적화하기 위해, 저희의 공개 Git 배포판을 개발자 컴퓨터와 ChatGPT 데스크톱 앱에 제공하기 시작했습니다. 클라이언트를 Pando로 이전할 때는 Git CLI를 감싼 내부 래퍼를 사용해 단계적으로 진행했습니다. 변경 요청과 코드 리뷰를 처리하는 여러 도구와 자동화용 API도 만들었습니다. Git 트래픽과 함께 레거시 API, 웹훅, 리포지터리 다운로드에 의존하던 수백 개의 연동도 이전했습니다.
Codex 자체도 개발자와 팀의 이전을 도왔습니다. 기존 체크아웃을 준비하고 에이전트가 새 시스템을 사용하도록 안내하는 스킬을 만들었습니다. 그 결과 개발자는 Codex에 개발 환경과 서비스를 이전해 달라고 요청하기만 하면 되었습니다.
Pando로 전환하면서 병합 신뢰성이 높아졌고, 푸시 지연 시간의 중앙값은 거의 80% 줄었습니다.
Pando는 소수의 CPU 코어만으로 하루에 1페타바이트의 Git 데이터를 제공할 수 있습니다. Pando는 한 시간에 57테라바이트가 넘는 Git 팩을 제공한 적이 있으며, 그중 99% 이상의 바이트가 로컬 디스크 캐시에서 제공되었습니다.
팩을 재사용하면 서비스 노드에서 적은 연산만으로도 여러 클라이언트가 동일한 리포지터리 이력을 다운로드할 수 있습니다. 이 사례는 하나의 리포지터리를 보여주지만, Pando는 훨씬 더 많은 리포지터리를 지원하도록 설계되었습니다. 수요가 늘어나면 데이터 전송과 Git 협상을 각각 독립적으로 확장할 수 있습니다.
Git 프로토콜과 클라이언트를 기반으로 Pando를 구축하면서 버그와 Git 자체의 성능을 개선할 기회를 발견할 수 있었습니다.
예를 들어 Pando는 재사용 가능한 팩을 클라이언트에 직접 제공하므로, 팩 다운로드와 인덱싱이 시스템의 속도와 신뢰성을 높이는 핵심이 되었습니다. 그 과정에서 동시 다운로드, 중단된 전송의 정리, 이미 완료된 다운로드의 재개와 관련된 예외 상황이 드러났습니다. 저희는 해당 처리 경로를 수정하고 페치에 기존 팩을 사용할 때의 검증 절차를 추가했으며, 그 결과를 업스트림에 기여했습니다.
시간과 대역폭이 어디에 쓰이는지 파악할 필요도 있었습니다. 그래서 packfile-uri 다운로드 추적과 팩 생성 시 바이트 수 집계를 추가해, 느린 페치가 팩 생성에 시간을 쓰는지 전송에 시간을 쓰는지 쉽게 구분할 수 있도록 했습니다.
OpenAI의 Git 빌드(새 창에서 열기)에서는 클라이언트가 독립적인 팩을 병렬로 다운로드하고 인덱싱할 수 있습니다. 또한 ‘명시적 have’ 개념을 추가해, 서버가 브랜치의 현재 끝 커밋을 푸시 클라이언트가 팩에서 제외해도 되는 이력과 별도로 알릴 수 있게 했습니다. Pando에서는 이를 통해 클라이언트가 --force-with-lease로 기능 브랜치가 이동하지 않았는지 확인하면서, 트렁크만을 기준으로 팩을 생성할 수 있습니다. 그러면 해당 트렁크 이력을 가진 클라이언트 누구나 같은 팩으로 브랜치를 가져올 수 있습니다.
이 개선 사항 중 일부는 이미 업스트림 Git에 반영되었으며, 그 밖의 많은 개선 사항은 저희 공개 리포지터리에 있습니다. 이러한 개선이 Pando 밖에서도 유용하도록 Git 커뮤니티와 계속 협력하고 있습니다. 진행 상황은 공개 패치 트래커(새 창에서 열기)에서 확인할 수 있습니다.
Pando는 Git 리포지터리 확장에 관한 통념을 재검토함으로써, 엔지니어와 에이전트가 이미 사용하는 Git 워크플로를 바꾸지 않고도 모노리포를 확장할 수 있게 해줍니다. Pando를 통해 모노리포에서 더 많은 작업을 병렬로 처리하고, 병합 신뢰성을 높이며, 코드 변경 사항을 게시하는 데 걸리는 시간을 줄일 수 있었습니다. 모노리포 외에도, 수많은 에이전트가 공유 Git 리포지터리에서 협업해야 하는 워크로드를 비롯해 OpenAI 내부에서 Pando를 활용할 다른 방안을 모색하고 있습니다.
엔지니어와 에이전트가 더 많은 소프트웨어를 만들어낼수록, OpenAI 밖에서도 더 많은 팀이 저희가 겪었던 소스 관리의 한계에 부딪히게 될 것입니다. 현재 Pando를 가능하게 하는 기술을 사용자와 공유할 방법을 모색하고 있습니다.
Pando는 Git 리포지터리 사용자와 운영자가 20년에 걸쳐 쌓아온 경험을 바탕으로 합니다. 앞으로도 Pando를 개발하는 동시에 오픈 소스 Git 커뮤니티와 협력하여, 더 많은 Git 사용자에게 도움이 되는 개선 사항을 공유하겠습니다.


