Skip to content
All writing Part 08 of 12 · 在容器裡遙控 Chrome 的完整路由
Engineering · 3 min read

Headless Service:用電話簿取代總機

Headless Service 直接回傳 Pod IP,不經過 proxy 路由。這一個改變就能修復 CDP GUID race,讓兩步連線固定打到同一個 Pod。

Regular vs Headless

Regular Service(總機) DNS → 10.96.0.50 proxy 每通隨機轉接 Pod A Pod B 兩通可能分到不同桌 Headless Service(電話簿) DNS → 10.0.1.5, 10.0.2.3 你自己選一個 IP Pod A Pod B 兩通一定打到同一張桌

Regular Service = 總機。你打一個號碼,總機隨機轉接。兩通電話可以接到不同人。

Headless Service = 電話簿。你拿到所有分機號碼,自己選一個直撥。

為什麼這能修復 GUID Race

API Pod 查 DNS,拿到所有 Chrome Pod 的 IP(不是一個 proxy IP)。選一個,GET 和 WebSocket 都打給它。兩步到同一支 Chrome,GUID 一定對。

var chromePodRoundRobin atomic.Uint64

addrs, _ := net.LookupHost(hostname)
idx := chromePodRoundRobin.Add(1) % uint64(len(addrs))
podAddr := net.JoinHostPort(addrs[idx], port)
// GET 和 WebSocket 都用 podAddr

DNS 只回傳 Ready 的 Pod IP。正在啟動或 probe 失敗的 Pod 不會出現。retry 時重新查 DNS,可以切換到另一個 Pod。「逃離壞掉的 Pod」的能力保留了。

一個 Request 的完整流程

每個 request 跑在自己的 goroutine 裡。五個步驟都用同一個局部變數 podAddr

goroutine(每個 request 一個) 1. net.LookupHost("chrome-svc") → [10.0.1.5, 10.0.2.3, 10.0.3.7] 2. atomic round-robin → podAddr = "10.0.1.5:9222"(局部變數) 3. GET http://10.0.1.5:9222/json/version(每次新 TCP) → Chrome 回傳 { webSocketDebuggerUrl: "ws://127.0.0.1:9223/.../GUID-X" } 4. 把 127.0.0.1 換成 10.0.1.5 → 跟第 3 步同一個 Pod 5. WebSocket 連到 10.0.1.5 → 導覽、截圖、關閉 共享 vs 局部 共享:只有 atomic 計數器 局部:podAddr、GUID、session 不用 mutex,沒有 data race。

Round-robin 計數器(atomic.Uint64)是所有 goroutine 之間唯一的共享狀態。其他東西都是局部的:podAddr、GUID、chromedp session。不用 mutex,沒有 data race。延伸閱讀:為什麼每次都要重新連線

clusterIP 是 immutable 的。你不能把現有的 Service 從 Regular 改成 Headless。Kubernetes 會拒絕更新。必須刪掉重建。ArgoCD 需要 Replace=trueForce=true sync options。


參考來源:

Related:為什麼每次都要重新連線、這個方案修復的 GUID raceDNS 容量模型,或回到系列總覽

Tags #kubernetes #chrome-devtools #containers
// connect

Be brave | Be wise | Be grateful

21 BreakinCode

// elsewhere
LinkedInMedium (lang: en)Youtube
wh:~$William Hung· © 2026 Taipei · GMT+8 · Available for collaboration