Share Notes

chundev

View the Project on GitHub latteouka/share-notes

MetalLB 實戰劇本 — 8 個情境看懂 on-prem 獨立 IP

日期:2026-05-13 情境:on-prem k3s 4-node cluster,用具體劇情拆解 MetalLB 各個機制


TL;DR

前一篇講了「為什麼用 MetalLB」和「跟 hostPort 比較」。這一篇用 8 個具體劇情把每個概念踩過一遍 — IP 怎麼憑空出現、node 斷電怎麼自動切、controller 死掉會發生什麼、IP 衝突怎麼診斷。看完應該對「MetalLB 真實運作」有畫面感。


劇情 1:IP 是「宣稱」出來的 — 用 ARP 把 .54 變出來

假設子網段 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 沒人在用。

安裝 MetalLB 前:.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 在整個子網路上不存在。封包送不出去。

安裝 MetalLB + 配 pool .54-.59

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

$ 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」。


劇情 2:tcpdump 三個 node 角度同步觀察

用 tcpdump 看 .54 收到流量時,三個 node 各看到什麼。

前置:

Node2(leader)看到:

$ 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 ...

Node1(pod 真正所在的 node)看到:

# 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

Node3(路人)看到:

$ 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,不是我)

結論


劇情 3:node3 突然斷電 — hostPort vs MetalLB 時序對比

假設 demo 服務跑在 cluster 上,node3 是入口。突然有人踢到電源線。

情境 A:用 hostPort(demo DNS → 192.168.50.53)

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 次。

情境 B:用 MetalLB(demo Service → 192.168.50.54)

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、自動更新

劇情 4:加第二個獨立 ingress 的成本

現況:cluster 上有 demo(走 shared nginx in node3)。產品經理跑來說「我要加一個 analytics 服務,也要對外、要獨立的 ingress(不想跟 demo 共用 nginx)」。

用 hostPort 加:步驟一大堆

  1. 想 pin analytics 的 nginx 在 node3 → ❌ Error: port 443 already allocated
  2. 改 pin 在 node1
  3. analytics DNS 要指 .51
  4. 跟 IT 部門要 .51 開通 443 (firewall)
  5. 客戶要試 → 給他 IP .51
  6. 過幾個月「我又要再加一個 monitoring 獨立 ingress」
  7. 想 pin 在 node2 → 還可以
  8. 過幾個月「再加一個 staging」
  9. 想 pin → 沒 node 了 ❌
  10. 開新 cluster?買新 VM?掙扎

每加一個都要:找空 node、改 DNS、跟 IT 喬 firewall、跟客戶溝通新地址。

用 MetalLB 加:宣告式

# 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 喬。


劇情 5:MetalLB controller pod 突然死掉

某天有人不小心把 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

試圖建新 Service:卡住

$ 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

Controller 自動重啟(k8s 機制)

$ 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 死掉不影響流量?

Controller 的工作(control plane):
  - 監聽 Service 變動
  - 從 pool 配 IP
  - 寫回 service.status

Speaker 的工作(data plane 的「廣告」部分):
  - 持續回 ARP
  - 維持 memberlist
  - 不需要 controller 隨時在線

→ 已配發的 IP 就像「已發出去的駕照」,發證所關門了駕照還能用

劇情 6:所有 speaker 都死光(災難測試)

某工程師手殘把 metallb-system 整個 namespace 砍了:

$ kubectl delete namespace metallb-system

所有 speakers + controller 都消失。

T+0 到 T+~ARP cache 過期前:「殭屍服務」

# 從外面打 .54 還會通
$ curl -k https://192.168.50.54
HTTP/2 200 OK     ✅ 還是通的!

# 為什麼?
# - 區網內機器的 ARP cache 還記得「.54 = node2_mac」
# - kube-proxy iptables 還在
# - 流量仍能完整跑完路徑

⚠️ 這時候 admin 不會察覺問題。看 dashboard 一切正常。

T+ARP cache TTL(典型 5-15 分鐘):服務開始壞

# 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 更新 → 服務恢復

學到的事


劇情 7:一場真實的 IP conflict 意外

某天某同事筆電插進公司網路,運氣不好抓到 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) ...   ← 又斷

診斷:tcpdump 看 ARP 廣播

$ 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...

修法

  1. 短期:找到那台 MacBook,請他重新拿 DHCP IP
  2. 長期:跟 IT 確認 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 保留」。


劇情 8:客戶機房 vs 公司 cluster — 同個需求兩個答案

場景 A:公司多 app cluster

推薦:MetalLB

理由:規模上去後 hostPort 不夠用,IP 不該綁 node,需要彈性。

場景 B:客戶機房單機部署

推薦:hostPort + nodeSelector

理由:

場景 C:客戶機房中型部署(3-5 node)

判斷標準


總結:什麼時候你會慶幸用了 MetalLB

情境 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