chundev
日期:2026-05-13 情境:on-prem k3s(多 node)、想讓不同 app 各自有獨立 ingress IP
on-prem k8s 沒有雲端 LoadBalancer,要讓 app 拿到「不綁特定 node 的 IP」基本上兩條路:MetalLB L2 模式(IP pool + ARP 廣播)或 dedicated nginx + hostPort + nodeSelector(IP 等於 node IP)。多 node cluster + 想任意分配 IP,MetalLB 是甜蜜點;單機客戶端 prod,hostPort 反而簡單到 IT 看得懂。
k8s Service type LoadBalancer 是設計給「雲端」用的。當你寫:
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
type: LoadBalancer
ports:
- port: 80
在 AWS / GCP / Azure,k8s controller-manager 會 call 雲廠商 API 建一個 ELB / GLB,把 external IP 寫回 service。
on-prem k8s 沒有這個 API 可以 call,所以你會看到:
$ kubectl get svc
NAME TYPE EXTERNAL-IP PORT(S)
nginx LoadBalancer <pending> 80:32xxx/TCP
External-IP 永遠卡在 <pending>。這不是 bug,是「沒人來填這個欄位」。
MetalLB 就是「on-prem 版本的雲端 LB 模擬器」,補上這塊缺。
| 方案 | 原理 | 適用 |
|---|---|---|
| A. shared ingress + 多 host rule | 整個 cluster 一顆 nginx,靠 Host header 分流 | 個人專案、原型 |
| B. dedicated nginx + hostPort + nodeSelector | 每個 app 一顆 nginx pin 到某 node,那個 node 的 IP 就是 ingress IP | 客戶端單機部署 |
| C. MetalLB + Service type=LoadBalancer | MetalLB controller 分配 IP pool,speaker 廣播 ARP | multi-node cluster、想任意 IP |
A 最簡單但「一顆 nginx 死全死」;B 最白話但 IP 跟著 node 走;C 最 cloud-native 但多一坨 controller。
┌─────────────────────────────────────────────────┐
│ Controller (Deployment, 單副本) │
│ - 監聽 Service type=LoadBalancer 變化 │
│ - 從 IPAddressPool 配 IP │
│ - 寫回 service.status.loadBalancer.ingress │
└─────────────────────────────────────────────────┘
│ 配出 IP 後
▼
┌─────────────────────────────────────────────────┐
│ Speaker (DaemonSet, 每 node 一個) │
│ - L2 模式:用 ARP/NDP 告訴外面「我能接這個 IP」 │
│ - BGP 模式:跟 router 講 BGP route │
└─────────────────────────────────────────────────┘
關鍵理解:
假設 demo Service 拿到 192.168.50.54,client 用瀏覽器打 https://demo.example.com 進來:
1. DNS: demo.example.com → 192.168.50.54
2. Client: "Who has 192.168.50.54?" (ARP broadcast)
│
▼ L2 broadcast 到整個 VLAN
3. Speaker leader (假設在 node2) 回答:
"192.168.50.54 is at MAC=aa:bb:cc:dd:ee:ff" (= node2 的 MAC)
│
▼ Client 拿到 MAC,把封包送到 node2 的 NIC
4. node2 kernel 收到目的 IP=.54 的封包
│
▼ kube-proxy 的 iptables/IPVS rules 命中
5. NAT 到後端 pod IP
│
▼
6. pod 處理請求
第 4 步是反直覺的關鍵: 192.168.50.54 沒有真的綁在 node2 的網卡上。
# 在 node2 跑這個 — 看不到 .54
$ ip addr show
inet 192.168.50.52/24 ... # 只有 node 本身的 IP
.54 純粹是 speaker 在 ARP 層「騙」網路 — 跟 client 說「.54 的 MAC 是我」。kernel 收到封包後,靠 kube-proxy 設定的 iptables 規則做 NAT 給後端 pod。
這就是為什麼 MetalLB 不需要動 kernel network、不需要管理員權限改 NIC config。
每個 Service IP 由 單一 speaker 負責回 ARP(不是所有 speaker 一起回,否則 ARP 衝突)。
Leader 怎麼選:
Leader node 死掉發生什麼:
| 時間 | 事件 |
|---|---|
| T+0 | leader node 突然死 |
| T+5~10s | 其他 speakers 透過 memberlist health check 偵測到 leader 失聯 |
| T+10s | 新 leader 接手,立刻發 gratuitous ARP(”我是 .54 的新 MAC”) |
| T+10s+ε | 路由器 / client ARP cache 更新,流量切到新 node |
整個切換典型在 10 秒內完成。 慢的部分主要在 memberlist 偵測(5-10s),ARP 廣播本身是毫秒級。
⚠️ 潛在陷阱:
externalTrafficPolicy: Local 可能在多副本情境出現 ARP storm(issue #2747)| 維度 | L2 | BGP |
|---|---|---|
| 網路需求 | 跟 cluster 在同 L2 broadcast domain | 需要一台會講 BGP 的 router |
| 流量分散 | 單 node 接,kube-proxy 內部分散 | 多 node ECMP 並行接 |
| 故障切換 | ARP re-announce,~10s | BGP withdraw,~秒級 |
| 適用 | 小至中型部署、辦公室機房 | 多機房 / 大流量 / 跟 IT 協作 |
大多數 on-prem 場景 L2 就夠用。BGP 是「你的網路 team 願意配合,而且流量真的大」才考慮。
| 維度 | MetalLB L2 | hostPort + nodeSelector |
|---|---|---|
| IP 來源 | 從 pool 任意分配(e.g. .54-.59) |
固定等於 nodeSelector 選中的 node IP |
| 加新獨立 ingress | 改 manifest 申請新 IP 即可 | 需要找新 node(hostPort 80/443 不能重複占) |
| node 掛掉 | speaker 自動接手,~10s 切過去 | ingress 整個掛,要手動換 node 並改 DNS |
| DNS 變更頻率 | 配出 IP 後不再變 | 每次換 node 都要改 DNS |
| 裝設成本 | controller + speaker DaemonSet(2 個 manifest) | 0,直接 Deployment + hostPort |
| IT 認知門檻 | 要理解 ARP / VIP 概念 | 「這個 IP 就是這台機器」直觀 |
| 多 node 利用率 | speakers 在所有 node,IP 可漂移 | nginx 只在被 pin 的 node |
MetalLB 適合:
hostPort + nodeSelector 適合:
決策樹:
on-prem cluster?
├ 1-2 node 客戶機房 → hostPort + nodeSelector
└ 3+ node 內部 cluster
├ 預期長期會有多個獨立 ingress → MetalLB
└ 只有一個 app、IP 永遠不變 → 用既有 shared ingress 也行
兩者不衝突可共存:既有 shared nginx 繼續用 hostPort,新獨立 app 走 MetalLB + LoadBalancer。