Когда управляющий узел CI последовательно проверяет версии на облачном Mac, загружает код, запускает сборку и получает артефакты, создание нового SSH-соединения для каждой команды приводит к повторному согласованию открытых ключей, аутентификации и инициализации сеанса. Задержка при отдельном подключении может быть почти незаметной, но при выполнении десятков коротких команд время рукопожатий начинает занимать существенную долю общего времени. Встроенный в SSH механизм ControlMaster позволяет последующим командам использовать уже аутентифицированное TCP-соединение, однако вместе с ним необходимо правильно организовать изоляцию сокетов, обнаружение недействительных соединений и очистку при завершении.
Сначала определите объект оптимизации
Повторное использование соединения подходит для сценария, в котором «одно задание CI в течение нескольких минут многократно обращается к одному и тому же Mac». Если задание запускает только одну длительную сборку, сокращение времени рукопожатия даст ограниченный эффект. Если же управляющий узел последовательно выполняет более десятка коротких команд, повторное использование соединения обычно оказывается заметно полезнее.
Сначала зафиксируйте процесс подключения без повторного использования:
time ssh -o BatchMode=yes build-mac 'sw_vers -productVersion'
ssh -vv -o BatchMode=yes build-mac 'true' 2>ssh-debug.log
Проверьте, повторяются ли в отладочном журнале этапы обмена ключами и аутентификации. Не сравнивайте случайное время выполнения одной команды: выполните как минимум несколько последовательных запусков и отдельно оцените время установления соединения и выполнения удалённой команды.
ControlMaster сокращает затраты на установление соединения. Он не ускоряет саму компиляцию в Xcode, разрешение зависимостей или передачу файлов.
Создайте минимальную конфигурацию повторного использования
Создайте отдельный псевдоним для сборочного узла в файле ~/.ssh/config на управляющем узле CI. %C хеширует параметры соединения и предотвращает ошибки при создании Unix Socket из-за слишком длинных имён пользователя и узла.
Host build-mac
HostName mac.example.internal
User ci-runner
BatchMode yes
ControlMaster auto
ControlPersist 10m
ControlPath ~/.ssh/control/%C
ConnectTimeout 15
ServerAliveInterval 30
ServerAliveCountMax 3
StrictHostKeyChecking yes
UserKnownHostsFile ~/.ssh/known_hosts_ci
При подготовке каталога обязательно ограничьте права доступа:
install -d -m 700 "$HOME/.ssh/control"
chmod 600 "$HOME/.ssh/known_hosts_ci"
ssh build-mac 'printf "%s\n" ready'
ssh -O check build-mac
Первая команда создаёт основное соединение, а ssh -O check должна вернуть состояние управляющего процесса. Параметр ControlPersist 10m означает, что после завершения последнего сеанса основное соединение может сохраняться не более десяти минут. Он не означает, что задание может полагаться на это соединение неограниченно долго.
Закрепите идентификатор узла
Не устанавливайте StrictHostKeyChecking=no ради удобства. Получите открытый ключ узла по контролируемому каналу, добавьте его в отдельный файл known_hosts для CI и предусмотрите явную процедуру обновления при пересоздании узла или ротации ключей. Повторное использование соединения сокращает только число последующих рукопожатий и не меняет границу доверия при первом подключении.
Изолируйте сокеты параллельных заданий
Одна из самых распространённых ошибок на общем Runner — использование ~/.ssh/control/%C всеми заданиями. Если два конвейера одновременно подключаются к одному узлу, один из них может повторно использовать основное соединение, созданное другим. Если после этого одно задание выполнит команду завершения, соединение второго задания также будет прервано.
Более надёжный вариант — создать отдельный каталог для каждого задания и переопределить ControlPath в командной строке:
set -euo pipefail
job_id="${CI_JOB_ID:-local-$$}"
control_dir="${TMPDIR:-/tmp}/ssh-control-${job_id}"
install -d -m 700 "$control_dir"
ssh_opts=(
-o ControlMaster=auto
-o ControlPersist=10m
-o "ControlPath=${control_dir}/%C"
-o BatchMode=yes
)
ssh "${ssh_opts[@]}" build-mac 'xcodebuild -version'
ssh "${ssh_opts[@]}" build-mac 'git --version'
Идентификатор задания должен поступать из доверенного контекста конвейера и быть ограничен короткой строкой. Не подставляйте имя ветки непосредственно в путь: косые черты, пробелы и слишком длинные имена могут помешать созданию сокета.
Обнаруживайте недействительные соединения и безопасно создавайте их заново
После переключения сети, перезапуска удалённого узла или аварийного завершения управляющего процесса файл сокета может остаться на месте, хотя соответствующее соединение уже недоступно. Проверяйте соединение перед запуском рабочей команды. Если проверка завершилась неудачно, удалите каталог сокетов только текущего задания и установите соединение заново.
if ! ssh "${ssh_opts[@]}" -O check build-mac >/dev/null 2>&1; then
rm -rf "$control_dir"
install -d -m 700 "$control_dir"
ssh "${ssh_opts[@]}" build-mac 'true'
fi
Здесь разрешено удалять только каталог, созданный текущим заданием. Нельзя очищать общий каталог с помощью rm -rf ~/.ssh/control/*. Также избегайте безусловного повторного запуска команд сборки: к моменту разрыва соединения удалённая команда могла уже запуститься. Безопасный подход — автоматически повторять только идемпотентные проверки, а затем определять состояние исходного задания по процессу, файлу блокировки или номеру сборки.
Учитывайте ограничение числа сеансов на сервере
Одно основное соединение может обслуживать несколько логических сеансов, но их количество не безгранично. При слишком большом числе одновременно выполняемых команд сервер может отклонить новый channel. Сначала ограничьте число параллельных SSH-команд в одном задании, а затем оцените необходимость изменения серверной политики. Не рассматривайте увеличение лимита сеансов как стандартное решение проблемы.
Гарантированно очищайте ресурсы при завершении
При штатном завершении закрывайте основное соединение управляющей командой. При аварийном завершении каталог задания всё равно необходимо удалить. В Shell-скрипте эту логику можно централизовать с помощью trap:
cleanup() {
ssh "${ssh_opts[@]}" -O exit build-mac >/dev/null 2>&1 || true
rm -rf "$control_dir"
}
trap cleanup EXIT INT TERM
Итоговая проверка должна охватывать четыре пункта: последовательные команды действительно используют одно основное соединение; два параллельных задания работают с разными каталогами; после перезапуска удалённого узла соединение успешно создаётся заново; после отмены задания не остаются сокеты. Для автоматизированных заданий на SetMini следует применять те же принципы: после проверки доступных конфигураций в панели управления рассматривайте уровень соединений как инфраструктуру, состояние которой можно проверить, восстановить и очистить, а не как скрытое состояние, которое никогда не становится недействительным.
Часто задаваемые вопросы
Стоит ли задавать максимально долгий ControlPersist?
Нет. Для CI обычно достаточно 5–15 минут, чтобы покрыть последовательные команды одного задания. Более долгий срок повышает риск устаревших сокетов и ошибочного использования между заданиями.
Можно ли параллельным заданиям использовать общий ControlPath?
Не рекомендуется. Создавайте отдельный каталог сокетов с правами 700 для каждого задания, затем выполняйте ssh -O exit и удаляйте каталог при завершении.
Отключает ли ControlMaster проверку ключа удалённого узла?
Нет. Первое управляющее соединение должно использовать StrictHostKeyChecking=yes и контролируемый файл known_hosts с ожидаемым ключом удалённого Mac.
Перенесите этапы разработки на облачный Mac с доступом в любое время
Выберите одну из двух конфигураций Apple Silicon и четырёх дата-центров. Ресурсы не распределяются между арендаторами, а фактическая доступность отображается в консоли в реальном времени.