- Кэш DerivedData на одном Mac не существует на втором Runner по умолчанию — скрытая цена multi-node iOS CI в том, что каждая машина холодно стартует сама по себе
- Делить можно слой артефактов сборки (DerivedData, результат разрешения SPM, Pods/) — не «несколько машин одновременно пишут в один каталог»; pull → build → push или региональный cache hub — mainstream-подход 2026 года
- На флоте Cloud Mac используйте хеш lockfile + версию Xcode как cache keys с rsync/object storage — warm-сборки часто экономят ещё 5–15 минут (поверх оптимизации кэша на одной машине)
Вы настроили actions/cache в GitHub Actions, и warm-сборки упали с 18 до 9 минут — затем масштабировались до трёх self-hosted M4 Runner, и P95 снова пополз к 16 минутам. Каждая машина «видит этот commit впервые».
Это не откат к нулю, а фаза два multi-node сборок: общий кэш Xcode. Статья не про «зачем кэш» (см. гайд по CocoaPods / SPM / DerivedData на одной машине), а про как синхронизировать данные сборки между несколькими Mac, чтобы любой Runner переиспользовал модули, которые уже скомпилировал коллега.
1. Почему multi-node CI медленнее одной машины
Когда балансировка случайно назначает jobs на Runner A/B/C, состояние диска каждой машины независимо. MyAppKit.swiftmodule, который Runner A только что скомпилировал, бесполезен для Runner B — пока вы не перенесёте артефакты.
Runner’ы GitHub используют actions/cache, чтобы хранить tarballs в облаке и восстанавливать при старте job. Self-hosted или флоты Cloud Mac часто не имеют аналогичного managed-слоя, поэтому команды либо:
- Смиряются с прогревом на каждой машине (лишние вычисления)
- Монтируют каталоги кэша по NFS с параллельной записью (риск порчи)
- Строят cache hub: pull перед сборкой, push после успеха
Третий вариант самый распространённый среди iOS-команд и подходит для multi-region развёртываний: три M4 в US East делят один cache bucket, два в Asia-Pacific — другой, а между регионами синхронизируется только слой SPM/Pods.
2. Слои данных сборки Xcode
Перед sync разделите, что стоит передавать, и что нельзя смешивать.
2.1 DerivedData
Каталог, на который указывает xcodebuild -derivedDataPath, с скомпилированными .o, .swiftmodule, промежуточными файлами линковки и частичными индексами. Объём часто достигает нескольких ГБ — это главный рычаг скорости warm-сборок и слой, наиболее чувствительный к версии Xcode.
Зафиксируйте путь в CI, например /Volumes/CI/DerivedData/<scheme>, совпадающий с переменной REMOTE в sync-скрипте — в статье про кэш на одной машине подчёркивалось: разные пути — нет кэша вообще.
2.2 SPM и CocoaPods
SPM: ~/Library/Caches/org.swift.swiftpm + .build в репозитории (если коммитите результат разрешения). CocoaPods: Pods/ и ~/Library/Caches/CocoaPods.
Эти слои меняются реже DerivedData (следуют lockfile), поэтому подходят для sync между машинами и даже между регионами. Многие команды пушат в S3 только SPM/Pods, а DerivedData держат регионально через rsync.
2.3 ModuleCache и глобальные кэши Xcode
~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex и различные SDKStatCaches. Обычно обрабатываются вместе с DerivedData; при отдельном sync требуйте одинаковую minor-версию Xcode.
3. Четыре архитектуры sync multi-node
| Паттерн | Подход | Плюсы | Риски |
|---|---|---|---|
| Только локально | У каждого Runner свой диск, без sharing | Нулевые ops | Горизонтальное масштабирование не экономит время |
| Общий NFS-mount | DerivedData на сетевом томе, read-only или read-write | Простая настройка | Параллельная запись легко портит данные; сетевая задержка тормозит linking |
| rsync hub | Центральный каталог или leader-машина; pull перед сборкой, push после успеха | Контролируемо, хорошая совместимость с Xcode | Нужны скрипты и блокировки |
| Tarball в object storage | Упаковка и загрузка в S3/R2 по cache key; распаковка при старте job | Межрегионально, близко к модели GHA cache | Большие загрузки DerivedData медленны; нужно сжатие |
В практике 2026 года флоты Cloud Mac в одном ЦОД / регионе предпочитают rsync hub; глобальные команды используют object storage для SPM/Pods и держат DerivedData регионально. Не считайте NFS серебряной пулей для multi-writer shared DerivedData.
Никогда не запускайте xcodebuild на двух Runner’ах одновременно против одного дерева DerivedData. Если нужно делить том — файловые блокировки или single-writer multi-reader (один designated warmer собирает, затем rsync раздаёт).
4. Cache keys и инвалидация
Ключи общего multi-node кэша должны быть консервативнее, чем на одной машине — ложное попадание хуже холодного старта (загадочные ошибки линковки, сбои подписи).
Поля для ключа:
xcodebuild -versionили переменная окруженияXCODE_VERSIONhashFiles('**/Podfile.lock')илиPackage.resolved- Имя scheme или набор targets (отдельные кэши для multi-scheme репозиториев)
arm64(префикс только Apple Silicon — избегает legacy x86-загрязнения)
Не используйте полный github.sha как единственный ключ — иначе каждая машина всегда промахивается. Слойные restore-keys: dd-arm64-main-<lockhash> → dd-arm64-main- → dd-arm64-.
Инвалидировать при: обновлении Xcode, смене lockfile, SWIFT_VERSION или крупном изменении Build Settings, или после ручного clean build — увеличьте суффикс ключа или удалите удалённый каталог.
5. Практика: скрипт rsync pull/push
Ниже минимальный скрипт, проверенный на флоте M4 Cloud Mac. Cache hub находится на cache-leader.internal:/cache/ios/ (машина с большим диском во флоте или NAS).
#!/usr/bin/env bash
# cache-sync.sh — 在 xcodebuild 前后调用
set -euo pipefail
ACTION="${1:?pull|push}"
CACHE_ROOT="${CACHE_ROOT:-cache-leader.internal:/cache/ios}"
SCHEME="${SCHEME:-MyApp}"
KEY="${CACHE_KEY:-arm64-$(md5 -q Podfile.lock 2>/dev/null || echo nolock)-$(xcodebuild -version | head -1 | tr ' ' '-')}"
LOCAL_DD="${DERIVED_DATA_PATH:-/Volumes/CI/DerivedData/${SCHEME}}"
REMOTE="${CACHE_ROOT}/${KEY}/${SCHEME}/DerivedData"
LOCAL_SPM="${HOME}/Library/Caches/org.swift.swiftpm"
REMOTE_SPM="${CACHE_ROOT}/${KEY}/swiftpm"
rsync_opts=(-az --delete-delay --contimeout=10)
case "$ACTION" in
pull)
rsync "${rsync_opts[@]}" "${REMOTE}/" "${LOCAL_DD}/" || true
rsync "${rsync_opts[@]}" "${REMOTE_SPM}/" "${LOCAL_SPM}/" || true
;;
push)
# 仅成功构建后调用
rsync "${rsync_opts[@]}" "${LOCAL_DD}/" "${REMOTE}/"
rsync "${rsync_opts[@]}" "${LOCAL_SPM}/" "${REMOTE_SPM}/"
;;
*) echo "usage: $0 pull|push" >&2; exit 1 ;;
esac
CI-поток:
- Старт job →
cache-sync.sh pull xcodebuild -scheme "$SCHEME" -derivedDataPath "$LOCAL_DD" build- Только при успешной сборке →
cache-sync.sh push
При параллельных машинах push может конфликтовать — используйте flock или примите last-write-wins; DerivedData при том же lockfile + Xcode должны быть совместимы. При очень частом push переходите на асинхронную загрузку (rsync в фоне после сборки, без блокировки pipeline).
6. Межрегиональные cache hub на Cloud Mac
Когда Runner’ы в US East, US West и Asia-Pacific, трансокеанический rsync полного DerivedData часто не окупается (задержка + egress). Рекомендуемый подход:
- Один cache bucket на регион (S3 / R2 / object storage провайдера)
- Содержимое sync: tarball
Pods/по хешуPodfile.lock+ SPM-кэш поPackage.resolved - DerivedData только внутри региона через rsync; первый job холодный, следующие warm
- Согласуйте версии образов с тегами golden image, чтобы дрейф Xcode не инвалидировал всю библиотеку
Сжимайте SPM-кэш через tar czf перед загрузкой — часто на 40 %–60 % меньше; DerivedData уже состоит из множества мелких файлов, поэтому tar + параллельная загрузка стабильнее сырого межрегионального rsync.
7. Подключение к GitHub Actions / self-hosted Runner
На self-hosted runner’ах оберните workflow так:
- name: Restore shared Xcode cache
run: ./scripts/cache-sync.sh pull
env:
CACHE_ROOT: ${{ secrets.IOS_CACHE_SSH }}
SCHEME: MyApp
DERIVED_DATA_PATH: /Volumes/CI/DerivedData/MyApp
- name: Build
run: xcodebuild -scheme MyApp -derivedDataPath /Volumes/CI/DerivedData/MyApp build
- name: Save shared Xcode cache
if: success()
run: ./scripts/cache-sync.sh push
Если вы всё ещё используете GitHub-hosted actions/cache, self-hosted флоты могут идти двумя дорожками: GHA cache как fallback, rsync hub как низколатентный same-region слой. Держите пространства имён ключей раздельно, чтобы не перезаписывать друг друга.
Мониторинг: длительность pull/push, переданные байты и была ли сборка инкрементальной (считайте строки CompileSwift в логах xcodebuild). Когда P95 замедляется, сначала проверьте cache miss, затем нагрузку на машины — тот же playbook, что при разборе нестабильного времени сборки.
8. Чеклист ловушек
- Параллельная запись в DerivedData: случайные
stat cache file corrupted→ переход на pull/push - Разные minor-версии Xcode: несовпадение ABI модулей → ключ включает версию, образы делят единый tag
- Несовпадение путей: кэш по пути A, сборка по пути B → полный rebuild
- Push после clean: пустые каталоги перезаписывают хороший кэш → push только при успехе и не после clean
- Подписанные продукты в кэше: устаревшая подпись после смены provisioning → исключите
Build/Productsиз кэша или разделите ключи по configuration - Диск заполнен: раздувание DerivedData → периодически LRU-очищайте удалённые каталоги
${KEY}, храните последние N хешей lockfile
9. FAQ
Могут ли несколько Mac напрямую делить один каталог DerivedData?
Не при одновременной записи. Read-only mount или single-writer/multi-reader возможны, но на этапе linking бывают блокировки. В проде: локальный DerivedData + rsync.
Нужна ли одна версия Xcode для общего кэша?
Да. После обновления Xcode — новый cache key; старые каталоги удаляйте асинхронно.
Сравнение с удалённым кэшем Bazel / Tuist?
Bazel/Tuist мельче (уровень action) для огромных monorepo. Обычным Xcode-проектам проще мигрировать через sync DerivedData, совместимо с xcodebuild.
Сколько диска нужно нескольким Cloud Mac?
Локально 50–100 ГБ на машину для DerivedData + SPM; хаб — по числу параллельных scheme × размер кэша × число версий. M4 + 2 ТБ обычно хватает средним командам.
Заключение
Общий кэш Xcode решает «второй холодный старт» эры multi-Runner: после оптимизации кэша на одной машине горизонтальное масштабирование не должно снова платить полный compile tax. Три разделённых слоя (Pods / SPM / DerivedData), одна дисциплина sync (pull-build-push) и одно железное правило (никогда не писать в DerivedData параллельно).
Кэш — самый дешёвый множитель вычислений в распределённых сборках; ROI быстрее, чем купить ещё два M4.
Начните с одного cache leader и cache-sync.sh; именуйте удалённые каталоги по хешу lockfile; пушьте только при зелёной сборке. Кривая P95 на следующей неделе покажет, стоило ли оно того.
Cloud Mac с запасом диска для multi-node CI
Vuncloud Mac mini M4: US East, US West и Asia-Pacific с SSH — идеально для больших томов DerivedData, rsync cache hub и флотов self-hosted Runner.
Читать дальше
- CocoaPods / SPM / DerivedData: гайд по кэшу на одной машине
- Golden image и автоматизация Cloud Mac
- Единая среда сборки для трансграничных iOS-команд
- Почему iOS CI/CD работает на Mac mini M4
Поведение toolchain Apple следует официальным релизам; аудируйте SSH и учётные данные object storage по политике безопасности вашей команды. Последнее обновление: 27 июля 2026 г.