مقیاسپذیری Git در OpenAI
ساخت سامانهٔ کنترل کد منبع در مقیاس عاملها، همراه با سرمایهگذاری در زیستبوم Git.
در سال گذشته، اندازهٔ محل نگهداری یکپارچهٔ ما و بار تحمیلشده به آن با سرعتی خیرهکننده افزایش یافته است. روشن بود که باید سامانهٔ کنترل کد منبع خود را بازطراحی کنیم تا بتواند پاسخگوی حجم کار همزمانی باشد که Codex ممکن میسازد.
Pando را ساختیم؛ بکاندی جدید برای محلهای نگهداری Git، بدون توقف کار مهندسی یا تغییر در شیوهٔ استفادهٔ تیممان از Git. اکنون تمام فعالیتهای توسعه در محل نگهداری یکپارچهٔ OpenAI بر بستر Pando انجام میشود. Pando برای دستیابی به دو هدف بلندپروازانه طراحی شده است: نخست، نوشتن با مقیاسپذیری افقی، بهینهشده برای پوش همزمان به تعداد زیادی شاخه. و دوم، به حداکثر رساندن توان عملیاتی خواندن، تا کلاینتها بتوانند داده را آنقدر سریع دانلود کنند که تمام پهنای باند در دسترس شبکهشان به کار گرفته شود.
با حفظ سازگاری با پروتکل و کلاینت Git به این اهداف رسیدیم تا مهندسان و عاملهایمان بتوانند Pando را بیدردسر در گردشکارهای فعلی خود به کار بگیرند. در محیط عملیاتی، Pando میانهٔ تأخیر پوش را به حدود یکپنجم رساند. اکنون روزانه نزدیک به یک پتابایت دادهٔ Git ارائه میکند.
در این مطلب، انگیزههای ساخت سامانهٔ جدید کنترل کد منبع و دلیل انتخابمان برای حفظ سازگاری با Git را شرح میدهیم. سامانهٔ خود را با رویکردهای رایج مقیاسپذیری Git مقایسه میکنیم و توضیح میدهیم چرا Pando توانایی منحصربهفردی برای پاسخگویی به نیازهای مقیاسپذیری OpenAI دارد. همچنین از گامهای بعدی و ادامهٔ مشارکتمان در Git برای پشتیبانی از کار در مقیاس عاملها میگوییم.
Codex مقیاس و سرعت توسعه در OpenAI را تغییر داده و به مهندسان امکان داده است کارهای بسیار بیشتری را بهصورت موازی انجام دهند. این یعنی شاخهها و پوشهای بیشتر، ادغامهای بیشتر، و واکشیها و کلونهای بیشتر از سوی محیطهای توسعه، کارهای پژوهشی و یکپارچهسازی مداوم (CI).
این نیازها اثر یکدیگر را تشدید میکنند. حالا یک مهندس چندین تغییر را همزمان آمادهٔ CI دارد و هر پوش میتواند چندین کار را به واکشی یک کامیت مشترک وادارد.
از حدود اواخر سال گذشته، سامانههای پشتیبان محل نگهداری یکپارچهٔ ما بهسختی از پس این حجم کار برمیآمدند. با واکشیها و پوشهای کند و ادغامهای ناموفق روبهرو بودیم و قابلیت اطمینان گاهی به زیر ۹۰٪ میرسید. آیا به محدودیت خود Git رسیده بودیم، یا محدودیت شیوهٔ سرویسدهی محل نگهداری یکپارچهمان؟ Git از دل جامعهای شکل گرفت که وصلههای هسته را از طریق ایمیل ردوبدل میکرد. بیش از دو دهه است که توسعهدهندگان با تغییر شیوهٔ ساخت نرمافزار، ابزارهای خود را حول Git تطبیق دادهاند. وقتی کار با محل نگهداری یکپارچهٔ خودمان دشوار شد، برایمان مهم بود که بتوانیم همچنان بر پایهٔ آن دستاوردها پیش برویم.
راهی دیدیم که آن گردشکار آشنا را حفظ کنیم و در عین حال بکاند را بازسازی کنیم تا پوش به شاخههای مختلف را موازی پردازش کند و یک تاریخچهٔ مشترک را همزمان به خوانندگان متعدد ارائه دهد. انتخاب Git بهعنوان زیربنای گردشکار کنترل کد منبع، به این معنا بود که بهبودهای کلاینت میتوانست خارج از OpenAI نیز مفید باشد. میخواستیم این بهبودها را از نسخهٔ خودمان از Git(در یک پنجره جدید باز میشود) به پروژهٔ اصلی برگردانیم و با جامعهٔ گستردهتر به اشتراک بگذاریم.
مقیاسپذیری سامانهٔ کنترل کد منبع دو بُعد اساسی دارد: نویسندگان زیادی که به شاخههای مختلف پوش میکنند و خوانندگان زیادی که یک تاریخچهٔ مشترک را واکشی میکنند. Pando برای هر دو بهینه شده است.
نخست، Pando بهروزرسانیهای هر شاخه را مستقل هماهنگ میکند. بهروزرسانی شاخههای نامرتبط میتواند موازی پیش برود و خوانندهای که شاخهای را واکشی میکند، تاریخچهٔ موردنیاز آن بازبینی را دریافت میکند. در مقابل، خوانندهای که چند شاخه را بررسی میکند، ممکن است وضعیت آنها را در لحظههای متفاوت ببیند.
دوم، Pando بهجای ساخت فایل بستهٔ جداگانه برای هر کلاینت، فایلهای بسته را در واکشیهای مختلف دوباره استفاده میکند؛ فایل بسته قالب Git برای فشردهسازی چند شیء در کنار هم است. ممکن است کلاینت برخی اشیای موجود را دوباره دانلود کند، اما سرور میتواند با محاسبات اندک، همان بایتها را به کلاینتهای متعدد ارائه دهد.
این انتخابها به ما اجازه میدهند ظرفیت نوشتن را بین شاخهها افزایش دهیم و درخواستهای خواندن را از حافظهٔ نهان پاسخ دهیم. برای دیدن اینکه این بخشها چگونه کنار هم کار میکنند، از نحوهٔ ذخیره و انتقال تاریخچه در Git شروع کنیم.
Git باید بتواند دو بخش از محل نگهداری را همگام نگه دارد: اشیا و ارجاعها.
محل نگهداری Git، تصویرهای لحظهای فایلها و تاریخچهٔ اتصال آنها را بهشکل گرافی از اشیا ثبت میکند. هر شیء میتواند بلاب، درخت، کامیت یا برچسب باشد. بلابها محتوای یک فایل را نگه میدارند و درختها نامها را به بلابها یا درختهای دیگر پیوند میدهند تا ساختار پوشهها شکل بگیرد. کامیتها به یک درخت ریشه و صفر یا چند کامیت والد اشاره میکنند و تاریخچهٔ محل نگهداری شما را میسازند.
اشیا با هشِ نوع و محتوایشان شناسایی میشوند. اشیا تغییرناپذیر نیز هستند: ثبت فایل تغییرکرده، بلاب جدیدی میسازد، اما فایلها و پوشههای بدون تغییر میتوانند از اشیای موجود دوباره استفاده کنند.
شاخهها و برچسبها ارجاعهایی هستند که به کامیتها اشاره میکنند و به آنها «refs» نیز گفته میشود. کامیتکردن روی یک شاخه، کامیت جدیدی ایجاد میکند و ارجاع شاخه را بهروزرسانی میکند تا به آن اشاره کند.
اشیای Git با شناسه به یکدیگر ارجاع میدهند: کامیتها به درختها، و درختها به درختها و بلابها. شاخهٔ refs/heads/main به کامیت C2 اشاره میکند که والد آن C1 است. C1 به درخت ریشهٔ T1 اشاره میکند که در آن parser.py به بلاب B1 ارجاع میدهد. C2 به درخت ریشهٔ T2 اشاره میکند که در آن parser.py به بلاب B2 ارجاع میدهد. در هر دو درخت ریشه، docs/ به همان درخت T3 ارجاع میدهد. ورودی README.md آن به بلاب B3 و usage.md به بلاب B4 اشاره میکند.
Git اشیا را در یکی از دو حالت «جداگانه» یا «بستهبندیشده» ذخیره میکند. اشیای جداگانه در فایلهای مستقلی ذخیره میشوند که با شناسهٔ شیء شناسایی میشوند. اشیای بستهبندیشده در یک فایل بسته کنار هم ذخیره میشوند؛ به این ترتیب میتوان اشیای مشابه را بهشکل اختلاف نسبت به یکدیگر ذخیره کرد.
Git میتواند بلابها را در فایلهای جداگانه ذخیره کند یا آنها را با هم بستهبندی کند و یک بلاب را بهشکل اختلاف نسبت به دیگری کدگذاری کند.
در Git، یک شیء از یک کامیت قابلدسترسی است اگر Git بتواند با دنبالکردن پیوندهای آن کامیت از میان والدها، درختها و بلابها به آن برسد. هنگام واکشی، کلاینت به سرور میگوید کدام کامیتها را میخواهد و کدامها را از قبل دارد. سرور اشیای قابلدسترسی از کامیتهای درخواستی را پیدا میکند، مواردی را که میداند کلاینت دارد کنار میگذارد و بقیه را در یک فایل بسته با همان قالبی میفرستد که Git روی دیسک استفاده میکند. کلون کامل بدون هیچ شیئی آغاز میشود، پس به همهٔ اشیای قابلدسترسی از تاریخچهٔ در حال کلونشدن نیاز دارد.
پوش اشیا را در جهت عکس منتقل میکند و از سرور میخواهد یک یا چند ارجاع را بهروزرسانی کند. کلاینت با استفاده از مجموعهٔ ارجاعهای اعلامشدهٔ سرور، از ارسال اشیایی که سرور از قبل دارد خودداری میکند. بنابراین بستهٔ ارسالی میتواند به اشیای موجود وابسته باشد: دریافت بسته بهتنهایی برای بازسازی تاریخچهٔ جدید کافی نیست.
تکثیر یک محل نگهداری Git یعنی در دسترس نگهداشتن اشیا و ارجاعها روی چندین سرور. پیش از آنکه یک گرهٔ رونوشت بتواند مقدار جدید ارجاع را ارائه دهد، باید بتواند اشیای قابلدسترسی از آن مقدار را تأمین کند. سرویس همچنین باید بهروزرسانیهای ارجاع رقیب را هماهنگ کند؛ یعنی پوشهایی که میکوشند یک شاخه را از همان نوک قبلی به کامیتهای جدید متفاوت جابهجا کنند. سامانههایی که ترتیب واحدی بر همهٔ نوشتنها در محل نگهداری تحمیل میکنند، ممکن است پوش به شاخههای نامرتبط را نیز منتظر یکدیگر نگه دارند.
هنگام طراحی Pando، رویکردهای رایج مقیاسپذیری کنترل کد منبع با Git را ارزیابی کردیم: رویکردی که بهروزرسانی ارجاعها را میان رونوشتهای محل نگهداری با ثبت سهمرحلهای هماهنگ میکند (3PC، پروتکلی برای هماهنگکردن ثبت در چند گره)، و رویکردی که بهروزرسانیها را با یک لاگ پیشنوشت مشترک در ذخیرهساز بلاب مرتب میکند (WAL، سابقهای از تغییرات که پیش از اعمالشان نوشته میشود).
در طراحی ثبت سهمرحلهای که مدلسازی کردیم، سرویس Git نسخهٔ کاملی از محل نگهداری را روی چند گرهٔ رونوشت نگه میدارد. وقتی هر سروری یک پوش دریافت میکند، فایل بستهٔ ورودی کپی و بهصورت موازی به همهٔ گرههای رونوشت ارسال میشود. برای انتشار پوش، اکثریت گرههای رونوشت باید بهروزرسانی ارجاع را بپذیرند. سرور مدیر این فرایند، هماهنگکننده است و این فرایند به چند دور ارتباط میان هماهنگکننده و هر گرهٔ رونوشت دیگر نیاز دارد.
برای پذیرش بهروزرسانی ارجاع، هر گرهٔ رونوشت نسخهٔ خود از ارجاع را قفل میکند و بررسی میکند که همچنان به کامیت موردانتظار کلاینت اشاره داشته باشد. سپس هماهنگکننده پیام پیشثبت را به گرههای رونوشتِ پذیرنده میفرستد. وقتی اکثریت پیام پیشثبت را تأیید کردند، هماهنگکننده به آنها میگوید بهروزرسانی را انجام دهند. پس از آن، هر گرهٔ رونوشتِ بهروز میتواند نوک جدید شاخه را از نسخهٔ محلی محل نگهداری خود اعلام کند.
نگهداری رونوشتهای کامل، پاسخگویی محلی به خواندنها را ممکن میکند و تضمین میکند کلاینتها صرفنظر از گرهٔ انتخابی، نمایی سازگار از همهٔ ارجاعها دریافت کنند. در نتیجه، ارجاعها یکییکی بهروزرسانی میشوند و هر پوش به چند رفتوبرگشت برای هماهنگکردن بهروزرسانی ارجاع میان گرههای رونوشت نیاز دارد.
انتظار برای رسیدن به حد نصاب، عملکرد دیسک گرههای رونوشت و تأخیر ارتباط را در مسیر پوش دخیل میکند. با افزایش حجم نوشتن، گرههای رونوشت باید علاوه بر ترافیک خواندن، از پس بستههای ورودی نیز برآیند.
در طراحی مبتنی بر WAL که مدلسازی کردیم، سرویس Git از یک اساس نامهٔ مشترک در ذخیرهساز بلاب استفاده میکند تا بهروزرسانیها را در سراسر محل نگهداری مرتب کند. وقتی سرور یک پوش دریافت میکند، فایل بستهٔ ورودی را در ذخیرهساز بلاب بارگذاری و بهروزرسانی ارجاع را در نسخهٔ محلی محل نگهداری خود آماده میکند. برای انتشار پوش، مدخلی به اساس نامهٔ مشترکِ ذخیرهشده در ذخیرهساز بلاب اضافه میکند که بهروزرسانی ارجاع را ثبت میکند و به فایل بسته اشاره دارد. سپس سرور Git اساس نامه را با عملیات مقایسهوتعویض بهروزرسانی میکند. این عملیات یا اساس نامه را بهصورت اتمی جایگزین میکند، یا تغییر را رد میکند. اگر در این فاصله نویسندهٔ دیگری آن را بهروزرسانی کرده باشد، سرور مدخلهای افزودهشده را میخواند، بررسی میکند بهروزرسانی ارجاعش هنوز معتبر است و با همان فایل بستهٔ قبلاً بارگذاریشده دوباره تلاش میکند.
نویسندگان میتوانند فایلهای بستهٔ خود را موازی بارگذاری کنند، اما حتی پوش به شاخههای نامرتبط هم نیازمند بهروزرسانی همان اساس نامهٔ مشترک است. زمان رفتوبرگشت یک مقایسهوتعویض موفق، بسامد انتشار اساس نامهٔ جدید برای محل نگهداری را محدود میکند. ذخیرهساز بلابِ کمتأخیر این مرحله را سریع میکند. پردازش دستهای اجازه میدهد یک اساس نامه چند ارجاع را همزمان بهروزرسانی کند، اما به بهای صفکردن بهروزرسانیها و افزایش تأخیر.
ذخیرهساز بلاب میتواند فایلهای بسته را موازی دریافت کند، اما بهروزرسانیهای اساس نامهٔ مشترک همچنان با یکدیگر رقابت دارند. افزایش ظرفیت ذخیرهساز بلاب بهتنهایی این گلوگاه را برطرف نمیکند. هر گرهٔ رونوشت همچنان باید پیش از ارائهٔ محلی بستهها، آنها را از ذخیرهساز بلاب بخواند و نمایهسازی کند. اگر سرعت نوشتنهای ورودی از این فرایند بیشتر شود، گرههای رونوشت عقب میمانند و کلاینتها برای واکشی کامیتهای تازه پوششده بیشتر منتظر میمانند.
اینجاست که به Pando میرسیم. Pando معماری جدیدی برای سرویسدهی محلهای نگهداری Git است که با هدف پشتیبانی از کلاینتهای بسیاری طراحی شده که همزمان پوش میکنند. نحوهٔ کار آن به این صورت است:
پیش از توسعهٔ Pando، حدس میزدیم بتوانیم بدون کنارگذاشتن Git نیازهایمان را برآورده کنیم، اما لازم بود از برخی مواردی که بیشتر سامانههای Git ضروری میدانند، صرفنظر کنیم:
سازگاری در سراسر محل نگهداری: ارائهٔ تصویری سازگار از همهٔ ارجاعها در یک لحظهٔ معین، به هماهنگی در سراسر محل نگهداری نیاز دارد. در سامانهای که همهٔ بهروزرسانیهای ارجاع را در یک ترتیب مشترک قرار میدهد، پوشها به شاخههای نامرتبط بر سر یک نقطهٔ هماهنگی رقابت میکنند.
فایل بستهٔ اختصاصی برای هر کلاینت: اگر دو کلاینت با تاریخچههای محلی متفاوت یک شاخه را واکشی کنند، سرور ممکن است مجبور شود برای هرکدام بستهای متفاوت بسازد تا اشیایی را که از قبل دارند ارسال نکند. ساخت این پاسخها پردازنده و ورودی/خروجی دیسک مصرف میکند و مقیاسپذیری برای تعداد زیاد خواننده را دشوار میسازد.
در مقابل، Pando برای مقیاسپذیری نوشتن، مدل سازگاری منعطفتری (سازگاری علّی) ارائه میدهد و برای مقیاسپذیری خواندن، فایلهای بسته را دوباره استفاده میکند.
وقتی کلاینتی به یک شاخه پوش میکند، Pando فایل بستهٔ ورودی را بهشکل یک بلاب تغییرناپذیر در ذخیرهساز بلاب ذخیره میکند. هر بار که شاخه جابهجا میشود، Pando یک نسخهٔ ارجاع در ذخیرهساز فراداده ثبت میکند که شامل نوک جدید شاخه، بستهٔ تأمینکنندهٔ آن و نسخههای پیشین ارجاع است که بسته به آنها وابسته است. ذخیرهساز بلاب و ذخیرهساز فراداده هر دو مقیاسپذیری افقی دارند.
واکشی یک کامیت یا ارجاع از Pando مستلزم بازپخش مجموعهای از نسخههای ارجاع (فایلهای بسته) برای کلاینت است تا وضعیت کلاینتی بازسازی شود که نسخهٔ ارجاعِ در حال واکشی را ایجاد کرده است. این روش، در سراسر محل نگهداری شکل ضعیفتری از سازگاری را فراهم میکند، اما برای بهروزرسانی هر ارجاع بهتنهایی، سریالپذیری را تضمین میکند. کلاینتها فقط هنگام پوش همزمان به یک ارجاع مشترک با هم تعارض پیدا میکنند. کلاینتی که چند شاخه را میخواند، نمیتواند فرض کند این خواندنها وضعیت سراسر محل نگهداری را در یک لحظهٔ واحد نشان میدهند.
هر پوش به شاخهٔ اصلی، یک بستهٔ افزایشی شامل یک یا چند کامیت اضافه میکند. هر پوش به شاخهٔ جانبی، کامیتهای قبلی آن شاخه را دوباره شامل میشود تا کلاینتی که نسخهٔ اعلامشدهٔ شاخهٔ اصلی را دارد، بتواند شاخهٔ موضوعی را با یک بسته واکشی کند.
این شکلی از سازگاری علّی است. برای واکشی یک ارجاع، خواننده تاریخچهٔ نویسندهای را میبیند که آن را ایجاد کرده است. هنگام پوش، Pando تعیین میکند کلاینت و سرور بر سر کدام تاریخچهٔ مشترک توافق کنند تا کلاینتها فایلهای بستهای بسازند که اشتراکگذاری آنها آسانتر باشد.
در نتیجه، Pando در عملیات Git وضعیت همهٔ ارجاعها را در یک لحظهٔ معین ارائه نمیدهد و در کلون یا واکشی نیز ترتیبی برای بهروزرسانی ارجاعها تضمین نمیکند. برای نمونه، فرض کنید شاخههای foo و bar هر دو هنگام مذاکره برای یک کلون کامل بهروزرسانی شوند. هنگام مذاکره، ممکن است یک کلاینت نسخهٔ ۱ از foo و نسخهٔ ۲ از bar را ببیند، درحالیکه کلاینت دیگری نسخهٔ ۲ از foo و نسخهٔ ۱ از bar را میبیند. در عمل، کاربران و عاملها به ترتیب سراسری بهروزرسانی ارجاعها وابسته نیستند؛ آنچه برایشان اهمیت دارد، ترتیب بهروزرسانیهای یک ارجاع واحد است.
در این شبیهسازی سه مدل سازگاری، ۱٬۰۰۰ کلاینت را طوری تنظیم میکنیم که هرکدام شاخهای مجزا انتخاب کند و سپس پیوسته کامیتهای منحصربهفرد پوش کند. زمان واقعی کل اجرا و تأخیر از پوش تا واکشی را اندازه میگیریم. معیار دوم برای بارهای کاری مهم است، چون زمان رفتوبرگشت لازم برای همکاری کاربران و عاملها، یا استفادهٔ آنها از سامانههای دیگر متکی بر کنترل نسخه را اندازه میگیرد. این شبیهسازی، سختافزار را از ماشینهای مجازی و سامانههای ذخیرهسازی بلاب معمولی تا ردهبالا مدلسازی میکند. در این شبیهسازی، الزام سازگاری در سراسر محل نگهداری را برای ثبت سهمرحلهای کاهش دادهایم.
Simulating Git Server Implementations
Follow each writer through repeated pushes, and separately measure when an independent reader can check out each commit.
این شبیهسازی را اجرا کنید تا Pando را با دو رویکرد دیگر مقایسه کنید. زیرساخت را از سختافزار معمولی تا ردهبالا انتخاب کنید و تعداد کلاینتها و تنظیمات دیگر را تغییر دهید تا تفاوت عملکردشان را ببینید.
سامانهٔ ثبت سهمرحلهایِ مدلسازیشده، زیر بار و با اشباع دیسکها، تأخیر نوشتن بسیار بیشتری نشان میدهد. در مقابل، سامانههای مبتنی بر لاگ پیشنوشت به همین دلیل تأخیر خواندن بسیار بیشتری دارند، زیرا سرورها بهسختی میتوانند خواندن لاگ را همپای تغییرات پیش ببرند. هم در آزمونهای واقعی روی محل نگهداری یکپارچهٔ ما و هم در این شبیهسازی، 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 رویکرد متفاوتی دارد. بهجای روکردن همهٔ کارتها، فقط مجموعهای منتخب از شاخهها را به کلاینت اعلام میکند. این کار دو دلیل دارد:
ارائهٔ نمایی سازگار از همهٔ ارجاعها پرهزینه است. با وجود میلیونها شاخه، حجم اعلام ارجاعها بهتنهایی ممکن است از ۱۰۰ MiB بیشتر شود. برای این کار، هر سرور باید یا نگاشتی بهروز نگه دارد که سربار هماهنگی زیادی دارد، یا پرسوجویی از پایگاه داده انجام دهد که هزینهاش با تعداد ارجاعها رشد میکند.
با ارائهٔ تنها زیرمجموعهای از ارجاعها به کلاینت، کلاینت بستهای میسازد که فقط به اشیایی وابسته است که سرور صریحاً اجازهٔ حذف آنها از بسته را داده است. ردیابی وابستگیها به Pando اجازه میدهد فایل بستهای را که کلاینت در یک پوش ساخته، دوباره بهعنوان پاسخ یک واکشی بعدی ارائه کند.
هرچه سرور Git ارجاعهای بیشتری اعلام کند، کلاینتهای پوشکننده منابع بیشتری برای کنارگذاشتن اشیا دارند و میتوانند بستههای کوچکتری بسازند. اما استفادهٔ دوباره از این فایلهای بسته مستلزم آن است که سرور بتواند ثابت کند کلاینتهای واکشیکننده همهٔ وابستگیها را دارند. در غیر این صورت، ممکن است فایل بسته فاقد اشیایی باشد که کلاینت به آنها نیاز دارد.
از سوی دیگر، اگر سرور Git هیچ ارجاعی اعلام نکند، کلاینت فایل بستهای شامل کل تاریخچهٔ محل نگهداری میسازد. هر کلاینت دیگری میتواند از آن فایل بسته استفاده کند، اما بهای سنگینش این است که واکشی هر شاخه عملاً به یک کلون کامل تبدیل میشود.
اعلام ارجاعهای بیشتر به فایلهای بستهٔ کوچکتر منجر میشود. بخش عمدهٔ کاهش حجم میتواند تنها با اعلام یک ارجاع حاصل شود.
بهطور پیشفرض، وقتی کلاینت یک شاخهٔ قابلیت جدید را پوش میکند، Pando فقط یک شاخهٔ اصلیِ تعیینشده را بهعنوان مبنا اعلام میکند. با این روش، با افزایش شاخههای محل نگهداری، اندازهٔ پیام اعلام ثابت میماند، اما این انتخاب هزینههایی هم دارد. وقتی مهندسی بارها به یک شاخه پوش میکند یا یک شاخهٔ قابلیت را روی شاخهٔ قابلیت دیگری بنا میکند، کلاینت باید هرچه خارج از شاخهٔ اصلی است دوباره بفرستد.
در نتیجه، پوشهای متوالی شاخه بزرگتر میشوند، ممکن است اشیای تکراری داشته باشند و پهنای باند بیشتری از کلاینت مصرف میکنند. در محل نگهداری یکپارچهٔ ما، میانهٔ اندازهٔ فایل بستهای که با این روش تولید میشد زیر ۲۰ KiB و صدک ۹۹ آن زیر ۳ MiB بود. این بدهبستان برای ما منطقی بود، چون خوانندهای که تاریخچهٔ اعلامشدهٔ شاخهٔ اصلی را دارد، میتواند شاخه را با همان یک فایل بسته واکشی کند.
پس از انتخاب مجموعهٔ ارجاعهایی که باید به کلاینت اعلام شوند، Pando پوش را میپذیرد و ذخیرهساز فرادادهاش را بهروزرسانی میکند. Pando بهروزرسانی فراداده را به بخشهای همگام و ناهمگام تقسیم میکند. بهروزرسانی همگام فراداده تضمین میکند که کلاینتها بتوانند کامیت تازهبهروزشده را بلافاصله واکشی کنند. در این مرحله، نسخهٔ ارجاع جدیدی نوشته و نگاشت اشارهگر ارجاع به آن نسخه بهروزرسانی میشود؛ نوک جدید شاخه، وابستگیهای مورد استفاده در مذاکرهٔ فایل بسته و پیوندی به فایل بستهٔ پوششده نیز ذخیره میشوند.
همزمان، کارهای ناهمگام مانند زمانبندی فشردهسازی نیز در همان تراکنش ثبت میشوند. برای پوشهای بسیار بزرگ، فقط زیرمجموعهای از کامیتها بهصورت همگام نوشته میشوند. به این ترتیب، صرفنظر از اندازهٔ پوش، فقط به تعداد ثابتی خواندن و نوشتن در ذخیرهساز فراداده نیاز داریم.
نکتهٔ مهم این است که سرورهای Pando میتوانند پوش به ارجاعهای نامرتبط را مستقل بپذیرند. تنها کلیدی که بر سر آن رقابت وجود دارد، اشارهگر ارجاع است که با عملیاتی اتمی شبیه مقایسهوتعویض از آن محافظت میکنیم. میتوانیم با افزودن سرورهای Pando، واحدهای افزایش ظرفیت ذخیرهساز فراداده یا ذخیرهسازهای بلاب، توان عملیاتی نوشتن را افزایش دهیم.
وقتی کلاینت واکشی میکند، دو مجموعه از شناسههای کامیت را به سرور میدهد که در پروتکل با نامهای have و want شناخته میشوند. در سرور معمول Git، سرور تفاضل مجموعهٔ اشیای قابلدسترسی از خواستهها و مجموعهٔ اشیای قابلدسترسی از داشتهها را محاسبه و فشرده میکند و نتیجه را به کلاینت میفرستد. هرچند سرور متنباز 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 را به دنبالهای از نسخههای ارجاع نگاشت میکند. درخواست یک شاخهٔ جانبی، بهمعنای درخواست تاریخچهٔ شاخهٔ اصلی تا وابستگیهای آن شاخه نیز هست.
پروتکل Git در پاسخ HTTP به کلون یا واکشی، فقط اجازهٔ ارسال یک فایل بسته را میدهد. اما قابلیت کمتر شناختهشدهٔ packfile-uri در پروتکل Git به سرور اجازه میدهد پیوندهایی برای دانلود جداگانهٔ بستههای بیشتر برگرداند. Pando فقط از همین روش استفاده میکند و در بدنهٔ پاسخ، فایل بستهای خالی میفرستد. برای پشتیبانی از این قابلیت، OpenAI چندین بهبود در packfile-uri را به پروژهٔ اصلی ارائه کرد.
سامانههای OpenAI در مناطق جغرافیایی متعددی فعالاند؛ بنابراین در هر منطقه خوشههای Pando را با حافظهٔ نهان محلی نگه میداریم. سرویس تحویل بستهٔ Express دادههای ذخیرهساز بلاب ما را در حافظهٔ نهان نگه میدارد و سرویس جداگانهای همین کار را برای ذخیرهساز فراداده انجام میدهد. این حافظههای نهان به Pando اجازه میدهند درخواستهای مربوط به شناسهٔ کامیت یا نوع خاصی از خواندن ارجاع را کاملاً از وضعیت ذخیرهشده در حافظهٔ نهان همان خوشه پاسخ دهد. برای هدایت درخواستهای فایل بسته به سرورهای Express از هشسازی سازگار استفاده میکنیم و نرخ یافتن داده در حافظهٔ نهان بیش از ۹۹٪ است.
این سامانهٔ حافظهٔ نهان توان عملیاتی بسیار بالایی دارد و میتواند تمام ظرفیت شبکهٔ بین کلاینتها و Pando را به کار بگیرد. با استفاده از این URIهای فایل بسته و حافظههای نهان محلی، تأیید کردهایم که حتی فقط با ماشینهای مجازی استاندارد ارائهدهندگان ابر، عملیات کلون و واکشی تمام ظرفیت اتصالهای شبکهٔ ۱۰ گیگابیتبرثانیهای را مصرف میکند.
در بیشتر سرورهای Git، بستهبندی مجدد و جمعآوری زباله برای حفظ عملکرد محل نگهداریِ روبهرشد ضروریاند. هر دو مرحله فضا را آزاد میکنند، اما به محاسبات و ورودی/خروجی زیادی نیاز دارند.
در محل نگهداری پرتحرکی مانند محل نگهداری یکپارچهٔ 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 استفاده میکنند، خواندن یک شیء دلخواه از فایل بسته معمولاً در L0 نسبت به L4 به خواندن دادهٔ کمتری نیاز دارد، چون احتمال فشردهبودن آن کمتر است.
فشردهسازی در Pando حجم کل دادهٔ ارسالی به کلاینتها را بهطور چشمگیری کاهش میدهد و باعث میشود هزینهٔ برنامهریزی بستهها نسبت به میزان عقبماندگی کلاینت، رشدی زیرخطی داشته باشد. این موضوع بهویژه برای محلهای نگهداری بزرگ با تغییرات سریع اهمیت دارد؛ وضعیتی که انتظار داریم در توسعهٔ نرمافزار به روال معمول تبدیل شود.
Pando را با Codex ساختیم و از آغاز پروژه تا استقرار در محیط عملیاتی، فقط ۱۰ هفته زمان برد. هستهٔ Pando را یک مهندس طراحی کرد، تیمهایی از عاملها پیادهسازی کردند و گروه کوچکی از مهندسان مستقر کردند. Pando در آوریل امسال سرویسدهی به ترافیک عملیاتی را آغاز کرد و بخشی از مجموعهٔ بزرگتر ابزارهای مدیریت کنترل کد منبع در OpenAI است.
برای اعتبارسنجی طراحی Pando از رویکردهای متنوعی استفاده کردیم. اصول خود را برای ساخت زیربناهای نرمافزاری امنتر به کار گرفتیم. چون پروژه را از صفر شروع میکردیم، تصمیم گرفتیم Pando را با Rust و بدون وابستگیهای قدیمی پیادهسازی کنیم. مجموعهآزمونی برای انطباق نوشتیم تا عاملها بتوانند تغییرات خود را با رفتار استاندارد Git بسنجند. همچنین چارچوبهای آزمون شبیهسازی قطعی ساختیم تا ناورداها و ویژگیهای سامانه را بررسی کنیم و شرایط رقابتی ظریف و پسرفتها در مذاکره و برنامهریزی بستهها را بیابیم و برطرف کنیم.
مدلهای زیادی در ساخت این سامانه نقش داشتند؛ با عرضهٔ مدلهای جدید، از آنها برای بهبود عملکرد و قابلیت اطمینان Pando استفاده کردیم. عاملها با استفاده از GPT‑6 Astra، در Lean و TLA+ کد نوشتند تا «نظریهٔ Pando» را بهصورت صوری تدوین کنند. از آن برای یافتن و رفع اشکالهای پیادهسازی و اثبات ویژگیهای سامانه استفاده کردیم. چندین جلسهٔ پیوسته، صدها ساعت اجرا شدند تا حالتهای مرزی Pando را پیدا و رفع کنند. همچنین عاملها را شبانهروزی با مدلهای دارای قابلیت Daybreak اجرا میکنیم تا آزمونهای تیم قرمز را روی Git و Pando انجام دهند.
Pando را ابتدا بهعنوان پراکسیِ عبور خواندن و نوشتن به سامانهٔ کنترل کد منبع موجودمان مستقر کردیم. این کار به ما اجازه داد Pando و سامانهٔ قبلی را همگام نگه داریم و ترافیک را بدون وقفه و با اطمینان به Pando منتقل کنیم.
Pando در طراحی حافظهٔ نهان خود میتواند پیش از ارسال درخواستها به سامانهٔ پشت پراکسی، موارد تکراری را حذف و درخواستها را ترکیب کند. به این ترتیب، درحالیکه سامانهها را یکییکی به Pando منتقل میکردیم، ابزارهای موجودمان همچنان درست کار میکردند. با انتقال بارهای کاری بیشتر به Pando، پوشهای آنها حافظهٔ نهان Pando را پر میکرد و قابلیت اطمینان بکاند قبلی ما بهتر میشد.
برای بهینهسازی عملکرد هنگام استفاده از Pando، توزیع عمومی Git خود را روی دستگاههای توسعهدهندگان و درون برنامهٔ دسکتاپ ChatGPT عرضه کردیم. برای انتقال مرحلهای کلاینتها به Pando، از یک پوشش داخلی برای رابط خط فرمان Git استفاده کردیم. ابزارهای متعدد دیگری نیز برای رسیدگی به درخواستهای تغییر، بازبینی کد و APIهای خودکارسازی ساختیم. همراه با ترافیک Git، صدها یکپارچهسازی وابسته به APIهای قدیمی، وبهوکها و دانلود محلهای نگهداری را نیز منتقل کردیم.
خود Codex به توسعهدهندگان و تیمها در این انتقال کمک کرد. مهارتهایی برای آمادهسازی نسخههای کاری موجود و راهنمایی عاملها در استفاده از سامانهٔ جدید ایجاد کردیم. در نتیجه، توسعهدهندگان کافی بود از Codex بخواهند محیطهای توسعه و سرویسهایشان را منتقل کند.
انتقال به Pando قابلیت اطمینان ادغام را بهبود داد و میانهٔ تأخیر پوش را نزدیک به ۸۰٪ کاهش داد.
Pando میتواند تنها با چند هستهٔ پردازنده، روزانه یک پتابایت دادهٔ Git ارائه کند. Pando در یک ساعت بیش از ۵۷ ترابایت بستهٔ Git ارائه کرده است که بیش از ۹۹٪ بایتهای آن از حافظهٔ نهان دیسک محلی تأمین شدهاند.
استفادهٔ دوباره از بستهها به کلاینتهای متعدد اجازه میدهد تاریخچهٔ یکسان محل نگهداری را با محاسبات اندک روی گرههای سرویسدهنده دانلود کنند. این نمونه فقط یک محل نگهداری را نشان میدهد، اما Pando برای پشتیبانی از تعداد بسیار بیشتری طراحی شده است. با افزایش تقاضا، میتوانیم ظرفیت تحویل داده و مذاکرهٔ Git را مستقل از هم افزایش دهیم.
ساخت Pando بر پایهٔ پروتکل و کلاینت Git به ما کمک کرد اشکالها و فرصتهای بهبود عملکرد خود Git را شناسایی کنیم.
برای مثال، Pando بستههای قابلاستفادهٔ مجدد را مستقیم به کلاینتها ارائه میدهد؛ بنابراین دانلود و نمایهسازی آن بستهها به بخش مهمی از سریع و قابلاعتمادکردن سامانه تبدیل شد. این کار حالتهای مرزی مربوط به دانلودهای همزمان، پاکسازی پس از انتقالهای قطعشده و ازسرگیری دانلودهای قبلاً تکمیلشده را آشکار کرد. این مسیرهای اجرایی را اصلاح کردیم، هنگام پذیرش بستهٔ موجود برای واکشی اعتبارسنجیهایی افزودیم و این دستاوردها را به پروژهٔ اصلی برگرداندیم.
همچنین لازم بود بدانیم زمان و پهنای باند کجا مصرف میشوند؛ بنابراین ردیابی دانلودهای packfile-uri و شمارش بایتهای تولید بسته را اضافه کردیم تا تشخیص اینکه کندی واکشی ناشی از تولید بستههاست یا انتقالشان، آسانتر شود.
در نسخهٔ Git ساختهشده توسط OpenAI(در یک پنجره جدید باز میشود)، کلاینت میتواند بستههای مستقل را بهصورت موازی دانلود و نمایهسازی کند. مفهوم «داشتههای صریح» را اضافه کردیم تا سرور بتواند نوک فعلی شاخه را جدا از تاریخچهای اعلام کند که کلاینت پوشکننده مجاز است از بستهاش کنار بگذارد. در Pando، کلاینتها میتوانند با --force-with-lease بررسی کنند که شاخهٔ قابلیت جابهجا نشده است و بستهٔ آن را فقط بر مبنای شاخهٔ اصلی بسازند. سپس هرکسی که آن تاریخچهٔ شاخهٔ اصلی را داشته باشد، میتواند شاخه را با همان بسته واکشی کند.
چند مورد از این بهبودها اکنون در Git اصلی قرار دارند و بسیاری دیگر در محل نگهداری عمومی ما هستند. همکاری با جامعهٔ Git را ادامه میدهیم تا این بهبودها فراتر از Pando نیز مفید باشند. میتوانید این فعالیتها را در سامانهٔ عمومی پیگیری وصلههای ما(در یک پنجره جدید باز میشود) دنبال کنید.
Pando با بازنگری در دیدگاههای رایج دربارهٔ مقیاسپذیری محلهای نگهداری Git، به ما امکان میدهد ظرفیت محل نگهداری یکپارچهٔ خود را افزایش دهیم، بدون آنکه گردشکارهای Git مورد استفادهٔ مهندسان و عاملهایمان تغییر کند. Pando در محل نگهداری یکپارچهٔ ما، امکان اجرای موازی بیشتر را فراهم کرده، قابلیت اطمینان ادغام را بهبود داده و زمان انتشار تغییرات کد را کاهش داده است. علاوه بر محل نگهداری یکپارچه، کاربردهای دیگر Pando را نیز در OpenAI بررسی میکنیم؛ از جمله بارهای کاریای که در آنها تعداد زیادی عامل باید روی یک محل نگهداری مشترک Git همکاری کنند.
با افزایش تولید نرمافزار توسط مهندسان و عاملها، تیمهای بیشتری خارج از OpenAI نیز با محدودیتهایی روبهرو خواهند شد که ما در کنترل کد منبع تجربه کردیم. اکنون در حال بررسی راههایی هستیم تا فناوری زیربنای Pando را در اختیار کاربرانمان قرار دهیم.
Pando بر دو دهه تجربهٔ کاربران و گردانندگان محلهای نگهداری Git تکیه دارد. توسعهٔ Pando را ادامه خواهیم داد و همزمان با جامعهٔ متنباز Git همکاری میکنیم تا بهبودهایی را که به سود طیف گستردهتری از کاربران Git است، به اشتراک بگذاریم.


