A Git skálázása az OpenAI-nál
Az ügynökök léptékéhez igazodó verziókezelés kiépítése a Git-ökoszisztéma fejlesztésével párhuzamosan.
Az elmúlt évben monorepónk mérete és a vele szemben támasztott igények elképesztő ütemben nőttek. Egyértelművé vált, hogy meg kell újítanunk verziókezelő rendszerünket, hogy lépést tarthasson a Codex által lehetővé tett párhuzamos munkavégzés mértékével.
Létrehoztuk a Pandót, a Git-adattárak új háttérrendszerét, anélkül, hogy megszakítottuk volna a fejlesztői munkát vagy megváltoztattuk volna csapatunk Git-használatát. Ma már az OpenAI monorepójában minden fejlesztés a Pandóra épül. A Pandót két ambiciózus célra terveztük. Az első a horizontálisan skálázható írás, sok ágra történő egyidejű feltöltésre optimalizálva. A második az olvasási adatátviteli egység maximalizálása, hogy a kliensek elég gyorsan tölthessék le az adatokat a rendelkezésükre álló hálózati sávszélesség teljes kihasználásához.
E célokat a Git protokolljával és kliensével való kompatibilitás megőrzésével értük el, így mérnökeink és ügynökeink zökkenőmentesen illeszthették be a Pandót meglévő munkafolyamataikba. Éles környezetben a Pando közel az ötödére csökkentette a feltöltések medián késleltetését. Ma már naponta közel egy petabájtnyi Git-adatot szolgál ki.
Bemutatjuk, miért építettünk új verziókezelő rendszert, és miért döntöttünk a Git-kompatibilitás megőrzése mellett. Összehasonlítjuk rendszerünket a Git skálázásának elterjedt megközelítéseivel, és leírjuk, miért különösen alkalmas a Pando az OpenAI skálázhatósági igényeinek kielégítésére. Kitérünk a következő lépésekre is, és arra, hogyan járulunk hozzá továbbra is a Git fejlesztéséhez az ügynökök munkájának léptékéhez igazodva.
A Codex megváltoztatta a fejlesztés léptékét és ütemét az OpenAI-nál: a mérnökök jóval több munkát végezhetnek párhuzamosan. Ez több ágat, feltöltést és egyesítést, valamint több lekérést és klónozást jelent a fejlesztői környezetek, kutatási feladatok és CI-folyamatok részéről.
Ezek az igények egymást erősítik. Egy mérnöknek ma már egyszerre több módosítása is készen áll a CI-re, és minden feltöltés több olyan feladatot indíthat el, amely ugyanazt a commitot kéri le.
Nagyjából a tavalyi év végétől a monorepónkat kiszolgáló rendszerek egyre nehezebben tartottak lépést. A lekérések és feltöltések lelassultak, az egyesítések meghiúsultak, a megbízhatóság pedig időnként 90% alá esett. Vajon magának a Gitnek a korlátját értük el, vagy annak, ahogyan a monorepónkat kiszolgáltuk? A Git egy olyan közösségből nőtt ki, amely e-mailben cserélt kerneljavításokat. A fejlesztők több mint két évtizede igazítják eszközeiket a Githez, miközben a szoftverkészítés módja folyamatosan változik. Amikor saját monorepónkban nehézségeket kezdtünk tapasztalni, fontos volt számunkra, hogy továbbra is erre a munkára építhessünk.
Láttunk lehetőséget a megszokott munkafolyamat megőrzésére úgy, hogy közben újraépítjük a háttérrendszert: az párhuzamosan kezelheti a különböző ágakra történő feltöltéseket, és egyszerre sok olvasónak szolgálhatja ki ugyanazt az előzményt. Mivel verziókezelési munkafolyamatunkat a Gitre alapoztuk, a kliens fejlesztései az OpenAI-on kívül is hasznosak lehetnek. Szerettük volna ezeket a fejlesztéseket a saját Git-változatunkból(új ablakban nyílik meg) visszajuttatni az eredeti projektbe, megosztva őket a szélesebb közösséggel.
A verziókezelő rendszerek skálázásának két alapvető dimenziója van: sok író tölt fel különböző ágakra, és sok olvasó kéri le ugyanazt az előzményt. A Pando mindkettőre optimalizál.
Először is, a Pando minden ág frissítéseit egymástól függetlenül koordinálja. A független ágak frissítései párhuzamosan haladhatnak, és az ágat lekérő olvasó megkapja az adott verzióhoz szükséges előzményt. Ennek ára, hogy a több ágat vizsgáló olvasó eltérő időpillanatokhoz tartozó állapotukat láthatja.
Másodszor, a Pando újrafelhasználja a csomagfájlokat a lekérések között, ahelyett, hogy minden kliensnek külön készítene egyet. A csomagfájl a Git több objektum együttes tömörítésére szolgáló formátuma. A kliens így néhány már meglévő objektumot is letölthet, cserébe a szerver kevés számítással sok kliensnek szolgálhatja ki ugyanazokat a bájtokat.
Ezek a döntések lehetővé teszik az írások ágak közötti skálázását és az olvasások gyorsítótárból való kiszolgálását. Hogy megértsük, hogyan kapcsolódnak egymáshoz, először nézzük meg, miként tárolja és továbbítja a Git az előzményeket.
A Gitnek az adattár két részét kell szinkronban tartania: az objektumokat és a referenciákat.
A Git-adattár a fájlok pillanatképeit és az azokat összekapcsoló előzményt objektumgráfként rögzíti. Egy objektum blob, fa, commit vagy címke lehet. A blobok egy-egy fájl tartalmát tárolják, a fák pedig neveket rendelnek blobokhoz vagy más fákhoz, így alakítva ki a könyvtárstruktúrát. A commitok egy gyökérfára és nulla vagy több szülőcommitra mutatnak, létrehozva az adattár előzményét.
Az objektumokat a típusukból és tartalmukból képzett hash azonosítja. Az objektumok megváltoztathatatlanok is: egy módosított fájl rögzítése új blobot hoz létre, míg a változatlan fájlok és könyvtárak újrafelhasználhatják a meglévő objektumokat.
Az ágak és címkék commitokra mutató referenciák, röviden „refek”. Ha egy ágon commitot készítünk, létrejön az új commit, és az ág referenciája frissül, hogy arra mutasson.
A Git-objektumok azonosító alapján hivatkoznak egymásra: a commitok fákra, a fák pedig más fákra és blobokra. A refs/heads/main ág a C2 commitra mutat, amelynek szülője C1. C1 a T1 gyökérfára mutat, amelyben a parser.py a B1 blobra hivatkozik. C2 a T2 gyökérfára mutat, amelyben a parser.py a B2 blobra hivatkozik. Mindkét gyökérfában a docs/ ugyanarra a fára, T3-ra hivatkozik. Ennek README.md bejegyzése a B3 blobra, a usage.md pedig a B4 blobra mutat.
A Git az objektumokat „különálló” vagy „csomagolt” állapotban tárolja. A különálló objektumokat egyedi fájlok tárolják, amelyeket az objektumazonosítójuk azonosít. A csomagolt objektumok közösen, egy csomagfájlban találhatók, így a hasonló objektumok egymáshoz viszonyított különbségként tárolhatók.
A Git külön fájlokban tárolhatja a blobokat, vagy közös csomagba rendezheti őket, az egyik blobot a másikhoz képest vett különbségként kódolva.
A Gitben egy objektum akkor érhető el egy commitból, ha a Git a commitból indulva, a szülőkre, fákra és blobokra mutató hivatkozásokat követve meg tudja találni. Lekéréskor a kliens közli a szerverrel, mely commitokat kéri, és melyekkel rendelkezik már. A szerver megkeresi a kért commitokból elérhető objektumokat, kizárja azokat, amelyekről tudja, hogy a kliensnél már megvannak, a maradékot pedig a Git lemezen használt formátumával megegyező csomagfájlban küldi el. A teljes klónozás meglévő objektumok nélkül indul, ezért szüksége van a klónozott előzményből elérhető összes objektumra.
A feltöltés az ellenkező irányba továbbítja az objektumokat, és egy vagy több referencia frissítését kéri a szervertől. A kliens a szerver által meghirdetett referenciák halmaza alapján kerüli el a szerveren már meglévő objektumok elküldését. Az elküldött csomag tehát meglévő objektumoktól függhet: önmagában a csomag fogadása nem elég az új előzmény rekonstruálásához.
Egy Git-adattár replikálása azt jelenti, hogy az objektumokat és a referenciákat is több szerveren tesszük elérhetővé. Mielőtt egy replika kiszolgálhatná egy referencia új értékét, képesnek kell lennie az abból elérhető objektumok biztosítására. A szolgáltatásnak a versengő referenciafrissítéseket is koordinálnia kell, vagyis azokat a feltöltéseket, amelyek ugyanazt az ágat ugyanarról a régi ágvégről különböző új commitokra próbálják átállítani. Az adattár összes írását egyetlen sorrendbe rendező rendszerekben a független ágakra történő feltöltések is várakozásra kényszeríthetik egymást.
A Pando tervezésekor megvizsgáltuk a Git-alapú verziókezelés skálázásának hagyományos megközelítéseit. Az egyik háromfázisú véglegesítéssel (3PC, a véglegesítést több csomópont között koordináló protokoll) hangolja össze az adattár replikáinak referenciafrissítéseit. A másik a blobtárolóban elhelyezett közös előzetes naplóval (WAL, a változtatásokat alkalmazásuk előtt rögzítő napló) rendezi sorba a frissítéseket.
Az általunk modellezett háromfázisú véglegesítési megoldásban a Git-szolgáltatás több replikán tartja meg az adattár teljes másolatát. Amikor bármelyik szerver feltöltést fogad, a beérkező csomagfájlt lemásolja, és párhuzamosan elküldi az összes replikának. A feltöltés közzétételéhez a replikák többségének el kell fogadnia a referenciafrissítést. Az ezt irányító szerver a koordinátor; a folyamat több kommunikációs kört igényel közte és az összes többi replika között.
A referenciafrissítés elfogadásához minden replika zárolja a referencia saját példányát, és ellenőrzi, hogy az továbbra is a kliens által várt commitra mutat-e. A koordinátor ezután előzetes véglegesítési üzenetet küld az elfogadó replikáknak. Amint a többség nyugtázta az előzetes véglegesítési üzenetet, a koordinátor utasítja őket a frissítés végrehajtására. Ezután bármely naprakész replika meghirdetheti az új ágvéget az adattár helyi másolatából.
A teljes replikák fenntartása lehetővé teszi az olvasások helyi kiszolgálását, és garantálja, hogy a kliensek minden referenciáról konzisztens nézetet kapjanak, bármelyik replikát választják is. Ennek következtében a referenciák egyesével frissülnek, és minden feltöltés több oda-vissza kommunikációs kört igényel a referenciafrissítés replikák közötti koordinálásához.
A kvórumra várakozás miatt a replikák lemezteljesítménye és a kommunikáció késleltetése közvetlenül befolyásolja a feltöltést. Ahogy nő az írások mennyisége, a replikáknak az olvasási forgalom mellett a beérkező csomagokkal is lépést kell tartaniuk.
Az általunk modellezett WAL-alapú megoldásban a Git-szolgáltatás egy blobtárolóban lévő közös manifest segítségével rendezi sorba az adattár frissítéseit. Amikor egy szerver feltöltést fogad, a beérkező csomagfájlt blobtárolóba tölti, és az adattár helyi másolatában előkészíti a referenciafrissítést. A feltöltés közzétételéhez egy bejegyzést ad a blobtárolóban lévő közös manifesthez, amely rögzíti a referenciafrissítést, és a csomagfájlra mutat. A Git-szerver ezután összehasonlítás és csere művelettel frissíti a manifestet. Ez atomikusan lecseréli a manifestet, vagy elutasítja a módosítást. Ha időközben egy másik író frissítette, a szerver beolvassa a közben bekerült bejegyzéseket, ellenőrzi, hogy referenciafrissítése továbbra is érvényes-e, majd a már feltöltött csomagfájllal újrapróbálkozik.
Az írók párhuzamosan tölthetik fel csomagfájljaikat, de még a független ágakra történő feltöltésekhez is ugyanazt a manifestet kell frissíteni. Egy sikeres összehasonlítás és csere oda-vissza ideje korlátozza, milyen gyakran tehet közzé az adattár új manifestet. Az alacsony késleltetésű blobtároló gyorssá teszi ezt a lépést. Kötegelt feldolgozással egy manifest egyszerre több referenciát is frissíthet, ennek ára azonban a frissítések sorba állítása és a nagyobb késleltetés.
A blobtároló párhuzamosan fogadhat csomagfájlokat, de a közös manifest frissítései továbbra is versengenek egymással. Önmagában a blobtároló kapacitásának növelése nem oldja fel ezt a szűk keresztmetszetet. Minden replikának továbbra is be kell olvasnia és indexelnie kell a csomagokat a blobtárolóból, mielőtt helyben kiszolgálhatná őket. Ha az írások ennél gyorsabban érkeznek, a replikák lemaradnak, és a klienseknek tovább kell várniuk a frissen feltöltött commitok lekérésére.
Ezzel el is érkeztünk a Pandóhoz. A Pando új architektúrát kínál a Git-adattárak kiszolgálására, azzal a céllal, hogy sok, egyszerre feltöltő kliensre skálázható legyen. Így működik:
A Pando fejlesztése előtt úgy sejtettük, hogy a Git feladása nélkül is teljesíthetjük a követelményeinket. Néhány olyan elvárásból azonban engednünk kellett, amelyet a legtöbb Git-rendszer alapkövetelménynek tekint:
Az egész adattárra kiterjedő konzisztencia: Az összes referencia konzisztens, adott időpillanatot tükröző pillanatképéhez az egész adattárra kiterjedő koordináció szükséges. Ha egy rendszer az összes referenciafrissítést közös sorrendbe rendezi, a független ágakra történő feltöltések egyetlen koordinációs pontért versengenek.
Egyedi csomagfájl minden kliensnek: Ha két, eltérő helyi előzménnyel rendelkező kliens ugyanazt az ágat kéri le, a szervernek külön csomagot kellhet előállítania mindkettőnek, hogy ne küldjön el már meglévő objektumokat. Ezeknek a válaszoknak az előállítása CPU-kapacitást és lemez-I/O-t igényel, ami megnehezíti sok olvasó kliens kiszolgálását.
A Pando ehelyett lazább konzisztenciamodellt (kauzális konzisztenciát) biztosít az írások skálázásához, az olvasásokat pedig a csomagfájlok újrafelhasználásával skálázza.
Amikor egy kliens feltölt egy ágra, a Pando a beérkező csomagfájlt megváltoztathatatlan blobként menti egy blobtárolóba. Az ág minden elmozdulásakor a Pando egy referenciaverziót rögzít egy metaadattárolóban: ez tartalmazza az új ágvéget, az azt biztosító csomagot és az összes korábbi referenciaverziót, amelytől a csomag függ. A blobtároló és a metaadattároló egyaránt horizontálisan skálázható.
Egy commit vagy referencia lekéréséhez a Pandónak referenciaverziók (csomagfájlok) sorozatát kell átadnia a kliensnek, rekonstruálva annak a kliensnek az állapotát, amely a lekért referenciaverziót létrehozta. Ez az egész adattárra nézve gyengébb konzisztenciát biztosít, miközben az egyes referenciák frissítései sorosíthatók maradnak. A kliensek csak akkor ütköznek, ha egyszerre töltenek fel ugyanarra a referenciára. A több ágat olvasó kliens nem feltételezheti, hogy ezek az olvasások az adattár ugyanazon időpillanatbeli állapotát írják le.
A főágra történő feltöltés egy vagy több commitot tartalmazó inkrementális csomagot ad hozzá. A mellékágra történő minden feltöltés újra tartalmazza a témaág korábbi commitjait, így a főág meghirdetett verziójával rendelkező kliens egyetlen csomagban lekérheti a témaágat.
Ez a kauzális konzisztencia egyik formája. Egy referencia lekéréséhez az olvasó annak az író kliensnek az előzményét látja, amely a referenciát létrehozta. Feltöltés közben a Pando szabályozza, hogy a kliens és a szerver milyen közös előzményben állapodik meg, így a kliensek könnyebben újrafelhasználható csomagfájlokat állítanak elő.
Ennek következtében a Pando a Git-műveletek során nem biztosít az összes referenciára kiterjedő, egyetlen időpillanathoz tartozó állapotot, sem a referenciafrissítések sorrendjét egy klónozáson vagy lekérésen belül. Tegyük fel például, hogy a foo és a bar ág is frissül egy teljes klónozás egyeztetési szakaszában. Az egyeztetés során az egyik kliens a foo ág 1. és a bar ág 2. verzióját láthatja, míg egy másik a foo ág 2. és a bar ág 1. verzióját. A gyakorlatban a felhasználók és az ügynökök nem a referenciafrissítések globális sorrendjére támaszkodnak, hanem egy-egy referencia frissítéseinek sorrendjére.
A három konzisztenciamodell szimulációjában 1000 klienst állítunk be úgy, hogy mindegyik külön ágat válasszon, majd ismételten egyedi commitokat töltsön fel. Közben mérjük a teljes tényleges futási időt és a feltöltéstől a lekérhetőségig eltelt időt. Az utóbbi fontos mutató a munkaterhelések szempontjából: azt méri, mennyi idő kell ahhoz, hogy a felhasználók és ügynökök együttműködhessenek, vagy más, verziókezelésre épülő rendszereket használhassanak. A szimuláció az általános célú megoldásoktól a prémium kategóriáig modellez virtuális gépeket és blobtároló rendszereket. Ebben a szimulációban a háromfázisú véglegesítésnél lazítottunk az egész adattárra kiterjedő konzisztencián.
Git-szerverek megvalósításainak szimulálása
Kövesd nyomon az egyes író kliensek ismételt feltöltéseit, és mérd külön, hogy egy független olvasó kliens mikor tudja kivenni az egyes commitokhoz tartozó állapotot.
Futtasd ezt a szimulációt, amely a Pandót két másik megközelítéssel hasonlítja össze. Válassz infrastruktúrát az általános célú hardvertől a prémium kategóriáig, állítsd be a kliensek számát és a többi paramétert, majd hasonlítsd össze az eredményeket.
A modellezett háromfázisú véglegesítési rendszerben terhelés alatt jelentősen nő az írási késleltetés, ahogy a lemezek elérik kapacitásuk határát. Az előzetes naplózást használó rendszereknél ugyanez az olvasási késleltetést növeli jelentősen, mert a szerverek nehezen tartanak lépést a napló olvasásával. A monorepónkon végzett valós tesztekben és ebben a szimulációban is sok ezer kliens tölthet fel párhuzamosan a Pandóval anélkül, hogy terhelés alatt romlana a teljesítmény.
Feltöltéskor a Pando a beérkező csomagfájlt a blobtárolóba továbbítás közben ellenőrzi, anélkül, hogy kicsomagolná egy helyi adattárba. Amikor egy kliens lekérést indít, a Pando kiválasztja a számára szükséges tárolt csomagokat, a kliens pedig letölti őket. Egyetlen szervernek sem kell csomagfájlokat alkalmaznia egy helyi adattárra, sem költséges ki- és becsomagolási műveleteket végeznie. Mivel a szerverek elsősorban egyeztetést és csomagfájl-kiválasztást végeznek, a Pando általános célú hardveren is futtatható.
Így a Pando a kliensek, a kliensek és a Pando közötti hálózat, illetve a blobtároló szolgáltatás korlátaiig skálázható. Elegendő kapacitás mellett a független ágakon dolgozó kliensek mindegyike a saját kapcsolata által megengedett sebességgel továbbíthat csomagokat. Ha több klienst szeretnénk ugyanilyen sebességgel kiszolgálni, további Pando-szervereket adhatunk a rendszerhez, és velük együtt bővíthetjük a metaadattárolót és a blobtárolót.
Ág feltöltése és lekérése a Pandóban
A Pando a blobtárolóban tárolja az író kliens csomagfájlját, a referencia verzióját pedig a metaadat-szolgáltatásban rögzíti. Lekéréskor a Pando kiválasztja az olvasó kliens számára szükséges csomagokat, az Express pedig elküldi őket.
Egy ügynök helyben szerkeszti a fájlokat, commitot készít, majd futtatja a git push parancsot, amely csomagfájlt hoz létre a módosításokból az ág frissítéséhez. A Pando fogadja a csomagot, és folyamatosan továbbítja a blobtárolóba. Miután egyetlen dekódolási menetben ellenőrizte a beérkező feltöltést, a Pando több kulcsra kiterjedő tranzakcióval rögzít egy referenciaverziót a metaadattárolónkban, és atomikusan frissíti a referenciamutatót. Így a Pando „saját írás visszaolvasása” típusú konzisztenciát biztosít. A teljes írási művelet metaadat-olvasást és -írást igényel.
Amikor egy másik kliens lekéri az ágat, a Pando a kliens meglévő előzménye alapján kiválasztja a tárolt csomagok megfelelő halmazát. Ez a művelet metaadat-olvasást igényel, ahol a kliens meglévő és kívánt állapota közötti legrövidebb távolság, referenciaverziókban mérve. A szerver visszaadja a csomagfájlok URI-jait, a kliens pedig az Express szolgáltatáson keresztül tölti le őket, amely regionális gyorsítótárakban tárolja a csomagfájlokat.
Ez a megoldás a Pando architektúrájának három központi szolgáltatására épül: a feltöltésre, a lekérésre és az összevonásra.
Feltöltéskor a hagyományos Git-szerver úgy viselkedik, mint egy pókerjátékos, aki kiteríti az összes lapját: minden meglévő ágát felfedi a kliens előtt. A kliens ezután összegyűjti a commitokat, fákat és blobokat, hogy olyan csomagfájlt hozzon létre, amely kihagyja azokat az objektumokat, amelyekről tudja, hogy a szerveren már megvannak.
Több referencia meghirdetésével csökkenthető a feltöltés mérete. Ez viszont megnehezítheti a csomag újrafelhasználását, mert egy másik kliensnél hiányozhatnak az író által kihagyott objektumok.
A Pando más megközelítést alkalmaz. Ahelyett, hogy minden lapját megmutatná, csak néhány kiválasztott ágat hirdet meg a kliensnek. Ennek két oka van:
Az összes referencia konzisztens nézetének biztosítása költséges. Több millió ág esetén már önmagában a referenciák meghirdetése is meghaladhatja a 100 MiB-ot. Ehhez minden szervernek vagy naprakész leképezést kell fenntartania jelentős koordinációs többlettel, vagy olyan adatbázis-lekérdezést kell futtatnia, amelynek költsége a referenciák számával nő.
Ha a kliens csak a referenciák egy részét kapja meg, olyan csomagot állít elő, amely kizárólag attól függ, amit a szerver kifejezetten megengedett kihagyni belőle. A függőségek nyomon követésével a Pando újrafelhasználhatja a kliens által egy feltöltéskor létrehozott csomagfájlt, és egy későbbi lekérés válaszaként szolgálhatja ki.
Minél több referenciát hirdet meg egy Git-szerver, annál több forrás alapján hagyhatnak ki objektumokat a feltöltő kliensek, így kisebb csomagokat készíthetnek. E csomagfájlok újrafelhasználásához azonban a szervernek bizonyítania kell tudni, hogy a lekérő kliensek az összes függőséggel rendelkeznek. Ellenkező esetben a csomagfájlból hiányozhatnak a kliens számára szükséges objektumok.
Ha viszont a Git-szerver egyetlen referenciát sem hirdet meg, a kliens az adattár teljes előzményét tartalmazó csomagfájlt állít elő. Ezt a csomagfájlt bármely más kliens használhatja, de ennek nagy ára van: bármely ág lekérése gyakorlatilag teljes klónozássá válik.
Több referencia meghirdetése kisebb csomagfájlokat eredményez. A méretcsökkenés nagy része már egyetlen referencia meghirdetésével is elérhető.
Alapértelmezés szerint, amikor egy kliens új funkcióágat tölt fel, a Pando csak egy kijelölt főágat hirdet meg alapként. Így a meghirdetés mérete akkor sem nő, ha új ágak kerülnek az adattárba, ennek azonban vannak hátrányai. Ha egy mérnök sokszor tölt fel ugyanarra az ágra, vagy egymásra épít funkcióágakat, a kliensnek újra el kell küldenie mindent, ami nincs benne a főágban.
Emiatt az egymást követő ágfeltöltések nagyobbak lesznek, esetleg redundáns objektumokat is tartalmaznak, és több sávszélességet használnak a kliensnél. Monorepónkban az így előállított csomagfájlok medián mérete 20 KiB alatt, a 99. percentilis pedig 3 MiB alatt volt. Számunkra ez észszerű kompromisszum volt, mert a főág meghirdetett előzményével már rendelkező olvasó egyetlen csomagfájllal lekérheti az ágat.
Miután kiválasztotta a kliensnek meghirdetendő referenciákat, a Pando fogadja a feltöltést, és frissíti a metaadattárolóját. A Pando szinkron és aszinkron részre bontja a metaadatok frissítését. A szinkron metaadat-frissítés biztosítja, hogy a kliensek azonnal lekérhessék a frissen frissített commitot. Ez új referenciaverziót ír, és a referenciamutatót erre az új verzióra állítja. A verzió tárolja az új ágvéget, a csomagfájl egyeztetéséhez használt függőségeket és a feltöltött csomagfájlra mutató hivatkozást.
Ezzel egy időben, ugyanabban a tranzakcióban az aszinkron feladatokat is rögzíti, például az összevonás ütemezését. Nagyon nagy feltöltéseknél csak a commitok egy részét írja szinkron módon. Így a feltöltés méretétől függetlenül állandó számú olvasásra és írásra van szükség a metaadattárolóban.
Fontos, hogy a Pando-szerverek egymástól függetlenül fogadhatnak feltöltéseket a nem összefüggő referenciákra. Az egyetlen kulcs, amelyért versengés folyhat, a referenciamutató; ezt atomikus, összehasonlítás és csere jellegű művelettel védjük. Az írási adatátviteli egységet további Pando-szerverekkel, a metaadattároló skálázási egységeivel vagy blobtárolókkal növelhetjük.
Lekéréskor a kliens két commitazonosító-halmazt ad át a szervernek; ezeket a protokoll have és want néven ismeri. A hagyományos Git-szerver kiszámítja a kért és a meglévő commitokból elérhető objektumok halmazkülönbségét, tömöríti az objektumokat, majd elküldi az eredményt a kliensnek. Bár a nyílt forráskódú Git-szerver többféle helyi indexszel gyorsítja ezt a műveletet, e csomagfájlok előállítása költséges lehet, és általában nehezen gyorsítótárazhatók.
A Pando ehelyett a feltöltéskor rögzített commit-leképezésekben keresi meg a kliens have és want elemeit, és az azokat tartalmazó referenciaverziók halmazára oldja fel őket. Minden ilyen referenciaverzió egy korábban a Pando által fogadott csomagfájlra hivatkozik. A szerver ez alapján határozza meg, mire van szüksége a kliensnek, és kiválasztja a szükséges objektumhalmazt lefedő csomagfájlokat.
Lekérések tervezése a Pandóban
A Pando összeveti a kliens meglévő adatait az igényeltekkel, majd kiválasztja a szükséges tárolt csomagfájlokat, beleértve az ágak hiányzó függőségeit is.
27 főági csomag lekérése.
Válassz ki egy ágat a főági függőségeinek bevonásához.
A Pando a Git-protokoll meglévő és kért commitjait referenciaverziók sorozatára képezi le. Egy mellékág lekérése a főágon lévő függőségeinek lekérését is magában foglalja.
A Git-protokoll klónozáskor vagy lekéréskor csak egyetlen csomagfájl elküldését engedi a HTTP-válaszban. A Git-protokoll egy kevéssé ismert funkciója, a packfile-uri azonban lehetővé teszi, hogy a szerver további csomagok külön letöltésére szolgáló hivatkozásokat adjon vissza. A Pando kizárólag ezt használja, és magában a válaszban üres csomagfájlt továbbít. E funkció támogatásához az OpenAI több packfile-uri-fejlesztéssel járult hozzá az eredeti projekthez.
Az OpenAI rendszerei számos földrajzi régióban működnek, ezért mindegyikben helyi gyorsítótárakkal rendelkező Pando-klasztereket tartunk fenn. Az Express csomagkiszolgáló szolgáltatás a blobtárolónkat gyorsítótárazza, a metaadattárolóhoz pedig külön gyorsítótár-szolgáltatás tartozik. Ezekkel a gyorsítótárakkal a Pando egy commitazonosítóra irányuló kérést vagy egy referencia speciális olvasását teljes egészében az adott klaszter gyorsítótárazott állapotából szolgálhatja ki. Konzisztens hasheléssel irányítjuk a csomagfájlokra vonatkozó kéréseket az Express-szerverekhez, 99% feletti gyorsítótár-találati aránnyal.
Ez a gyorsítótár-rendszer rendkívül nagy adatátviteli egységet biztosít, így teljesen kihasználhatja a kliensek és a Pando közötti hálózati kapacitást. E csomagfájl-URI-k és helyi gyorsítótárak használatával igazoltuk, hogy a klónozások és lekérések pusztán a felhőszolgáltatók szokásos virtuális gépein is teljesen kihasználják a 10 Gbps-os hálózati kapcsolatokat.
A legtöbb Git-szervernél újracsomagolásra és szemétgyűjtésre van szükség a növekvő adattár teljesítményének fenntartásához. Mindkét lépés tárhelyet szabadít fel, de számítás- és I/O-igényes.
Egy olyan gyorsan változó adattárban, mint az OpenAI monorepója, nem járható út, hogy az utolsó lekérés óta történt minden feltöltéshez külön csomagfájl-URI-t küldjünk. Egy kliensnek naponta több tízezer csomagfájlt kellene letöltenie, hogy naprakészre hozza a főágát.
A Pando ehelyett a háttérben egyesíti a csomagokat e többletterhelés csökkentésére, a naplóstruktúrájú egyesítőfák összevonási eljárásához hasonló megoldással. Minden feltöltés egy, a Pando által „L0” csomagfájlnak nevezett fájlt hoz létre: ez a Pando által továbbítható legkisebb adategység. Idővel háttérfolyamatok referenciaverziók sorozatát lefedő csoportokba rendezik ezeket a csomagfájlokat, az L0-tól mértani léptékben növekedve. Monorepónk ma az L4 szintig használ csomagfájl-lefedéseket.
Ezek a lefedések kettős célt szolgálnak. Amikor egy lefedés kellően megtelik, egy összevonó folyamat lefoglalja, és egyetlen csomagfájlt készít belőle. Ez a csomagfájlon belüli objektumok átrendezésével javítja a tömörítési arányt. Emellett a lekérési egyeztetés teljesítményét is javítja, mert a referenciaverziók felett ugrólistaként működik. Az egyes szintek indexeinek egyidejű bejárásával a Pando logaritmikus idő alatt választhatja ki a kliensnek elküldendő csomagokat. Ezek a csomagfájl-lefedések újracsomagolva és anélkül is hatékonyabbá teszik a lekérések tervezését, csökkentve a Pando adatátviteli egységére és késleltetésére vonatkozó követelményeket.
Összevonás a Pandóban
A kisebb csomagfájlokat a szinteken át egyre nagyobb lefedő egységekbe csoportosítják, ami javítja a tömörítést és gyorsítja a lekéréseket.
Az összevonás a háttérben több referenciaverzió csomagfájljait egyesíti, így kevesebb fájllal, hatékonyabban végezhetők a lekérések.
Sok Git-szerverrel ellentétben itt minden szint csomagfájljai megmaradnak; nem dobjuk el és szemétgyűjtéssel sem töröljük őket. Minden szint csomagjait folyamatosan használjuk a kliensek kiszolgálására, a lekérésben megadott have és want elemeik alapján. A Pandót API-ként használó klienseknél pedig egy tetszőleges objektum csomagfájlból való kiolvasása általában kevesebb adat beolvasását igényli L0-ból, mint L4-ből, mert kisebb valószínűséggel tömörített.
A Pando összevonási eljárása jelentősen csökkenti a klienseknek elküldött adatok összméretét, és lehetővé teszi, hogy a csomagtervezés költsége a kliens lemaradásánál lassabb, szublineáris ütemben növekedjen. Ez különösen fontos a nagy, gyorsan változó adattáraknál, amelyek várakozásaink szerint mindennapossá válnak a szoftverfejlesztésben.
A Pandót a Codex segítségével építettük meg: az indulástól az éles üzemig mindössze 10 hét telt el. A Pando magját egyetlen mérnök tervezte, ügynökcsapatok valósították meg, és néhány mérnök helyezte üzembe. A Pando idén áprilisban kezdett éles forgalmat kiszolgálni, és az OpenAI-nál használt verziókezelési eszközök nagyobb rendszerének egyik eleme.
A Pando terveinek ellenőrzésekor sokféle megközelítést alkalmaztunk. Alkalmaztuk a biztonságosabb szoftveralapok kiépítésére vonatkozó elveinket. Mivel zöldmezős projektről volt szó, úgy döntöttünk, hogy a Pandót Rustban, örökölt függőségek nélkül valósítjuk meg. Megfelelőségi tesztcsomagot írtunk, hogy az ügynökök a Git szokásos működéséhez mérten ellenőrizhessék módosításaikat. Determinisztikus szimulációs teszteléshez is fejlesztettünk végrehajtási környezeteket, amelyekkel ellenőrizhettük a rendszer invariánsait és tulajdonságait. Így az egyeztetés és a csomagtervezés nehezen észrevehető versenyhelyzeteit és regresszióit is felderíthettük és mérsékelhettük.
Sok modell járult hozzá a rendszerhez; ahogy új modellek váltak elérhetővé, a Pando teljesítményének és megbízhatóságának javítására használtuk őket. A GPT‑6 Astra használatával az ügynökök Lean és TLA+ nyelven formalizálták a „Pando elméletét”. Ezzel hibákat találtunk és javítottunk a megvalósításban, valamint bizonyítottuk a rendszer egyes tulajdonságait. Több folyamatos munkamenet futott több száz órán át, hogy felderítse és javítsa a Pando szélső eseteit. Emellett éjjel-nappal futtatunk ügynököket, amelyek Daybreak-képességekkel rendelkező modellekkel támadói szemszögből tesztelik a Git és a Pando biztonságát.
A Pandót kezdetben a meglévő verziókezelő rendszerünk elé helyezett, az olvasásokat és írásokat továbbító proxyként vezettük be. Így szinkronban tarthattuk a Pandót és a korábbi rendszert, és biztonságosan, leállás nélkül irányíthattuk át a forgalmat a Pandóra.
Gyorsítótárazási megoldásának részeként a Pando a továbbítás előtt a kérések ismétlődéseit is kiszűrheti, és összevonhatja őket. Így a meglévő eszközeink megbízhatóan működhettek, miközben rendszereinket egyenként átállítottuk a Pandóra. Ahogy egyre több munkaterhelést vittünk át a Pandóra, a feltöltések megtöltötték annak gyorsítótárát, és javították a korábbi háttérrendszerünk megbízhatóságát.
A Pando használata melletti teljesítmény optimalizálásához elkezdtük eljuttatni nyilvános Git-disztribúciónkat a fejlesztői gépekre és a ChatGPT asztali alkalmazásába. A klienseket a Git parancssori felülete köré épített belső burkolóprogrammal, szakaszosan állítottuk át a Pandóra. Számos további eszközt készítettünk a módosítási kérelmek és a kódellenőrzés kezeléséhez, valamint API-kat az automatizálásokhoz. A Git-forgalommal együtt több száz, régi API-kra, webhookokra és adattárletöltésekre támaszkodó integrációt is áttelepítettünk.
Maga a Codex is segített a fejlesztőknek és a csapatoknak az átállásban. Készségeket készítettünk a meglévő munkapéldányok előkészítésére és arra, hogy az ügynököket az új rendszer használatában irányítsák. Ennek köszönhetően a fejlesztők egyszerűen megkérhették a Codexet, hogy állítsa át fejlesztői környezeteiket és szolgáltatásaikat.
A Pandóra való átállás javította az egyesítések megbízhatóságát, és közel 80%-kal csökkentette a feltöltések medián késleltetését.
A Pando mindössze néhány CPU-maggal képes naponta egy petabájtnyi Git-adatot kiszolgálni. A Pando egyetlen óra alatt több mint 57 terabájtnyi Git-csomagot szolgált ki, a bájtok több mint 99%-át helyi lemezes gyorsítótárból.
A csomagok újrafelhasználásával sok kliens töltheti le ugyanazt az adattárelőzményt, miközben a kiszolgáló csomópontoknak kevés számítást kell végezniük. Ez a példa egyetlen adattárat mutat be, de a Pandót jóval több támogatására terveztük. Az igények növekedésével az adatkiszolgálást és a Git-egyeztetést egymástól függetlenül skálázhatjuk.
Mivel a Pandót a Git protokolljára és kliensére építettük, hibákat és lehetőségeket azonosíthattunk magának a Gitnek a teljesítményjavítására.
A Pando például közvetlenül a klienseknek szolgál ki újrafelhasználható csomagokat, ezért ezek letöltése és indexelése központi szerepet kapott a rendszer gyorssá és megbízhatóvá tételében. E munka során szélső esetek kerültek felszínre a párhuzamos letöltéseknél, a megszakadt átvitelek utáni takarításnál és a már befejezett letöltések folytatásánál. Javítottuk ezeket a kódútvonalakat, ellenőrzéseket vezettünk be a meglévő csomagok lekéréshez való elfogadásakor, és a módosításokat visszajuttattuk az eredeti projektbe.
Azt is látnunk kellett, mire megy el az idő és a sávszélesség, ezért nyomkövetést adtunk a packfile-uri-letöltésekhez, a csomaggeneráláshoz pedig bájtszámlálást. Így könnyebb megállapítani, hogy egy lassú lekérés idejét a csomagok előállítása vagy továbbítása emészti-e fel.
Az OpenAI Git-változatában(új ablakban nyílik meg) a kliens párhuzamosan töltheti le és indexelheti a független csomagokat. Bevezettük az „explicit meglévő commitok” fogalmát, hogy a szerver az ág aktuális végét külön hirdethesse meg attól az előzménytől, amelyet a feltöltő kliens kihagyhat a csomagjából. A Pandón ez lehetővé teszi, hogy a kliensek a --force-with-lease kapcsolóval ellenőrizzék, hogy egy funkcióág nem mozdult-e el, a csomagot pedig kizárólag a főág alapján építsék fel. Ezután bárki, aki rendelkezik a főág adott előzményével, ugyanazzal a csomaggal kérheti le az ágat.
E fejlesztések közül több már az eredeti Gitben is megtalálható, sok másik pedig a nyilvános adattárunkban érhető el. Továbbra is együttműködünk a Git közösségével, hogy ezek a fejlesztések a Pandón túl is hasznosak legyenek. Ezt a munkát a nyilvános javításkövetőnkben(új ablakban nyílik meg) követheted.
A Git-adattárak skálázásáról alkotott bevett elképzelések újragondolásával a Pando lehetővé teszi monorepónk bővítését anélkül, hogy változtatnunk kellene a mérnökeink és ügynökeink által már használt Git-munkafolyamatokon. A Pando nagyobb párhuzamosságot tett lehetővé monorepónkban, javította az egyesítések megbízhatóságát, és csökkentette a kódmódosítások közzétételéhez szükséges időt. A monorepón túl a Pando más felhasználási lehetőségeit is vizsgáljuk az OpenAI-on belül, például olyan munkaterheléseknél, ahol sok ügynöknek kell együttműködnie egy közös Git-adattárban.
Ahogy a mérnökök és ügynökök egyre több szoftvert készítenek, az OpenAI-on kívül is egyre több csapat szembesül majd azokkal a verziókezelési korlátokkal, amelyekbe mi ütköztünk. Jelenleg azt vizsgáljuk, hogyan oszthatnánk meg felhasználóinkkal a Pandót lehetővé tevő technológiát.
A Pando a Git-adattárak felhasználóinak és üzemeltetőinek két évtizedes tapasztalatára épít. Folytatjuk a Pando fejlesztését, és a nyílt forráskódú Git közösségével együttműködve megosztjuk azokat a fejlesztéseket, amelyek a Git-felhasználók szélesebb körének is hasznára válnak.


