Vuncloud Блог
← Назад к Dev Notes

Общий кэш Xcode на практике: синхронизация build-данных между несколькими узлами

DerivedData · SPM · CocoaPods · синхронизация runner'ов~13 мин чтения

Аналитическая панель — символизирует мониторинг и эффективность синхронизации кэша Xcode-сборок на нескольких узлах
TL;DR · Три тезиса
  • Кэш 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 переиспользовал модули, которые уже скомпилировал коллега.

3
слоя кэша: зависимости · артефакты · индексы
5–15 мин
типичная дополнительная экономия warm-сборок multi-node
1
железное правило: не писать в один DerivedData параллельно

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.

Редактор кода на MacBook — символ нескольких Runner, делящих один compile-кэш Xcode
Вы делите скомпилированные модули — не открытый проект на каждой машине. Каждый Runner всё равно собирает из локальной копии DerivedData.

3. Четыре архитектуры sync multi-node

ПаттернПодходПлюсыРиски
Только локальноУ каждого Runner свой диск, без sharingНулевые opsГоризонтальное масштабирование не экономит время
Общий NFS-mountDerivedData на сетевом томе, 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_VERSION
  • hashFiles('**/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-поток:

  1. Старт job → cache-sync.sh pull
  2. xcodebuild -scheme "$SCHEME" -derivedDataPath "$LOCAL_DD" build
  3. Только при успешной сборке → 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.

Тарифы Cloud Mac · Модель стоимости M4 CI Runner

Поведение toolchain Apple следует официальным релизам; аудируйте SSH и учётные данные object storage по политике безопасности вашей команды. Последнее обновление: 27 июля 2026 г.

Dev Notes · Ускорение сборки

Синхронизация DerivedData · мульти-runner · Cloud Mac

rsync-хаб · cache key · региональные cache bucket'ы

Тарифы Cloud Mac
Ограниченное предложение Посмотреть тарифы