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 编译、依赖解析或文件传输本身变快。
建立最小复用配置
在 CI 控制端的 ~/.ssh/config 中为构建主机建立专用别名。%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。应通过受控渠道取得主机公钥,写入 CI 专用的 known_hosts,并在节点重建或密钥轮换时走明确的更新流程。连接复用只减少后续握手,不改变首次连接的信任边界。
为并行任务隔离套接字
共享 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 后删除目录。
连接复用会绕过 SSH 主机密钥校验吗?
不会。首次建立主连接时仍应启用 StrictHostKeyChecking=yes,并使用受控的 known_hosts 文件固定目标主机身份。
把工程步骤放进可随时接入的云端 Mac
从两档 Apple Silicon 配置和四个机房中选择,资源不与其他租户共享,实际可用状态以控制台实时返回为准。