Share Notes

chundev

View the Project on GitHub latteouka/share-notes

on-prem k8s 給 app 獨立 IP — MetalLB vs hostPort+nodeSelector

日期:2026-05-13 情境:on-prem k3s(多 node)、想讓不同 app 各自有獨立 ingress IP


TL;DR

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 看得懂。


背景:on-prem LoadBalancer 為什麼永遠 pending

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。


MetalLB 兩元件架構

┌─────────────────────────────────────────────────┐
│  Controller (Deployment, 單副本)                  │
│   - 監聽 Service type=LoadBalancer 變化           │
│   - 從 IPAddressPool 配 IP                        │
│   - 寫回 service.status.loadBalancer.ingress      │
└─────────────────────────────────────────────────┘
                     │ 配出 IP 後
                     ▼
┌─────────────────────────────────────────────────┐
│  Speaker (DaemonSet, 每 node 一個)                │
│   - L2 模式:用 ARP/NDP 告訴外面「我能接這個 IP」    │
│   - BGP 模式:跟 router 講 BGP route              │
└─────────────────────────────────────────────────┘

關鍵理解:


L2 模式封包路徑(從 ARP 到 pod)

假設 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


Leader election 與故障切換

每個 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 廣播本身是毫秒級。

⚠️ 潛在陷阱:


L2 vs BGP

維度 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 vs hostPort+nodeSelector 對比

維度 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

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。


學到的事


參考資料