「服務發現」模式提供一個穩定的端點,讓服務的客戶端能存取到提供該服務的實例。為此,Kubernetes 依「服務的消費者與提供者位於叢集內或叢集外」提供了多種機制。
問題#
部署在 Kubernetes 上的應用很少獨自存在,通常必須與叢集內的其他服務、或叢集外的系統互動。互動可以由內部發起,也可以由外部刺激觸發:
- 內部發起通常透過輪詢式消費者(polling consumer):應用在啟動後或稍後主動連上另一個系統,開始收送資料。典型例子包括:Pod 內的應用連上檔案伺服器開始消費檔案、連上訊息代理開始收發訊息、連上關聯式資料庫或鍵值儲存開始讀寫資料。這種情境不需要 Kubernetes 額外設定,「批次工作」與「週期性工作」模式常用這個技巧。
- 外部刺激則是 Kubernetes 工作負載更常見的使用案例:長期執行的服務等待外部刺激,最常見的形式是來自叢集內其他 Pod 或外部系統的 HTTP 連線。
在後一種情況下,服務消費者需要一套機制來發現那些由排程器動態配置、且可能被彈性擴縮的 Pod。若得由我們自己追蹤、註冊與發現動態 Pod 的端點,將是極大的負擔——這正是 Kubernetes 透過多種機制實作服務發現模式的原因。
解法#
回顧「Kubernetes 之前的年代」,最常見的服務發現機制是客戶端發現(client-side discovery):當服務消費者要呼叫另一個可能擴展成多個實例的服務時,消費者身上帶著一個發現代理,能查詢註冊表取得服務實例清單,再挑一個來呼叫。典型做法是在消費端服務內嵌一個代理(如 ZooKeeper client、Consul client 或 Ribbon),或用共置的另一個行程(如 Prana)去註冊表查詢。

圖 12-1:客戶端服務發現
到了「Kubernetes 之後的年代」,分散式系統的許多非功能性職責——配置、健康檢查、修復、資源隔離——都往平台移動,服務發現與負載平衡也是如此。用服務導向架構(SOA)的說法:服務提供者實例在提供服務能力時仍須向服務註冊表註冊,服務消費者則須存取註冊表資訊才能連到服務。
在 Kubernetes 世界裡,這一切都在幕後發生:服務消費者呼叫一個固定的虛擬 Service 端點,由它動態發現以 Pod 實作的服務實例。 這是伺服器端服務發現。

圖 12-2:伺服器端服務發現——註冊與查詢都由 Kubernetes 承接
服務發現乍看是個簡單模式,實則可用多種機制實作,取決於服務消費者在叢集內或外、以及服務提供者在叢集內或外。
叢集內部的服務發現#
假設我們有個網頁應用要跑在 Kubernetes 上。一旦建立含數個複本的 Deployment,排程器就把 Pod 放到合適節點,每個 Pod 啟動前會取得一個叢集 IP。但若另一個 Pod 中的客戶端服務想消費這個網頁應用的端點,沒有簡單的辦法事先知道提供者 Pod 的 IP。
這正是 Kubernetes Service 資源要解決的問題:它為一組提供相同功能的 Pod 提供恆定而穩定的進入點。
最簡單的建立方式是 kubectl expose——它會建立虛擬 IP(稱為 clusterIP),並從資源中擷取 Pod 選擇器與埠號來組出 Service 定義。但若要完全掌控定義,我們手動建立:
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
selector:
app: random-generator # 比對 Pod 標籤的選擇器
ports:
- port: 80 # 可透過此埠聯繫到本 Service
targetPort: 8080 # Pod 實際監聽的埠
protocol: TCP這會建立一個名為 random-generator(名稱很重要,稍後發現時要用)、type: ClusterIP(預設值)的 Service,在 80 埠接受 TCP 連線,並路由到所有 app: random-generator 相符 Pod 的 8080 埠。Pod 何時、如何被建立都無所謂——任何相符的 Pod 都會成為路由目標。

