Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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-репозиторіями на сусідні пакети, збирачі, що шукають конфіги в корені), ламаються - набір має містити всі залежності;
  • для невеликого проєкту накладні витрати не виправдані.

Докладніше в документації: git sparse-checkout

Обидва способи зменшують обсяг завантаження, але обрізають різні речі.

Поверхневий клон (--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-пакетом: документація, спільні конфіги, ресурси. А якщо спільний код змінюється разом із застосунками постійно, - можливо, вам потрібен монорепозиторій.

Докладніше в документації: Pro Git: підмодулі

Монорепозиторій - кілька проєктів (застосунки, пакети, сервіси) в одному репозиторії. 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;
  • ключове питання: як часто зміна в одному проєкті вимагає зміни в іншому. Часто - монорепозиторій, рідко - окремі репозиторії.

Докладніше в документації: monorepo.tools

З часом у репозиторії накопичуються незапаковані об'єкти (кожен новий коміт створює окремі файли), недосяжні об'єкти (після 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) обслуговування виконує хостинг.

Докладніше в документації: git maintenance