Як GitHub переносить мільярд репозиторіїв без зупинки сервісу?

GitHub скопіював 99,99999 % репозиторіїв до Azure. Остання частка залишилася через мережевий round-trip у 25 мілісекунд.

Чому почався перенос

Час і форма міграції диктував перехід до агентної архітектури: швидко зростаючий трафік, жорсткий ліміт потужності дата-центрів GitHub і сусідніх регіонів, обмеження ланцюгів постачання. На початку вільної потужності з урахуванням усіх можливих розширень вистачило, щоб мігрувати безпечно, але не повільно. Git-операції — це Tier 0 сервіс. Майже все в GitHub залежить від даних репозиторіїв. Тому дані та сервіси мали переїхати першими. Кожна залежність теж повинна була переїхати рано. Перша копія потрапила в регіон Azure Central US у серпні 2025 року. Сьогодні більшість репозиторіїв мають дві з п'яти реплік у Azure, і Azure обробляє близько половини Git-трафіку. Довгий час Azure поглинав зростання, тоді як оригінальні дата-центри працювали стабільно. Перенос не завершено.

Вузьке місце, яке затримало найзавантаженіші репозиторії

У поточній архітектурі репозиторій, що розміщений у кількох регіонах, має нижчу межу пропускної здатності на запис. Кворум Spokes потребує більшості для коміту, але чекає відповіді від кожної здорової репліки. Найповільніша з них задає латентність кожного запису. Оскільки запис утримує блокування всього репозиторію до коміту, ця репліка визначає і потолок пропускної здатності. Коли репліка з'являється в цільовому регіоні Azure, вона знаходиться приблизно в 1000 милях від дата-центрів. Кожен мережевий round-trip займає близько 25 мс, а запис вимагає кілька таких поїздок. Репозиторій, вже близький до свого потолку, не може поглинути це на кожному серіалізованому записі. Він буде відставати з кожним push.

Кворуми на рівні посилань розблокують записи

Вимоги Git до узгодженості діють на кожне посилання окремо. Коли сервер приймає оновлення посилання, жоден клієнт не повинен бачити старе значення пізніше. Це справедливе для fast-forward та force push. Також це справедливе для видалення. Але оновлення різних посилань не залежать одне від одного і можуть застосовуватися незалежно. GitHub змінює область кворуму з всього репозиторію на окремі посилання. Утримання блокування всього репозиторію на час оновлення перетворює кворум на єдину чергу без впорядкування — один запис у полеті. З серіалізацією на рівні посилань чекають лише записи, що конфліктують за спільним посиланням. На практиці більшість записів до репозиторію торкаються різних посилань — агенти та розробники пушать у власні гілки — тому вони більше не чекають один одного. Кожен запис все ще відчуває міжрегіональну латентність, але записи до різних посилань можуть бути в полеті одночасно. Це прибирає єдине вузьке місце репозиторію. У внутрішніх тестах з реалістичним трафіком записи до різних посилань стабільно показували в 3–4 рази вищу пропускну здатність при нижчій латентності.

Масштабування читання через віртуальні репліки

Більшість репозиторіїв сильно нахилені до читання — десь від 100 до 1000 читань на один запис. Сьогодні читання й запис потрапляють на ті самі репліки, тому вони конкурують за однакові ресурси і не масштабуються незалежно. Коли дані репозиторію стають стійко збереженими в Azure Blob Storage і оновлюються з кожним вхідним записом, трафік читання можна відокремити від кворуму запису. Перший крок — зробити об'єктне сховище учасником кворуму — спеціальним учасником, яким є віртуальна репліка. Обчислення, що її представляє, — перший повністю ефемерний вузол. Він слугує мостом до об'єктного сховища, стрімить вхідні записи. Читання він не обслуговує. Оскільки це учасник кворуму, запис не підтверджується, доки не стане стійким у об'єктному сховищі. Це додає трохи латентності до кожного запису, але серіалізація на рівні посилань не дозволяє їй стати вузьким місцем пропускної здатності. Віртуальна репліка запису також об'єднує транзакції на неперетинних посиланнях у єдиний набір мережевих I/O до об'єктного сховища, тому завантажений репозиторій платить за один round-trip на багато одночасних записів замість одного на запис. З віртуальною реплікою запису на місці GitHub може масштабувати стільки віртуальних реплік читання, скільки потрібно репозиторію, на вимогу. Кожна репліка читання прогріває свій кеш з Azure Blob Storage і починає приймати трафік читання без вступу до кворуму, використовуючи об'єктне сховище як джерело істини і узгоджуючи свій локальний стан з ним у фоні. У внутрішніх тестах розподілений harness для навантажувального тестування регулярно знаходив потолки в поточній архітектурі. Проти віртуальних реплік читання harness вдарив у власні ліміти першим.

Що далі

Протокол коміту, що довів GitHub до мільярда репозиторіїв, не доведе його до наступного мільярда і далі. Кожен запис все ще чекає на репліки, кожна з яких тримає повну копію репозиторію, хоча дуже мало в записі дійсно потребує впорядкування. Git-об'єкти незмінні і іменовані своїм вмістом, тому їх можна зберігати без узгодження. Що залишилося — мало, а важче — впорядкувати це якомога швидше, не переносячи вузьке місце кудись інше. Проектувати це на дошці — одне. Робити це під мільярдом репозиторіїв, у які розробники і агенти пишуть цілодобово — інше. Це змусило передумати і про вторинні ефекти: де живе стійкий стан посилань і як масштабувати компактацію та збирання сміття. Агентський трафік не сповільнюється, і кожен з цих кроків дає репозиторіям більше запасу потужності, коли він викачується, не чекаючи кінцевого стану. Остання стаття цієї серії з'явиться на GitHub Universe і проведе туре кінцевого стану, його впливу на пропускну здатність запису, вторинних проблем, що довелося вирішити, і чому десь у стеку все ще існує невелика група консенсусу.

Джерела, за якими написано статтю

  • "GitHub Blog
    https://github.blog/engineering/architecture-optimization/the-bridge-to-agent-scale-git-infrastructure/"

Читайте також