Middle: питання на співбесіді з теми «Великі репозиторії й продуктивність»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
У великому монорепозиторії робочий каталог може містити сотні тисяч файлів, з яких розробнику потрібні кілька каталогів. Sparse-checkout лишає в робочому каталозі лише вказані шляхи - решта файлів є в історії, але не на диску.
git clone --filter=blob:none --sparse https://github.com/acme/monorepo.git
cd monorepo
git sparse-checkout set apps/billing packages/ui
git sparse-checkout add packages/auth
git sparse-checkout list
git sparse-checkout disable # повернути всі файли
Режим cone (за замовчуванням) - шаблони лише на рівні каталогів:
- до робочого каталогу потрапляють файли в корені репозиторію, вказані каталоги цілком і файли в їхніх батьківських каталогах (без вкладених підкаталогів);
- Git перевіряє шляхи значно швидше, ніж у режимі довільних шаблонів у стилі
.gitignore, тому для великих репозиторіїв cone рекомендований.
Що це дає:
git status,git checkout,git switchпрацюють з меншою кількістю файлів - значно швидше;- IDE індексує лише потрібний код;
- менше місця на диску.
Найкраще - разом з частковим клоном (--filter=blob:none): тоді вміст файлів поза sparse-набором навіть не завантажується з сервера.
Як інші операції поводяться з невидимими файлами:
- коміти, злиття й rebase працюють з усім деревом - Git знає про всі файли;
- конфлікт у файлі поза sparse-набором - Git тимчасово розміщує файл у робочому каталозі для розв'язання;
git grepіgit log -- pathможуть шукати й поза набором.
Типове застосування:
- монорепозиторій: розробник фронтенду бачить лише
apps/webі спільні пакети; - CI: збирання конкретного сервісу без виписування всього репозиторію - разом з
--depth 1і--filter; - документація в репозиторії з великим кодом.
Обмеження:
- інструменти, яким потрібні файли поза набором (наприклад, Composer з
path-репозиторіями на сусідні пакети, збирачі, що шукають конфіги в корені), ламаються - набір має містити всі залежності; - для невеликого проєкту накладні витрати не виправдані.
Обидва способи зменшують обсяг завантаження, але обрізають різні речі.
Поверхневий клон (--depth 1) обрізає історію: комітів до певної глибини просто немає.
Частковий клон (--filter) завантажує всю історію комітів, але не всі об'єкти - пропущені догружаються з сервера на вимогу:
git clone --filter=blob:none https://github.com/acme/app.git # blobless
git clone --filter=tree:0 https://github.com/acme/app.git # treeless
git clone --filter=blob:limit=1m https://github.com/acme/app.git # без файлів понад 1 МБ
| Поверхневий | Без blob-ів (blob:none) |
Без дерев (tree:0) |
|
|---|---|---|---|
| коміти | лише останні | усі | усі |
| дерева (каталоги) | лише останні | усі | на вимогу |
| вміст файлів | лише останні | лише для checkout, решта на вимогу | на вимогу |
git log |
обрізаний | повний | повний |
git log -p, blame |
обмежені | працюють, догружаючи файли | повільні, багато догрузок |
| для кого | CI, одноразові збирання | розробники | CI, якому потрібна історія комітів |
Чому blobless клон - добрий вибір для розробника великого репозиторію:
- історія повна:
log,merge-base,describe, перемикання гілок працюють як звичайно; - старі версії великих файлів не завантажуються, поки не знадобляться;
git blameчи перегляд старого коміту догружають потрібні об'єкти - помітна, але одноразова затримка.
Чому поверхневий клон - для CI: найменший обсяг і жодних мережевих запитів пізніше. Але все, що потребує історії, не працює.
Treeless клон економить найбільше серед часткових, але операції з історією файлів роблять багато запитів до сервера - для щоденної роботи зазвичай повільно.
Що потрібно:
- підтримка на сервері (GitHub, GitLab, Bitbucket підтримують);
- доступ до сервера при операціях, що потребують відсутніх об'єктів - офлайн частина команд не працюватиме;
- не поєднувати бездумно з
--depth- зазвичай обирають одне.
Разом зі sparse-checkout частковий клон дає найкращий ефект для монорепозиторію: не завантажуються ні старі версії, ні файли з чужих каталогів.
Докладніше в документації: GitHub Blog: partial clone і shallow clone
Кілька Laravel-застосунків використовують спільний код: модуль авторизації, клієнт API, набір Blade-компонентів. Є три основні способи.
1. Submodule - посилання на коміт іншого репозиторію:
git submodule add git@github.com:acme/shared.git packages/shared
- точна версія, окрема історія, окремі права доступу;
- незручно: додаткові кроки при клонуванні й оновленні, detached HEAD, легко забути оновити вказівник.
2. Subtree - код копіюється в репозиторій разом з історією:
git subtree add --prefix=packages/shared git@github.com:acme/shared.git main --squash
git subtree pull --prefix=packages/shared git@github.com:acme/shared.git main --squash
git subtree push --prefix=packages/shared git@github.com:acme/shared.git feature-x
- для інших учасників це просто звичайні файли - клон працює без додаткових кроків;
- зміни можна відправити назад в оригінальний репозиторій;
- команди довгі, легко змішати в одному коміті зміни спільного й основного коду;
- історія основного репозиторію росте.
3. Composer-пакет - стандартний спосіб для PHP:
{
"repositories": [
{ "type": "vcs", "url": "git@github.com:acme/shared.git" }
],
"require": { "acme/shared": "^2.1" }
}
- семантичні версії й діапазони, власні залежності пакета, автозавантаження, сервіс-провайдер Laravel;
- оновлення -
composer update acme/shared, аcomposer.lockфіксує точну версію; - приватні пакети - через Private Packagist, Satis чи
vcs-репозиторій з доступом.
Для локальної розробки пакета разом із застосунком - path-репозиторій:
{ "repositories": [{ "type": "path", "url": "../shared" }] }
Composer створює символьне посилання - зміни в пакеті видно одразу.
Порівняння:
| Submodule | Subtree | Composer | |
|---|---|---|---|
| версіонування | коміт | коміт | семантичні версії |
| залежності пакета | ні | ні | так |
| простота для команди | низька | висока | висока |
| зміни в обидва боки | так | так (subtree push) |
у репозиторії пакета |
Для PHP-коду Composer майже завжди кращий. Submodule і subtree доречні для того, що не є PHP-пакетом: документація, спільні конфіги, ресурси. А якщо спільний код змінюється разом із застосунками постійно, - можливо, вам потрібен монорепозиторій.
Монорепозиторій - кілька проєктів (застосунки, пакети, сервіси) в одному репозиторії. Polyrepo - кожен проєкт в окремому репозиторії.
Переваги монорепозиторію:
- атомарні зміни: змінити API пакета й усіх його споживачів одним комітом і одним pull request. У polyrepo це кілька узгоджених релізів у правильному порядку;
- немає пекла версій: усі проєкти завжди використовують поточну версію спільного коду;
- спільні інструменти: одна конфігурація Pint, PHPStan, CI, однакові правила;
- видимість: легко знайти всі використання функції, зробити рефакторинг по всьому коду;
- простіший онбординг: один клон - увесь контекст.
Проблеми монорепозиторію:
- розмір і продуктивність Git:
status,clone,checkoutповільнішають (лікується частковим клоном, sparse-checkout, fsmonitor); - CI: запускати все на кожну зміну - дорого. Потрібні інструменти, що визначають, які проєкти зачеплені зміною (Nx, Turborepo, Bazel, Pants чи власні скрипти по
git diff); - права доступу: Git не обмежує доступ до каталогів - бачать усі все (CODEOWNERS лише керує рев'ю);
- зв'язність: легко створити залежності, яких не мало б бути, - потрібні правила меж між модулями.
Переваги polyrepo:
- незалежні релізи, власний темп і власні інструменти кожної команди;
- чіткі межі й права доступу;
- маленькі швидкі репозиторії.
Проблеми polyrepo:
- зміни через кілька репозиторіїв - кілька pull request, порядок випусків, тимчасова несумісність;
- розбіжність версій: сервіси застрягають на старих версіях спільних пакетів;
- дублювання конфігурацій CI, лінтерів, шаблонів.
Гібрид, яким користується Laravel: розробка в монорепозиторії laravel/framework, а компоненти автоматично розділяються в окремі репозиторії лише для читання (illuminate/database, illuminate/support), щоб їх можна було встановити окремо.
Як обрати:
- кілька тісно пов'язаних проєктів однієї команди (застосунок, адмінка, спільні пакети) - монорепозиторій дає більше, ніж коштує;
- незалежні продукти різних команд з різним циклом випусків - polyrepo;
- ключове питання: як часто зміна в одному проєкті вимагає зміни в іншому. Часто - монорепозиторій, рідко - окремі репозиторії.
З часом у репозиторії накопичуються незапаковані об'єкти (кожен новий коміт створює окремі файли), недосяжні об'єкти (після rebase, reset, видалення гілок) і багато pack-файлів після кожного fetch. Це сповільнює операції й займає місце.
git gc (garbage collection):
- пакує окремі об'єкти в pack-файли з дельта-стисненням;
- видаляє недосяжні об'єкти, старші за термін (за замовчуванням 2 тижні, і лише ті, на які не посилається reflog - а reflog зберігає записи 90 днів, недосяжні - 30);
- упаковує посилання в
packed-refs, оновлює commit-graph.
git gc # звичайне прибирання
git gc --aggressive # повільне глибоке перепакування - рідко потрібне
git gc --prune=now # видалити недосяжні об'єкти негайно
git gc --auto Git запускає сам після деяких команд (commit, merge, fetch), коли незапакованих об'єктів понад 6700 чи pack-файлів понад 50. Тому вручну запускати gc зазвичай не потрібно.
Проблема автоматичного gc у великих репозиторіях: він запускається посеред роботи й може блокувати на хвилини.
git maintenance - сучасна заміна: обслуговування у фоні за розкладом:
git maintenance start
Реєструє репозиторій і задачі в системному планувальнику (launchd на macOS, systemd чи cron на Linux, Task Scheduler на Windows):
| Задача | Частота | Що робить |
|---|---|---|
prefetch |
щогодини | фоновий fetch у спеціальні ref-и - ваш git fetch потім майже миттєвий |
commit-graph |
щогодини | оновлює граф комітів для швидкого log і злиттів |
loose-objects |
щодня | пакує окремі об'єкти |
incremental-repack |
щодня | поступово об'єднує pack-файли без повного перепакування |
git maintenance run --task=gc # вручну конкретну задачу
git maintenance stop
Коли запускати вручну:
- після видалення великих файлів з історії (
git filter-repo) -git gc --prune=now, щоб звільнити місце; - після масового імпорту чи міграції репозиторію;
- для невеликого проєкту нічого робити не треба - автоматичного
gcдостатньо.
На сервері (GitHub, GitLab) обслуговування виконує хостинг.