圖 12-3:叢集內部的服務發現
兩個要記住的重點:Service 一旦建立就取得一個
clusterIP,該 IP 只能從叢集內部存取(名稱由此而來);而且只要 Service 定義存在,這個 IP 就不會改變。
那麼叢集內的其他應用如何得知這個動態配置的 clusterIP?有兩種方式。
透過環境變數發現#
Kubernetes 啟動 Pod 時,會把當下已存在的所有 Service 的細節填入其環境變數:
RANDOM_GENERATOR_SERVICE_HOST=10.109.72.32
RANDOM_GENERATOR_SERVICE_PORT=8080該 Pod 內的應用只要知道自己要消費的 Service 名稱,就能寫程式讀取這些環境變數。這是簡單機制,任何語言寫的應用都能用,也很容易在叢集外模擬以供開發測試。
主要問題是對 Service 建立時機的時序依賴:環境變數無法注入已在執行的 Pod,因此只有在 Service 建立之後才啟動的 Pod 才拿得到座標。這要求 Service 必須先於依賴它的 Pod 被定義——否則那些 Pod 必須重啟。
透過 DNS 查詢發現#
Kubernetes 執行一台 DNS 伺服器,所有 Pod 都自動被設定使用它。新 Service 建立時會自動取得新的 DNS 條目,所有 Pod 立刻可用。只要客戶端知道要存取的 Service 名稱,就能用完整網域名稱(FQDN)連到它:
random-generator.default.svc.cluster.local其中 random-generator 是 Service 名稱、default 是命名空間、svc 表示這是 Service 資源、cluster.local 是叢集特定後綴。叢集後綴可以省略;從同一命名空間存取時,命名空間也可以省略。
DNS 發現機制沒有環境變數機制的時序缺陷:只要 Service 一被定義,DNS 伺服器就允許所有 Pod 查詢所有 Service。不過若埠號是非標準的、或消費者不知道,你可能仍需靠環境變數查詢埠號。
type: ClusterIP 的其他特性#
其他 Service 型別都建立在這些特性之上:
- 多埠:單一 Service 定義可支援多組來源與目標埠。例如 Pod 同時支援 8080 的 HTTP 與 8443 的 HTTPS,不必定義兩個 Service,一個就能同時曝露 80 與 443。
- Session 親和性:預設情況下,新請求進來時 Service 會隨機挑一個 Pod 連線。可用
sessionAffinity: ClientIP改成「同一客戶端 IP 的所有請求都黏在同一個 Pod」。 - 就緒探針:若 Pod 定義了就緒檢查且檢查失敗,即使標籤選擇器相符,該 Pod 仍會被移出 Service 端點清單。
- 虛擬 IP:
type: ClusterIP取得的穩定虛擬 IP 不對應任何網路介面,實際上並不存在。是每個節點上的 kube-proxy 接手這個新 Service,更新節點的 iptables 規則,攔截目的地為此虛擬 IP 的封包並換成選定的 Pod IP。 - 指定 ClusterIP:建立時可用
.spec.clusterIP指定要用的 IP,必須是預定義範圍內的有效位址。雖然不建議,但在處理「被設定成使用特定 IP 的遺留應用」、或想沿用既有 DNS 條目時可能派上用場。
iptables 規則只加入 Service 定義中指定的協定(如 TCP 或 UDP),不加 ICMP 規則。因此無法 ping 一個 Service 的 IP 位址(ping 用的是 ICMP),但透過 TCP 存取(例如發 HTTP 請求)當然沒問題。
type: ClusterIP 的 Service 只能從叢集內存取,透過比對選擇器來發現 Pod,是最常用的型別。
手動指定的服務發現#
當我們建立帶 selector 的 Service 時,Kubernetes 會在端點資源清單中追蹤相符且可服務的 Pod(可用 kubectl get endpoints random-generator 查看)。但我們也可以把連線導向外部 IP 與埠,做法是省略 selector 定義,並手動建立端點資源:
apiVersion: v1
kind: Service
metadata:
name: external-service
spec:
type: ClusterIP
ports:
- protocol: TCP
port: 80接著定義一個同名的 endpoints 資源,內含目標 IP 與埠:
apiVersion: v1
kind: Endpoints
metadata:
name: external-service # 名稱必須與存取這些 Endpoints 的 Service 相同
subsets:
- addresses:
- ip: 1.1.1.1
- ip: 2.2.2.2
ports:
- port: 8080這個 Service 同樣只能在叢集內存取,消費方式與前述相同(環境變數或 DNS 查詢)。差別在於端點清單是手動維護的,其中的值通常指向叢集之外的 IP。

