雲端 Mac CI 的 SSH 連線複用與失效治理

雲端 Mac CI 的 SSH 連線複用與失效治理

當 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 配置與四個資料中心中選擇,資源不與其他租戶共享;實際可用狀態以控制台即時回傳為準。

選擇方案