В начале июня мы посадили Xcode CI и OpenClaw Gateway на один канадский M4 и протянули две недели — с launchd Nice=-5 и ограниченной параллельностью сборки. На третьей неделе в IM пошли жалобы: «Gateway снова тормозит». Логи сборки были зелёными, а health check curl в пике уезжал с 50 мс выше 400 мс. Мы снижали параллельность, чистили DerivedData, временно подняли тариф до 24 ГБ — проблема стала «раз в день» вместо «дважды», но не исчезла.
Это не ошибка конфигурации, а потолок модели same-host. Порты не конфликтуют (20300 vs 18789), ядер CPU хватает — борьба идёт за unified memory и swap. Статья продолжает совместное размещение и tuning launchd: когда нужен dual-node, как нарисовать топологию и переключить трафик без поломки Channels. Память и сборки: документация Apple по кэшу сборки Xcode; сеть: руководство по установке Tailscale.
Коротко: водораздел — в изоляции ресурсов, а не в «купить ещё одну машину».
-
Четыре жёстких сигнала — достаточно одного
≥50 сборок/день, ≥3 параллельных пайплайна, Gateway для конечных пользователей или ≥3 Critical swap за неделю — дальнейший Nice лишь откладывает.
≥50/день
-
Минимальный dual-node: build + выделенный Gateway
Узел сборки 24 ГБ+ и большой SSD; узел Gateway 16 ГБ на ~500 МБ постоянной нагрузки. Два M4 через Tailscale; в интернет — только нужные входы.
16 ГБ Gateway
-
Миграция = состояние + DNS, не переустановка
rsync конфига, те же токены, переключение MagicDNS Tailscale; сборка не останавливается, blue/green Gateway ≤15 минут.
Blue/green ≤15 мин
1. Почему same-host внезапно «не тянет»
Многие команды стартуют с одного облачного Mac на три роли: xcodebuild, OpenClaw Gateway на 18789, иногда VNC для связки ключей. При низкой нагрузке 16 ГБ кажутся с запасом. С ростом веток, параллельных UI-тестов и Channels из внутреннего пилота в прод-IM кривая памяти из «пилы» становится «плато» — через десять минут после сборки сжатые страницы всё ещё >8 ГБ, heap Gateway уходит в swap.
Тюнинг same-host (меньше параллельности, выше Nice, очистка кэша) снижает вероятность наложения пиков, но не убирает сами пики. Когда дневные сборки APAC и вечерний IM-пик в Северной Америке почти наверняка пересекаются, вы уже не оптимизируете параметры — вы играете в рулетку с планировщиком. У Apple Silicon нет отдельного VRAM; Xcode и Node.js Gateway делят один физический пул. После memory_pressure Warn пользователи чувствуют джиттер; CI видит редкие таймауты и сбои подписи.
Разделение — не миф «масштабирования», а разведение несжимаемых пиков: сборки только на узле A, RAM Gateway никогда не вытесняется xcsbuildd. Та же логика, что в самостоятельном macOS runner GitHub Actions на облачном Mac — выделенный runner без смешения с рабочим столом; здесь разделяют Gateway и CI.
Для команд с формальными SLA триггер часто не первый swap, а третий тикет «Gateway тормозит» за неделю при зелёных дашбордах CI. Тогда same-host перестаёт экономить — он прячет воспринимаемый простой.
Полезный эксперимент: измеряйте P95 Gateway только во время активных job xcodebuild. Если задержка растёт в 3–8 раз при CPU <70 %, это не сеть — это конкуренция за RAM. У нас compressed memory держалась >6 ГБ ещё десять минут после сборки; heap Node.js Gateway не мог стабильно оставаться resident. Каждое снижение -jobs или повышение Nice урезало пропускную способность CI, не спасая SLA Gateway. Классическая ловушка same-host: две команды оптимизируют друг против друга на одной машине.
Кто уже крутит self-hosted runner на облачном Mac, знает правило «без рабочего стола и без Gateway на runner». Dual-node split — то же правило, только Gateway здесь не побочный сервис, а прод-критичный. Отличие от «просто взять 32 ГБ»: больше RAM поднимает потолок для обоих пиков сразу, но не разводит их. Если пик сборки по-прежнему ест 18–22 ГБ, Gateway остаётся с остатком — а при параллельных UI-тестах остатка мало.
2. Три топологии: same-host, dual-node, triple-node
Сначала классификация, потом закупка:
- T0 same-host: один M4 для Xcode Server / self-hosted runner + Gateway. <30 сборок/день, внутренний Gateway, терпимые всплески задержки.
- T1 dual-node (фокус): узел A только CI (24 ГБ+, большой SSD); узел B только Gateway (16 ГБ). Tailscale или LAN ЦОД; Channels/Dashboard → B, триггеры сборки → A.
- T2 triple-node: отдельный хост подписи/upload или параллельных тестов. >150 сборок/день или жёсткое разделение TestFlight/compile; большинству команд хватает T1.
Асимметричный вывод: дело не в слабости M4, а в двух классах SLA в одном пуле RAM. Gateway нужен 99,9 % стабильного отклика; CI нужна пропускная способность. Same-host компромиссит оба одним SLA.
3. Same-host vs dual-node vs апгрейд: сравнение
| Измерение | Продолжить тюнинг Nice / меньше параллели | Один хост 32 ГБ Больше RAM, та же топология | Split dual-node CI + Gateway раздельно |
|---|---|---|---|
| Корневая причина | Смягчает наложение пиков | Поднимает потолок RAM | Изолирует два SLA |
| Задержка Gateway | Джиттер со сборками | Лучше, не ноль | <100 мс возможно при сборке |
| Месячная стоимость (грубо) | Минимум | Средне (апгрейд) | Средне (+ малый 16 ГБ) |
| Сложность ops | Низкая | Низкая | Средняя (+1 хост, Tailscale) |
| Подходящая фаза | <30 сборок/день | 2 параллельных job, внутр. GW | ≥50/день или внешний GW |
| Сигнал | Порог | Наблюдение | Ложное срабатывание |
|---|---|---|---|
| Частота сборок | ≥50/день | Логи CI / счётчик runner | Исключить ручной локальный archive |
| Параллельность | ≥3 полных сборки | xcodebuild -jobs, глубина очереди | Лёгкий lint не считается |
| Аудитория Gateway | Конечные пользователи / Channels 7×24 | SLA продукта | Только внутренний webhook может подождать |
| Давление памяти | ≥3 Critical/неделю | Логи memory_pressure | Сначала устранить разовую утечку |
| Диск | >50 ГБ/мес + высокий I/O wait | df -h + iostat | Сначала очистить archives |
4. Сценарии: матрица решений
- Дуэт, <20 сборок/день, Gateway только для себя: остаться на T0, хватит tuning launchd.
- Сборки APAC, Gateway для IM-пользователей: сразу T1. Follow-the-sun почти гарантирует наложение пиков.
- Облачный runner GHA, Gateway только запущен: runner на A, Gateway на B; не вешать 18789 на машину runner.
- Загрузка TestFlight vs компиляция за диск: сначала T1; при необходимости T2 хост подписи — не путать с этим runbook.
- Бюджет на одну машину: приоритет SLA Gateway, сборки ночью или меньше параллели — компромисс, не долгосрочная архитектура.
Если post-mortem показывает рост P95 Gateway только во время xcodebuild, это не сеть — это конкуренция за RAM. T1 решает это без остановки пайплайна.
Для распределённых команд с follow-the-sun: даже если ночные archive идут в Северной Америке, Gateway днём обслуживает agent-сессии и IM-Channels. Вероятность совпадения release-сборки с прод-инцидентом растёт с каждой feature-веткой. T0 работает, пока Gateway «best effort»; как только Product привязывает SLA отклика в Confluence, вы фактически обязаны в T1 — просто без второй машины.
Возражение «бюджет на одну машину»: приоритет SLA Gateway, сборки в ночное окно. Это переход, не целевая архитектура. За 4–8 недель обычно снова те же тикеты из-за роста частоты коммитов. Планируйте второй узел 16 ГБ под Gateway с самого начала; ROI — в меньшем support-времени, а не только в абсолютной аренде.
5. Рекомендуемые стеки (комбинируются)
- Минимальный dual-node: Hashvps Canada M4 24 ГБ (A, CI) + M4 16 ГБ (B, Gateway) + tailnet Tailscale + еженедельно
openclaw doctor. Стандартный апгрейд с T0. - Гибрид CI: A с self-hosted runner GitHub Actions; B Gateway + Channels. Оркестрация в GitHub, исполнение на macOS, Gateway вне пиков сборки. Согласуется с «суверенитетом среды» в рынке macOS-сборок 2027.
- Наблюдаемость: зонд задержки Gateway на B (
curlкаждые 5 с) + снимокmemory_pressureна A после сборки; двух недель данных хватает для ROI split перед руководством.
6. Типичные ошибки перед миграцией
- Сначала остановить CI, потом переносить Gateway: дорого; blue/green Gateway, CI работает.
- Переустановка вместо rsync: потеря
~/.openclaw, токенов, pairing Channels → все перелогиниваются. - Два публичных IP на 18789: во время cutover Tailscale или внутренний DNS — никогда два Gateway на Channels.
- Слишком рано удалить Gateway с build-хоста: окно отката: старый процесс жив, меняется только DNS.
- Часы и сертификаты: дрейф NTP >30 с → sporadic сбои токенов; в день миграции
sudo sntp -sS time.apple.com.
7. Runbook: dual-node за семь шагов
Допущение: раньше mac-ci-01 с CI+Gateway; новый mac-gw-02 (16 ГБ) только Gateway. Tailscale установлен (см. ops runbook OpenClaw).
Шаг 1: baseline (накануне)
Зафиксировать P50/P95 Gateway, число сборок, распределение memory_pressure. Экспорт openclaw status и списка Channels.
# 延迟采样 60 次
for i in $(seq 1 60); do
curl -o /dev/null -s -w "%{time_total}\n" http://127.0.0.1:18789/health
sleep 5
done | sort -n | awk '{a[NR]=$1} END{print "p50="a[int(NR*0.5)],"p95="a[int(NR*0.95)]}'
memory_pressure
vm_stat | head -8
Шаг 2: новый хост + Tailscale
На mac-gw-02: обновление macOS, Homebrew, Node, Tailscale; ping к mac-ci-01 <5 мс. На B не запускать xcodebuild.
Шаг 3: rsync состояния Gateway (окно обслуживания)
# 在原机 mac-ci-01 执行;先停 Gateway 避免写入分裂 sudo launchctl unload /Library/LaunchDaemons/com.openclaw.gateway.plist rsync -avz --delete \ ~/.openclaw/ \ builder@mac-gw-02.tailnet-abc.ts.net:~/.openclaw/ # 同步 launchd plist scp /Library/LaunchDaemons/com.openclaw.gateway.plist \ builder@mac-gw-02.tailnet-abc.ts.net:/tmp/
Шаг 4: запуск Gateway на новом хосте
sudo cp /tmp/com.openclaw.gateway.plist /Library/LaunchDaemons/ sudo launchctl load /Library/LaunchDaemons/com.openclaw.gateway.plist openclaw doctor curl -s http://127.0.0.1:18789/health sudo lsof -iTCP:18789 -sTCP:LISTEN
Шаг 5: трафик — MagicDNS или reverse proxy
Командный hostname Gateway (напр. gateway.tailnet-abc.ts.net) на новый хост; mobile и Channels на новое имя MagicDNS. Не перезапускать Gateway на старом хосте во время cutover.
Шаг 6: разгрузить узел сборки
После проверки Channels на B: снять Gateway launchd с A, вернуть параллельность CI 5–6 (24 ГБ).
# mac-ci-01:确认已无流量打到 18789 后
sudo launchctl unload /Library/LaunchDaemons/com.openclaw.gateway.plist
sudo mv /Library/LaunchDaemons/com.openclaw.gateway.plist \
/Library/LaunchDaemons/com.openclaw.gateway.plist.bak
defaults write com.apple.dt.Xcode \
IDEBuildOperationMaxNumberOfConcurrentCompileTasks 6
Шаг 7: наблюдать 48 ч + откат
Хранить backup .openclaw и plist.bak семь дней. При P95 >200 мс или падении Channels: DNS назад, старый plist — очередь CI не трогать. Детали: onboarding OpenClaw на удалённом Mac.
На этапе наблюдения сборки специально не останавливают: нужно убедиться, что build-узел держит повышенную параллельность, а P95 Gateway на B остаётся плоским. Сравните baseline из шага 1 с теми же часовыми окнами после миграции. Типичный успех: Critical memory_pressure на A может ещё случаться, но только во время сборок — больше не коррелирует с задержкой Gateway на том же хосте, потому что общего хоста больше нет.
Если команда сомневается в стоимости второй машины, посчитайте не только аренду, но и время инженеров на расследование «Gateway тормозит при зелёных CI-логах». Один такой инцидент в неделю быстро съедает экономию от отказа от dual-node.
8. FAQ
В1. Только 16 ГБ — логические роли без второго хоста?
Не заменяет физический split. Сдвиг по времени (сборки ночью) помогает ненадолго; follow-the-sun или Channels 7×24 снова создадут пики. Логическое разделение полезно, чтобы доказать закупку второй машины по задержке после переноса Gateway.
В2. Обязателен ли Tailscale?
Нет, но настоятельно рекомендуется. LAN провайдера, WireGuard или SSH-туннель тоже подойдут; Tailscale даёт MagicDNS, ACL, мало ops. Два M4 Hashvps Canada: RTT часто <2 мс — хватает для build webhook с B на A.
В3. Dual-node vs один хост 32 ГБ?
Часто близко: 24 ГБ build + 16 ГБ малый хост vs 32 ГБ solo. Важен доказуемый SLA Gateway; внешние команды считают минуты простоя, а не только аренду.
В4. Хостинговый runner GHA — всё равно свой Mac?
Суверенитет среды. Хостинговый macOS по минутам для пиков; self-hosted облачный Mac при стабильных >50 сборок/день с контролем keychain/DerivedData. Gateway независим от модели runner.
В5. Самый быстрый откат?
DNS назад + launchctl load на старом хосте. В день отката без массовых сборок; сначала health + одно сообщение Channel, потом CI. Двойная копия состояния до стабильных метрик.
В6. Какие метрики убеждают руководство?
Хватит двух недель. На старой схеме фиксируйте P95 Gateway и события memory_pressure Critical; после миграции — те же метрики на раздельных хостах. Если P95 в пике сборки падает с 400 мс до <100 мс, а Critical на Gateway-хосте обнуляется, бизнес-кейс яснее любого CPU-апгрейда. Зафиксируйте и «сэкономленное время сборки»: больше параллельных compile tasks на 24 ГБ без риска для Gateway.
9. Итог
Разделение Mac M4 CI — не провал, а следующий этап после успешного same-host tuning: сборка и Gateway оба нужны, но уже не в одной планке RAM. Четыре сигнала, T1 для изоляции SLA, семь шагов blue/green — водораздел в изоляции, а не в числе машин.
Если вы застряли между «Gateway тормозит» и «сборку нельзя останавливать», выделенный Gateway-хост 16 ГБ ближе к решению, чем ещё один Nice. Сборки на облаке 24 ГБ, агенты и пользователи на стабильном 18789 — доступная quasi-prod топология для малых команд в 2026. Это не роскошь, а способ перестать тушить одни и те же инциденты каждую неделю.
Dual-node: два облачных Mac mini
После разделения узлу сборки нужны 24 ГБ и большой SSD под DerivedData; Gateway хватает 16 ГБ для 7×24 с низкой задержкой. Hashvps Canada M4 bare metal, выделенный IPv4, SSH/VNC из коробки — две машины в одном регионе, низкая задержка Tailscale, удобный старт T1, часто лучше жёсткого апгрейда до 32 ГБ на одном хосте.
Планируете переход same-host → dual-node? Облачный Mac mini M4 Hashvps — сильная вторая машина — Смотреть тарифы и развести CI с Gateway чисто.