圖 12-4:手動指定的服務發現
Endpoints 可以放 Pod 的 IP,但不能放其他 Service 的虛擬 IP。
Service 有個好處:允許增刪 selector、在外部與內部提供者之間切換,而不必刪除資源定義(刪除會導致 Service IP 改變)。因此服務消費者可以繼續使用最初指向的同一個 Service IP,而實際的服務提供者實作則從地端遷移到 Kubernetes,完全不影響客戶端。
同屬手動配置目的地這一類,還有一種 Service:
apiVersion: v1
kind: Service
metadata:
name: database-service
spec:
type: ExternalName
externalName: my.database.example.com
ports:
- port: 80這個定義同樣沒有 selector,但 type 是 ExternalName。從實作角度看這是重要差異:它只透過 DNS 映射到 externalName 所指的內容,等於用 DNS CNAME 為外部端點建立別名,而不是透過 IP 位址走代理。
從叢集外部進行服務發現#
前述機制都使用只能從叢集內存取的虛擬 IP。但 Kubernetes 叢集不會與世隔絕——除了從 Pod 連到外部資源,外部應用想連到 Pod 提供的端點同樣常見。
NodePort#
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
type: NodePort # 在所有節點上開啟連接埠
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
# 指定固定埠(必須可用),或省略此欄位讓系統隨機配置
nodePort: 30036
protocol: TCP這個定義除了做前述 Service 該做的事之外,還在所有節點上保留 30036 埠並把進來的連線轉發給該 Service。於是這個 Service 既能在內部透過虛擬 IP 存取,也能在外部透過每個節點的專屬埠存取。

圖 12-5:NodePort 服務發現
這個做法看似不錯,但有若干值得注意的特性與缺點:
- 埠號:可以不指定
nodePort: 30036,讓 Kubernetes 從其範圍內挑一個空閒埠。 - 防火牆規則:由於這個做法在所有節點上開埠,你可能得額外設定防火牆規則讓外部客戶端連得進來。
- 節點選擇:外部客戶端可以連到叢集中任一節點。但若該節點不可用,挑選另一個健康節點是客戶端應用自己的責任。因此在節點前面放一個負載平衡器來挑健康節點並做故障移轉,通常是好主意。
- Pod 選擇:客戶端經 node port 連入時,會被路由到隨機挑選的 Pod,可能在同一節點,也可能在其他節點。加上
externalTrafficPolicy: Local可以避免這多一跳、強制 Kubernetes 只挑連線進入的那個節點上的 Pod。但設了這個選項後,Kubernetes 不允許連到其他節點上的 Pod,這可能造成問題——解法是確保每個節點上都有 Pod(例如用常駐服務),或確保客戶端知道哪些節點上有健康的 Pod。 - 來源位址:使用 NodePort 時,客戶端位址會被 source NAT——封包中含有客戶端 IP 的來源位址,會被換成節點的內部位址。例如客戶端把封包送到節點 1,節點 1 把來源位址換成自己的位址、目的位址換成 Pod 位址,再轉發到 Pod 所在的節點 2;Pod 收到封包時看到的來源位址是節點 1,而非原始客戶端。要避免這點,同樣可設
externalTrafficPolicy: Local,只把流量轉給節點 1 上的 Pod。
LoadBalancer#
NodePort 的限制是:我們仍需要一個負載平衡器來讓客戶端挑到健康節點。type: LoadBalancer 解決了這點——它除了建立一般 Service、在每個節點開埠之外,還會用雲端供應商的負載平衡器把服務對外曝露。

圖 12-6:負載平衡器服務發現——一台專有的負載平衡器作為進入 Kubernetes 叢集的閘道
因此這種 Service 只在雲端供應商支援 Kubernetes 並能佈建負載平衡器時才管用。
apiVersion: v1
kind: Service
metadata:
name: random-generator
spec:
type: LoadBalancer
clusterIP: 10.0.171.239 # clusterIP 與 loadBalancerIP 由 Kubernetes 在可用時指派
loadBalancerIP: 78.11.24.19
selector:
app: random-generator
ports:
- port: 80
targetPort: 8080
protocol: TCP
status: # status 欄位由 Kubernetes 管理,會加上 Ingress IP
loadBalancer:
ingress:
- ip: 146.148.47.155有了這份定義,外部客戶端就能連到負載平衡器,由它挑選節點並定位 Pod。
負載平衡器的佈建與服務發現的具體做法因雲端供應商而異:有些允許你定義負載平衡器位址,有些不行;有些提供保留來源位址的機制,有些則會換成負載平衡器位址。請查閱你所選供應商的具體實作。
還有一種 Service:headless service,你不為它請求專屬 IP,做法是在 spec 中設
clusterIP: None。對 headless service 而言,後端 Pod 會被加入內部 DNS 伺服器,最適合用於為 StatefulSet 實作 Service(詳見「有狀態服務」)。
應用層的服務發現:Ingress#
與前述機制不同,Ingress 不是一種 Service 型別,而是獨立的 Kubernetes 資源。它坐在 Service 前面,扮演智慧路由器與叢集進入點的角色,通常透過對外可及的 URL 提供基於 HTTP 的 Service 存取、負載平衡、SSL 終結與基於名稱的虛擬主機(也有其他特化的 Ingress 實作)。
Ingress 要能運作,叢集中必須有一個或多個 Ingress controller 在執行。
最簡單的 Ingress 曝露單一 Service:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: random-generator
spec:
backend:
serviceName: random-generator
servicePort: 8080依 Kubernetes 所在的基礎設施與 Ingress controller 實作,這份定義會配置一個對外可及的 IP,並在 80 埠曝露該 Service。但這和 type: LoadBalancer 差別不大——後者每份 Service 定義都需要一個外部 IP。
以下是依 HTTP URI 路徑把單一 IP 路由到多個 Service 的簡單扇出(fan-out)設定:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: random-generator
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules: # 給 Ingress controller 依請求路徑分派請求的專屬規則
- http:
paths:
- path: / # 把所有請求導到 random-generator 服務……
backend:
serviceName: random-generator
servicePort: 8080
- path: /cluster-status # ……除了 /cluster-status,它導往另一個服務
backend:
serviceName: cluster-status
servicePort: 80每種 Ingress controller 實作都不同,除了常規的 Ingress 定義外,controller 可能需要透過**註解(annotation)**傳入額外組態。

