chundev
日期:2026-05-13 情境:on-prem k3s 4-node cluster,用具體劇情拆解 MetalLB 各個機制
前一篇講了「為什麼用 MetalLB」和「跟 hostPort 比較」。這一篇用 8 個具體劇情把每個概念踩過一遍 — IP 怎麼憑空出現、node 斷電怎麼自動切、controller 死掉會發生什麼、IP 衝突怎麼診斷。看完應該對「MetalLB 真實運作」有畫面感。
假設子網段 192.168.50.0/24,現況:
已用 IP:
192.168.50.1 router
192.168.50.51 k8s-node1
192.168.50.52 k8s-node2
192.168.50.53 k8s-node3
192.168.50.57 k8s-node4
.54 沒人在用。
從任何一台機器試著 ping .54:
$ arping -c 3 -I eth0 192.168.50.54
ARPING 192.168.50.54 from 192.168.50.10 eth0
Sent 3 probes (3 broadcast(s))
Received 0 response(s) ← 沒人回
.54 在整個子網路上不存在。封包送不出去。
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: example-pool
namespace: metallb-system
spec:
addresses:
- 192.168.50.54-192.168.50.59
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2-example
namespace: metallb-system
spec:
ipAddressPools:
- example-pool
建一個 Service 跟它要 IP:
apiVersion: v1
kind: Service
metadata:
name: demo-nginx
spec:
type: LoadBalancer # 關鍵
selector:
app: demo-nginx
ports:
- port: 443
targetPort: 443
幾秒後:
$ kubectl get svc demo-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
demo-nginx LoadBalancer 10.43.X.X 192.168.50.54 443:31xxx/TCP
External-IP 從 <pending> 變成 192.168.50.54。
$ arping -c 3 -I eth0 192.168.50.54
ARPING 192.168.50.54 from 192.168.50.10 eth0
Unicast reply from 192.168.50.54 [aa:bb:cc:dd:ee:ff] 0.823ms
Unicast reply from 192.168.50.54 [aa:bb:cc:dd:ee:ff] 0.654ms
Unicast reply from 192.168.50.54 [aa:bb:cc:dd:ee:ff] 0.711ms
✅ .54 突然「出現了」。MAC 是 aa:bb:cc:dd:ee:ff — 用 ip link 比對會發現這是 k8s-node2 的 MAC。
所以 .54 在哪? 在 ARP 層的「宣稱」裡。沒人在任何 NIC 加這個 IP,但網路上的機器都被 MetalLB speaker 告知「.54 在 node2」。
用 tcpdump 看 .54 收到流量時,三個 node 各看到什麼。
demo-nginx → 192.168.50.54nginx-7d...-abc 跑在 k8s-node1curl https://192.168.50.54$ sudo tcpdump -i eth0 -nn 'arp or host 192.168.50.54'
# ARP 階段
17:42:01.001 ARP, Request who-has 192.168.50.54 tell 192.168.50.10
17:42:01.002 ARP, Reply 192.168.50.54 is-at aa:bb:cc:dd:ee:ff (= node2)
← speaker 回應
# TCP/TLS 流量階段
17:42:01.123 IP 192.168.50.10.54321 > 192.168.50.54.443: Flags [S] ← 進來
17:42:01.124 IP 192.168.50.54.443 > 192.168.50.10.54321: Flags [S.] ← 回去
← 注意 src 是 .54 不是 .52,因為 kube-proxy 做 NAT
17:42:01.125 IP 192.168.50.10.54321 > 192.168.50.54.443: Flags [.]
... TLS handshake + HTTP request/response ...
# ARP 看不到 .54(不是 leader,不回應)
# 但封包經過 overlay network 進來:
$ sudo tcpdump -i flannel.1 -nn
17:42:01.125 IP 10.42.2.5.54321 > 10.42.1.30.443: Flags [S]
↑ kube-proxy 已經 NAT 過 src 跟 dst
src=node2 的 pod IP(或 node2 IP)
dst=nginx pod 的 cluster IP
$ sudo tcpdump -i eth0 -nn arp
17:42:01.001 ARP, Request who-has 192.168.50.54 tell 192.168.50.10
← 收到 broadcast
←(不回應,因為 .54 的 leader 是 node2,不是我)
結論:
假設 demo 服務跑在 cluster 上,node3 是入口。突然有人踢到電源線。
T+0s node3 power off
T+0s 正在使用 demo 的 client:connection reset
T+0s 新 client 來訪:DNS 解析到 .53 → TCP SYN → 沒人接 → timeout
T+30s 監控告警觸發(PagerDuty 響)
T+5min 工程師看到告警、上 SSH 試圖診斷
T+8min 發現 node3 整台死,物理上沒辦法
T+10min 決定臨時轉到 node1
T+10min 在 node1 啟動新 nginx pod
T+10min 到 DNS provider 改 demo.example.com → 192.168.50.51
T+15min DNS TTL 過期、cache 慢慢更新
T+25min 大部分 client 開始 work(cache 慢的還沒)
T+數小時 node3 修好、再考慮要不要改回去
總服務中斷:~25 分鐘。需要人工介入 4-5 次。
T+0s node3 power off
T+0s 正在使用 demo 的 client:可能 1-2 個封包丟失
T+5-10s memberlist 在其他 3 個 node 上偵測到 node3 失聯
T+10s 新 leader 選出(假設 node1)
T+10s node1 speaker 發 gratuitous ARP:「.54 現在是我」
T+10s+ 區網內所有機器更新 ARP cache(毫秒級)
T+11s 新 client 來訪:DNS 解析 .54 → ARP 拿 node1 的 MAC → 連線成功
T+11s 舊 client 用 keep-alive:自動 reconnect
T+5min 監控告警可能來告知 node3 down,但服務沒中斷
T+數小時 node3 修好、加回 cluster,speaker 自動加入 memberlist
總服務中斷:~10 秒。零人工介入。DNS 完全沒動。
| 步驟 | hostPort | MetalLB |
|---|---|---|
| 偵測失敗 | 人 / 監控(分鐘級) | memberlist(秒級) |
| 決定接手者 | 工程師決策 | 自動 leader election |
| 切換流量 | 改 DNS、等 TTL | 發 gratuitous ARP、毫秒級 |
| 客戶端感知 | DNS cache、需要清 | ARP cache、自動更新 |
現況:cluster 上有 demo(走 shared nginx in node3)。產品經理跑來說「我要加一個 analytics 服務,也要對外、要獨立的 ingress(不想跟 demo 共用 nginx)」。
Error: port 443 already allocated每加一個都要:找空 node、改 DNS、跟 IT 喬 firewall、跟客戶溝通新地址。
# analytics-ingress.yaml
apiVersion: v1
kind: Service
metadata:
name: analytics-nginx
spec:
type: LoadBalancer
loadBalancerIP: 192.168.50.55 # 想要這個 IP
selector:
app: analytics-nginx
ports:
- port: 443
$ kubectl apply -f analytics-ingress.yaml
service/analytics-nginx created
$ kubectl get svc -n analytics analytics-nginx
NAME TYPE EXTERNAL-IP PORT(S)
analytics-nginx LoadBalancer 192.168.50.55 443:31xxx/TCP
接下來:
整個過程 1 分鐘。 不用找 node、不用搶 port、不用每次跟 IT 喬。
某天有人不小心把 metallb-system 的 controller 砍了:
$ kubectl delete pod -n metallb-system metallb-controller-7d-xyz
pod "metallb-controller-7d-xyz" deleted
# 既有服務還在運作嗎?
$ curl -k https://192.168.50.54
HTTP/2 200 OK ✅ 沒影響
# 為什麼?
# - .54 的 ARP claim 是 speaker 負責的,不是 controller
# - speaker 還活著 → 還在持續宣稱 .54
# - kube-proxy iptables rules 還在 → 流量還能 NAT 到 pod
$ kubectl apply -f new-service.yaml
service/new-thing created
$ kubectl get svc new-thing
NAME TYPE EXTERNAL-IP PORT(S)
new-thing LoadBalancer <pending> 80:31xxx/TCP
↑ 卡住,因為沒有 controller 來配 IP
$ kubectl get pods -n metallb-system
NAME READY STATUS RESTARTS AGE
metallb-controller-7d-abc 1/1 Running 0 15s
↑ Deployment 自動拉起新的
$ kubectl get svc new-thing
NAME TYPE EXTERNAL-IP PORT(S)
new-thing LoadBalancer 192.168.50.58 80:31xxx/TCP
↑ 自動配出來了
Controller 的工作(control plane):
- 監聽 Service 變動
- 從 pool 配 IP
- 寫回 service.status
Speaker 的工作(data plane 的「廣告」部分):
- 持續回 ARP
- 維持 memberlist
- 不需要 controller 隨時在線
→ 已配發的 IP 就像「已發出去的駕照」,發證所關門了駕照還能用
某工程師手殘把 metallb-system 整個 namespace 砍了:
$ kubectl delete namespace metallb-system
所有 speakers + controller 都消失。
# 從外面打 .54 還會通
$ curl -k https://192.168.50.54
HTTP/2 200 OK ✅ 還是通的!
# 為什麼?
# - 區網內機器的 ARP cache 還記得「.54 = node2_mac」
# - kube-proxy iptables 還在
# - 流量仍能完整跑完路徑
⚠️ 這時候 admin 不會察覺問題。看 dashboard 一切正常。
# ARP cache 過期,client OS 重新發 ARP request
$ arping 192.168.50.54
ARPING 192.168.50.54
Sent 3 probes
Received 0 response(s) ← 沒人回了,因為 speakers 死光
# 此時連線斷
$ curl -k https://192.168.50.54
curl: (28) Failed to connect: timeout
$ kubectl get pods -n metallb-system
No resources found.
$ kubectl get ns metallb-system
Error: namespace "metallb-system" not found.
# 重灌:
$ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.X.X/config/manifests/metallb-native.yaml
$ kubectl apply -f ip-pool.yaml
$ kubectl apply -f l2-advertisement.yaml
# 幾秒後 speaker 起來:
$ kubectl get pods -n metallb-system
NAME READY STATUS RESTARTS
metallb-controller-... 1/1 Running 0
metallb-speaker-node1-... 1/1 Running 0
metallb-speaker-node2-... 1/1 Running 0
metallb-speaker-node3-... 1/1 Running 0
metallb-speaker-node4-... 1/1 Running 0
# speaker 重新 leader election,發 gratuitous ARP
# 區網 ARP cache 更新 → 服務恢復
某天某同事筆電插進公司網路,運氣不好抓到 192.168.50.54 當作靜態 IP(他不知道這 IP 是 MetalLB 用的)。
# 從外面打 .54,時通時不通
$ curl -k https://192.168.50.54
HTTP/2 200 OK
$ curl -k https://192.168.50.54
curl: (35) error: connection refused ← 突然斷
$ curl -k https://192.168.50.54
HTTP/2 200 OK ← 又通了
$ curl -k https://192.168.50.54
curl: (35) ... ← 又斷
$ sudo tcpdump -i eth0 -nn 'arp and host 192.168.50.54'
17:55:00.001 ARP, Request who-has 192.168.50.54 tell 192.168.50.10
17:55:00.002 ARP, Reply 192.168.50.54 is-at aa:bb:cc:dd:ee:ff ← node2 (speaker)
17:55:00.003 ARP, Reply 192.168.50.54 is-at ff:ee:dd:cc:bb:aa ← ???
↑ 別人的 MAC
兩個來源都宣稱自己是 .54。
# 用 MAC 找出是哪台機器
$ # 在 switch 上查 MAC 表,或 ping 一下
$ ip neigh | grep ff:ee:dd:cc:bb:aa
192.168.50.54 dev eth0 lladdr ff:ee:dd:cc:bb:aa REACHABLE
↑ 跑了 nmap 才知道是某同事的 MacBook
client 的 ARP cache 收到「兩個答案」會被覆寫:
T+0 收到 node2 reply → cache: .54 = node2_mac → 連 cluster OK
T+1s 收到 macbook reply → cache: .54 = macbook_mac → 連到 macbook,沒人 listen 443 → reset
T+30s ARP cache 過期,重發 request,又收到兩個 reply...
192.168.50.54-59 排除在 DHCP pool 外、跟所有人公告這段 IP 是 cluster 保留# 部署 MetalLB pool 之前,先確認整段 IP 真的沒人用
for i in 54 55 56 57 58 59; do
arping -c 2 -I eth0 192.168.50.$i 2>&1 | grep -q "Received 0 response" && \
echo ".$i 沒人用 ✓" || echo ".$i 有人用 ✗"
done
跟 IT 確認 DHCP exclusion + 文件化「這段 IP 是 cluster 保留」。
推薦:MetalLB
理由:規模上去後 hostPort 不夠用,IP 不該綁 node,需要彈性。
推薦:hostPort + nodeSelector
理由:
判斷標準:
| 情境 | hostPort 痛點 | MetalLB 體驗 |
|---|---|---|
| Node 半夜掛了 | 5am pager call、登入修 | 醒來看告警、發現自己癒合了 |
| 新需求要加 ingress | 一周喬 node + DNS + IT | 5 分鐘 PR + apply |
| 客戶要 IP 白名單 | 「不同 app 不同 IP,要動 7 個地方」 | 給一個 IP pool 一勞永逸 |
| Pen test 要求隔離 | 「沒辦法,全部一顆 nginx」 | 每個 tenant 自己一條 |
| 換掉 node 硬體 | 改 DNS、清 cache、跟客戶解釋 | 換掉就好、IP 不動 |
前文:on-prem k8s 給 app 獨立 IP — MetalLB vs hostPort+nodeSelector