Submodule и LFS в CI: где теряются минуты
На удалённом узле Mac CI задержка складывается из последовательных этапов: рукопожатие с каждым origin подмодуля, разрешение ссылок LFS, пакетная выгрузка крупных объектов и запись на диск. Если подмодули вложены или указывают на разные хосты, холодный прогон превращается в «водопад» запросов. LFS добавляет отдельный batch-API: при низком lfs.concurrenttransfers вы оставляете пропускную способность неиспользованной; при слишком высоком — упираетесь в лимиты провайдера или в исчерпание файловых дескрипторов.
Рекомендуемый порядок инициализации submodule (после клонирования корня): синхронизировать URL из .gitmodules, затем параллельно инициализировать и обновить дерево. Пример последовательности:
- 1.
git submodule sync --recursive— привести remote подмодулей к актуальным URL из.gitmodules(важно при смене зеркала). - 2.
git submodule update --init --recursive --jobs 4— параллельная выгрузка до четырёх подмодулей одновременно (подберите--jobsпод CPU и лимиты сети). - 3. При shallow-политике: после checkout суперпроекта выполнить
git submodule foreach --recursive 'git fetch --depth=1 origin HEAD && git checkout FETCH_HEAD'или задатьsubmodule.<name>.shallow trueв конфиге там, где это поддерживается вашим хостингом. - 4. LFS: при массовых объектах сначала
export GIT_LFS_SKIP_SMUDGE=1на этапеsubmodule update, затем один разgit lfs installиgit lfs pullуже в корне и при необходимостиgit submodule foreach --recursive 'git lfs pull'.
Такой порядок уменьшает лишние smudge-проходы и даёт предсказуемую нагрузку на LFS-сервер. Общие принципы кеширования для Git см. также в материале о стабильности Git и повторах.
Параллельная выгрузка и политика lockfile: сравнение
Выбор между «максимальной параллелью» и «жёсткой фиксацией версий» зависит от того, монорепозиторий ли у вас с закреплёнными SHA подмодулей или несколько независимых репозиториев с отдельными pipeline. Таблица ниже помогает зафиксировать компромисс для удалённого Mac CI.
| Критерий | Монорепо + submodule на фиксированных SHA | Несколько репо / плавающие ветки подмодулей |
|---|---|---|
Параллель submodule update |
--jobs 4–8, стабильный кеш объектов |
--jobs 2–4 + чаще промах кеша по ветке |
| Lockfile / фиксация | SHA в суперпроекте = единственный источник истины; ключ кеша от .gitmodules + gitlink |
Нужны отдельные lock-артефакты или конвенция по тегам; ключ сложнее |
| LFS: риск лимитов | Пакеты объектов предсказуемы; можно поднять concurrenttransfers |
Выше риск 429/квот; снижайте конкуренцию |
| Инкрементальный fetch | git fetch --depth=1 + сохранённый .git на раннере |
Частые полные пересборки URL; проще от зеркала с низкой задержкой |
| Рекомендация для iOS/macOS | Закреплять подмодули, кешировать LFS и DerivedData отдельно | Ввести политику «только теги» для зависимых репо и ночной прогрев кеша |
Кеш и инкрементальный fetch: чеклист параметров
На удалённом Mac выгодно переиспользовать не только npm/Pods, но и объекты Git и LFS. Ниже — ориентиры для каталогов, ключей и команд fetch (подставьте пути к своему общему тому или образу раннера).
Если раннер эфемерный, разделяйте два уровня кеша: быстрый локальный на SSD узла (снимок .git после успешного прогона) и медленный восстанавливаемый в объектном хранилище или NFS с ключом, привязанным к коммиту суперпроекта. Так вы избегаете ситуации, когда при смене одного подмодуля инвалидируется гигабайтный архив целиком: включайте в ключ только идентификаторы изменившихся gitlink и хеш .gitmodules, а общие объекты оставляйте в alternates или в общем LFS-каталоге.
- Каталог LFS: по умолчанию
~/.git/lfs/objects; в CI задайтеgit config --global lfs.storage "/Volumes/Shared/lfs-cache"(или общий том) и монтируйте его между джобами. - Ключ кеша (пример):
sha256(.gitmodules)+ короткие SHA gitlink суперпроекта для каждого submodule + версия Xcode/агента; избегайте ключа только от ветки без коммита. - Инкремент:
git fetch origin +refs/heads/$BRANCH:refs/remotes/origin/$BRANCH --prune --depth=50затемgit submodule update --init --recursive --jobs N; при неизменных подмодулях пропускайте полныйforeach. - Альтернативные объекты: при нескольких клонах одного зеркала используйте
GIT_ALTERNATE_OBJECT_DIRECTORIESна общийobjects(осторожно с правами и утечкой кросс-репо).
git config --global lfs.concurrenttransfers 16— до 16 параллельных загрузок объектов (начните с 8 и измеряйте).git config --global lfs.batchsize 100— размер batch-запроса к API (на GitHub Enterprise иногда безопаснее 50).git config --global lfs.activitytimeout 300иlfs.dialtimeout 60— устойчивость на длинных объектах и нестабильном маршруте.- Ограничение исходящей полосы (если нужно делить канал с CocoaPods):
git config --global lfs.basictransfersonly trueв редких средах без multipart; чаще полезнееlfs.concurrenttransfers 4.
Сбои, повторы и таймауты: пороговые значения
Зафиксируйте в pipeline явные числа, чтобы ночные сбои не превращались в ручной разбор. Ниже — практичные ориентиры для скриптов оболочки или оркестратора (GitHub Actions, Jenkins, Buildkite и т.д.).
Git fetch / submodule. Три попытки с экспоненциальной задержкой: базовая пауза 5 с, множитель 2, максимум 60 с; таймаут команды 45–90 с на один fetch (зависит от размера истории).
LFS batch. При HTTP 429 или сетевом обрыве: уменьшить lfs.concurrenttransfers вдвое и повторить; не более 5 попыток на весь этап LFS.
Зеркало → прямой доступ. Переключаться на origin, если подряд три ошибки на зеркале, или если p95 задержка TCP/TLS к зеркалу > 2–3 с при стабильном пинге до публичного DNS; логируйте факт переключения в артефакты CI.
Сеть и диск удалённого Mac: быстрые проверки перед джобой
LFS и подмодули быстро заполняют том: перед тяжёлым прогоном проверьте свободное место и задержку до вашего Git/LFS endpoint. На macOS достаточно df -h по тому с ~ и рабочими каталогами; при использовании APFS обратите внимание на общий пул «System Data». Для сети выполните networkQuality (если доступно) или измерьте RTT до хоста зеркала и до первоисточника — расхождение в пользу зеркала должно быть устойчивым, иначе нет смысла в дополнительном звене.
- Диск: держите минимум 20–30 ГБ свободными на томе с
lfs.storage(или домашним каталогом LFS) и рабочей копией до старта джобы с крупными ассетами. - Сеть: зафиксируйте в runbook порог потерь пакетов > 1–2% к LFS-хосту как сигнал к переключению маршрута или VPN/релея.
- CPU/IO: при
--jobs 8на малом Mac следите заsysctl hw.ncpuи временем ожидания диска; иногда выгоднее 4 параллельных submodule, чем контенция на SSD.
FAQ: авторизация, квоты и зеркала
Почему LFS пишет «credentials not found» только в подмодуле? Часто lfs.url или insteadOf заданы в корне, но подмодуль клонирован с другим хостом. Пропишите учётные данные для каждого базового URL или используйте единый .netrc/CI-секрет с шаблоном хоста.
Как отличить квоту от обычного таймаута? Коды 402/403 в ответе batch API, сообщения о billing или резкий обрыв после N успешных объектов. Снизьте параллелизм, проверьте биллинг LFS у провайдера и убедитесь, что зеркало не режет анонимный трафик.
Стоит ли отключать LFS в CI? Только если ассеты подтягиваются иным каналом (внутренний CDN) и коммиты без LFS воспроизводимы. Иначе сборка Xcode может молча получить pointer-файлы вместо бинарников.
Почему submodule update «зависает» на одном репозитории? Часто это не зависание, а ожидание блокировки на сервере при слишком большом --jobs или антивирус/Time Machine, душащие I/O на macOS. Снизьте параллель, включите GIT_TRACE_PACKET на короткий прогон для диагностики и проверьте, не ушёл ли трафик в медленное зеркало без таймаута.
Итог
Submodule + LFS на удалённом Mac CI управляются не «магией», а порядком команд, разумной параллелью, ключами кеша и явными порогами повторов. Закрепите SHA подмодулей, синхронизируйте URL, отложите smudge, настройте lfs.concurrenttransfers и мониторьте диск. Чтобы сократить время pull и стабилизировать сборки iOS/macOS, выберите узел рядом с вашим трафиком и управляйте кешем сами: тарифы MacPull, оформление аренды удалённого Mac, центр помощи, главная и блог с гайдами по Git и ускорению зависимостей.
Удалённый Mac для стабильного CI с Submodule и LFS
Нативный macOS, SSH/VNC и полный контроль над кешем Git/LFS. Оформите аренду или откройте тарифы — без лишних шагов.