圖 12-7:應用層的服務發現
Ingress 是 Kubernetes 上最強大、同時也最複雜的服務發現機制。當你要在同一個 IP 底下曝露多個服務、且所有服務都使用相同的 L7(通常是 HTTP)協定時,它最有用。
延伸:OpenShift Routes
Red Hat OpenShift 是流行的 Kubernetes 企業版發行版。除了完全相容 Kubernetes 之外,它還提供一些額外功能,其中之一是 Route,與 Ingress 非常相似——相似到差異都不太容易看出來。首先,Route 早於 Kubernetes 引入 Ingress 物件,因此可視為 Ingress 的某種前身。
技術上兩者仍有差異:
- Route 會被 OpenShift 內建整合的 HAProxy 負載平衡器自動接手,因此不需要另外安裝 Ingress controller(不過你也可以替換掉 OpenShift 內建的負載平衡器)。
- 對通往 Service 的那一段,可使用 re-encryption 或 pass-through 等額外的 TLS 終結模式。
- 可使用多個帶權重的後端來分流流量。
- 支援萬用字元網域。
話說回來,你在 OpenShift 上同樣可以使用 Ingress,兩者可自由選擇。
討論#
叢集內部發現動態 Pod 一律透過 Service 資源達成,只是不同選項導向不同實作。Service 抽象是一種高階、雲原生的方式,用來配置虛擬 IP、iptables、DNS 記錄、環境變數這些低階細節。叢集外部的服務發現則建立在 Service 抽象之上,聚焦於把服務曝露給外界:NodePort 提供了曝露服務的基本功能,但高可用的配置需要與平台基礎設施供應商整合。
下表把本章各種機制由簡入繁整理如下:
| 名稱 | 組態 | 客戶端類型 | 摘要 |
|---|---|---|---|
| ClusterIP | type: ClusterIP + .spec.selector | 內部 | 最常用的內部發現機制 |
| Manual IP | type: ClusterIP + kind: Endpoints | 內部 | 外部 IP 的發現 |
| Manual FQDN | type: ExternalName + .spec.externalName | 內部 | 外部 FQDN 的發現 |
| Headless Service | type: ClusterIP + .spec.clusterIP: None | 內部 | 不含虛擬 IP、基於 DNS 的發現 |
| NodePort | type: NodePort | 外部 | 非 HTTP 流量的首選 |
| LoadBalancer | type: LoadBalancer | 外部 | 需要雲端基礎設施支援 |
| Ingress | kind: Ingress | 外部 | 基於 L7/HTTP 的智慧路由機制 |
旅程並未止步於此。Knative 專案在 Kubernetes 之上引入了新的原語,協助應用開發者處理進階的服務供給、建置與訊息傳遞。
就服務發現而言,Knative serving 子專案特別值得注意:它引入了一個與本章 Service 同 kind(但屬於不同 API group)的新 Service 資源,除了支援應用版本(revision)之外,也支援負載平衡器背後極具彈性的服務擴縮。
更多資訊#
- Service Discovery Example
- Kubernetes Services
- DNS for Services and Pods
- Debug Services
- Using Source IP
- Create an External Load Balancer
- Kubernetes NodePort versus LoadBalancer versus Ingress?
- Ingress
- Kubernetes Ingress versus OpenShift Route