Service:擋在短命 Pod 前面的固定門牌

· tech

#kubernetes#concept#networking

第二篇留了一個問題:既然 Pod 是短命的、被換掉就有新的 IP,那別的服務要怎麼穩定找到它?你總不能把某顆 Pod 的 IP 寫死在設定裡——它下一秒可能就不在了。這篇的主角 Service,就是 K8s 給這個問題的答案:一個固定的門牌,擋在一群會變的 Pod 前面。

先看清問題:Pod 的 IP 不能記

上一篇的滾動更新,每換一次版,舊 Pod 全被新 Pod 取代——而新 Pod 有新 IP。 ReplicaSet 補一顆掛掉的 Pod 時,補上的也是全新的 IP。也就是說,Pod 的 IP 本質上是浮動的。任何「我先查到後端 Pod 的 IP,然後記起來連它」的做法,都注定會在某次重排、某次換版之後斷線。你需要的是一個不會變的東西當中介。

Service:一個固定 IP + 一個 DNS 名字

Service 就是那個不變的中介。你給它一個 label selector(例如 app=web),它就代表所有帶這個標籤的 Pod;對外它有一個固定的虛擬 IP(ClusterIP)和一個固定的 DNS 名字。呼叫方永遠打這個門牌,Service 再把流量負載均衡到背後健康的 Pod:

呼叫方 只認 web 這名字 Service:web 固定 IP 10.96.0.10 · DNS web.*.svc selector: app=web 自動負載均衡到健康 Pod Pod · 10.1.2.7 ✓ 健康 Pod · IP 會變 掛了換新:.8 → .31 Pod · 10.1.4.2 ✓ 健康
呼叫方永遠打 Service 的固定門牌;背後 Pod 生生死死、IP 一直換,它完全無感——這就是 Service 的核心價值:用穩定的抽象擋住會變的實體

宣告一個 Service 其實很短,重點就是那個 selector:

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web           # 選中所有帶這個標籤的 Pod(不管它現在幾個、IP 是多少)
  ports:
    - port: 80         # Service 對外的 port
      targetPort: 8080 # 實際轉到 Pod 的 port

有了它,叢集裡其他服務要連,只要用名字——http://web(同 namespace)或 http://web.default.svc.cluster.local(跨 namespace 的完整寫法)。沒有人需要知道背後 Pod 是誰、有幾個、IP 是多少。

背後是一份會自動更新的名單

Service 怎麼知道現在哪些 Pod 該收流量?它背後有一份 Endpoints(新版叫 EndpointSlice)名單,列出「目前符合 selector 且通過健康檢查的 Pod IP」。而這份名單不是你維護的——又是 reconcile loop:

  • Pod 增減、被重排、換版 → controller 立刻更新這份名單。
  • Pod 還沒 ready(readiness probe 沒過)→ 不會被放進名單,流量不會打過去。

這正是滾動更新能做到「不中斷」的關鍵:新 Pod 要真的準備好才會進名單收流量,舊 Pod 收乾淨才移除。你在第一篇看到的那個「宣告期望、loop 收斂現實」的模式,在這裡又出現一次——只是這次收斂的對象是「哪些 Pod 該收流量」。

從叢集內到公網:Service 的幾種類型

上面的 ClusterIP 只有叢集內連得到。但總有些服務要讓外面連進來,於是 Service 有幾種「對外程度」遞增的類型:

ClusterIP 預設 · 只有叢集內能連(後端服務彼此互打) NodePort 每台 Node 開一個 port,外面用 Node:Port 連(測試常用) LoadBalancer 雲端配一個對外 IP / LB,正式對外服務的做法 Ingress L7 入口:一個 IP 按 host/path 分流到多個 Service 對外程度遞增
一層層把服務從叢集內推向公網。實務上對外多半是「Ingress 擺在 LoadBalancer 後面,按 path 把流量分給各個 ClusterIP Service」——它們最後都指向同一種內部門牌

實務上最常見的組合,是一個對外的 Ingress(或 LoadBalancer)+ 一堆內部的 ClusterIP Service:外面只開一個入口,進來後按網址路由到各個服務。至於 ClusterIP 的一個變形——headless Service(不配虛擬 IP、不做負載均衡,而是讓你用 DNS 直接拿到每一顆 Pod)——是有狀態服務(資料庫、StatefulSet)會用到的,系列第 6 篇再談。

反思

「穩定的抽象擋在會變的實體前面」是整個 K8s 的招牌動作

Service 這個設計,我越想越覺得漂亮:它沒有讓 Pod 變得更穩定,而是接受 Pod 就是會變,然後在前面架一層永遠不動的門牌。這跟第二篇說的「Pod 是牛不是寵物」是同一套哲學的兩面——你不去馴服會變的東西,你在它前面放一個不變的介面。我在 Airflow + Spark on K8s 那篇就吃過這個甜頭:Spark 的 executor 生生死死,但它們要找的 driver 靠一個固定的 Service 名字就找得到,不用管 driver Pod 實際落在哪台、IP 是多少。一旦你習慣「用穩定抽象包住短命實體」這個模式,K8s 後面的東西——Volume、StatefulSet、Ingress——都是它的變形。

服務之間用「名字」相認,而不是 IP

從寫死 IP 改成用 DNS 名字連服務,這個轉變比它看起來更重要。以前微服務之間互打,最痛的就是「那台機器 IP 換了、那個 port 改了」這種脆弱的耦合;在 K8s 裡,http://web 這個名字幾乎永遠有效,底層 Pod 怎麼搬、怎麼擴縮,名字都不變。這讓我在設計服務邊界時心態完全不同——我依賴的是一個穩定的合約(名字 + port),不是一個易碎的位置(IP)。 這也是為什麼我現在看任何系統,都會先問一句:「這裡耦合的是名字還是位置?」耦合位置的,遲早會為了那個位置的變動付出代價。

又是那個 reconcile loop——只是換了收斂對象

寫到這篇我更確定第一篇的判斷:reconcile loop 是理解 K8s 的那把萬能鑰匙。 Deployment 收斂的是「Pod 數量」,Service 背後的 Endpoints 收斂的是「哪些 Pod 該收流量」,本質是同一個迴圈。這對我 debug 特別有用:當「服務連不到後端」時,我不會瞎猜,而是直接去看那份名單——Endpoints 是不是空的?八成是 selector 標籤打錯、或 Pod 的 readiness probe 一直沒過所以沒進名單。把每個功能都還原成「誰在收斂什麼」,問題就從玄學變成一條可以按圖索驥的線。