Kubernetes Service詳解:實現服務發現與負載均衡

1. Service概念引入
k8s之部署Deployment章節我們介紹RS以及Deployment,Deployment提供了pod的管理方式,以及通過副本控制器RC保證集群中pod的數量保持為指定數量。同時Deployment還提供了相關升級、回滾、更新速度、灰度發布等功能。那么pod之間怎么進行訪問呢?在之前我們提過k8s之間的node網絡是互通的,同時它里面的pod網絡也是互通的,相關結構如下圖所示(暫時可以不需要理解里面的service,看得懂通過curl ip即可):
在集群中,pod可以通過指定podB的ip以及端口進行訪問。比如說,應用A訪問應用B,我們可以配置應用A中請求地址便可以進行訪問,但是如果pod掛了呢?通過上一章節的介紹,我們知道pod掛了之后,會重新啟動一個新的pod,但問題是ip也變了,這樣便沒法訪問了。這時候,肯定會有人說,Java boy不懼怕任何事情,俺有nacos!!nacos可以進行自動負載均衡以及服務發現,不需要配置ip,只需要配置服務名。是的,如果是使用nacos,集群內部的訪問便變得簡單了(其實nacos的作用跟今天所介紹的service很相似),但問題又來了,如果別人的應用沒有注冊到nacos上面去呢?萬一我是一個phper呢?又或者說,我集群內部的某個pod需要對外提供服務呢?這時候,便需要k8s中的Service來幫忙了。
2. 什么是Service
Kubernetes 中 Service 是 將運行在一個或一組 ?Pod? 上的網絡應用程序公開為網絡服務的方法?[注]。
我們可以這樣理解:Service 是 Kubernetes 中定義的一組 Pod 的穩定網絡入口(抽象),用于實現服務發現與負載均衡。 在我們部署應用的時候,為了服務的高可用性,一般來說,我們會將應用X部署在多個pod上,例如podA,podB,podC,他們各自有自己的ip地址。k8s的service將這三個pod給包裝起來了,包裝為了一個服務(service),其他的應用需要訪問應用X的時候,不再需要通過通過ip地址來訪問,而是直接通過服務名來訪問。例如,這個服務名叫做my-svc.my-ns.svc.cluster.local?,那么其他應用直接通過http://my-svc.my-ns.svc.cluster.local?便可以訪問應用X了。這是因為當你請求這個域名的時候,k8會為你自動轉發到對應的pod上去。
通過上面可以解釋我們可以知道,k8s的service主要有如下作用:
- ?解耦服務消費者與提供者:客戶端只需訪問 Service 名稱,無需知道后端 Pod IP(Pod 是臨時的,IP 會變);
- ?自動負載均衡:將流量分發到所有健康的后端 Pod;
- 提供穩定的網絡端點:即使 Pod 重建、擴縮容,Service 的 ClusterIP 和 DNS 名稱保持不變(這個之后解釋)。
3. Service的分類
根據Service的作用不同,可以分為如下的4種Service。
3.1 ClusterIP
ClusterIP的作用主要是在集群內部暴露服務,僅集群內可訪問。舉個列子,我用python寫了一個算法服務,這個算法服務提供了一個http請求接口,然后我java應用想要調用這個算法服務,我便可以將這個算法服務包裝為一個ClusterIP類型的Service,專門提供給java應用調用。那么,他的原理是怎樣的呢?
如下的代碼,便是構建一個ClusterIP類型的service:
apiVersion: v1
kind: Service
metadata:
name: backend-svc
spec:
type: ClusterIP
selector:
app: backend # 匹配的pod
ports:
- port: 80
targetPort: 8080
其中port=80代表這個service對外暴露的端口,也就是另一個pod需要訪問backend這個后端pod服務,需要通過http://backend-svc:80來進行訪問。targetPort=8080,代表service會將流量轉移到后端pod的哪個端口。
ClusterIP 類型的 Service 會被分配一個集群內部的專有且唯一的虛擬 IP 地址(由 Kubernetes 的控制平面從 --service-cluster-ip-range? 指定的 CIDR 范圍中分配)。這個 IP 不是真實網卡上的 IP,而是一個“虛擬 IP(ClusterIP)”。它只在 Kubernetes 集群內部可達,外部無法直接訪問。然后k8s內置的dns服務會為該service創建一個dns記錄,將例子中的backend-svc?解析為這個虛擬ip。這樣集群中的pod都會在訪問backend-svc?這個域名的時候就會自動解析到這個ClusterIP中。同時k8s的控制平面也會創建一個同名的Endpoints 對象,記錄對應Service當前實際可用的后端pod IP地址和端口列表。

在前面的知識中,我們知道,kube-proxy運行在每一個Node節點中,監聽Service和Endpoints的變化。當他發現有一個clusterIP service變化的時候,就會用小本本記錄下來,然后生成相關的ip映射規則(建立cluster IP和pod IP的映射規則)。這樣,當節點中的某個pod訪問Service的時候,節點中的Kube-Proxy就可以根據ip映射規則,將流量轉發到對應的服務上去。當然,一個cluster IP會對應多個pod IP,具體訪問哪一個,就根據kube-proxy設置的算法來進行執行。
@startuml
title Kubernetes Service 數據流轉:Service → Endpoints → kube-proxy → Pod
actor "客戶端 Pod" as client
participant "CoreDNS" as dns
participant "API Server" as apiserver
participant "Endpoints Controller" as controller
participant "kube-proxy" as kubeproxy
participant "后端 Pod" as pod
client -> dns : 1. 解析 DNS 名稱\nmy-svc.ns.svc.cluster.local
dns --> client : 2. 返回 ClusterIP\n(例如: 10.96.100.10)
client -> kubeproxy : 3. 訪問 10.96.100.10:80\n(通過節點網絡棧)
activate kubeproxy
note right of kubeproxy
kube-proxy 維護 iptables/IPVS 規則,
將 ClusterIP 映射到后端 Pod IP,
規則數據來源于 Endpoints。
end note
kubeproxy -> pod : 4. 轉發到 Pod IP:端口\n(例如: 10.244.1.5:80)
activate pod
pod --> kubeproxy : 5. 返回響應
deactivate pod
kubeproxy --> client : 6. 返回響應給客戶端
deactivate kubeproxy
== 后臺:Endpoints 同步流程 ==
pod -> apiserver : 7. Pod 創建成功\n(標簽: app=web)
apiserver -> controller : 8. 監聽事件:新 Pod 匹配\nService 的標簽選擇器
controller -> apiserver : 9. 創建/更新 Endpoints\n(my-svc,包含 Pod IP)
apiserver -> kubeproxy : 10. 推送 Endpoints 變更事件
kubeproxy -> kubeproxy : 11. 更新本地 iptables/IPVS 規則
@enduml
?
3.2 NodePort
在每個節點的 IP 上開放一個固定端口,外部可通過 <NodeIP>:<NodePort>? 訪問服務,NodePort 范圍默認為 30000-32767?。什么意思呢?在Cluster IP類型的service中,集群內部訪問當然沒問題,但是如果是集群外部呢?集群外部如何訪問集群內部的服務呢?這時候我們就需要創建一個NodePort類型的service了。當創建一個 type: NodePort 的 Service 時,Kubernetes 實際上做了兩件事:
- 創建一個ClusterIP的Service(比如 ClusterIP是
10.96.123.45) - 在每個節點上監聽一個端口(比如
31234)。
當外部流量通過 <NodeIP>:31234? 進入節點時,kube-proxy 會將該流量轉發到 Service 的 ClusterIP,然后再由 ClusterIP 的規則轉發到后端 Pod。
外部請求 → NodeIP:NodePort → (kube-proxy 轉發) → ClusterIP → 后端 Pod
這個時候,我們再看k8s官網的這句話,應該就能理解了吧。
通過每個節點上的 IP 和靜態端口(
NodePort?)公開 Service。 為了讓 Service 可通過節點端口訪問,Kubernetes 會為 Service 配置集群 IP 地址, 相當于你請求了type: ClusterIP的 Service。
3.3 LoadBalancer
前面的NodePort類型的service解決了集群外部訪問集群內部服務的問題,那么又有一個問題來了,我要是公網想訪問集群內部的服務呢?在k8s集群中,node的節點ip都是分配的內網ip,我們肯定是無法在公網上使用node ip來訪問對應的服務。這時候就需要借助LoadBalancer Service了。
?LoadBalancer? 是 k8s中最“面向外部”的 Service 類型之一,它建立在 ?NodePort?? 和 ?ClusterIP?? 之上,專為云環境設計,用于自動創建一個外部負載均衡器,將流量從互聯網直接導入你的服務。也就是說,當創建一個LoadBalancer的service時:
-
Kubernetes 會:
- 自動分配一個 ?ClusterIP(內部使用)
- 自動分配一個 ?NodePort(每個節點開放該端口)
-
云控制器管理器(cloud-controller-manager) 會檢測到這個 Service,?自動向云平臺 API 請求創建一個外部負載均衡器。
-
該負載均衡器會被配置為:
- ?前端?:分配一個公網 IP(或 DNS 名稱)
- 后端:將流量轉發到所有節點的 NodePort
@startuml
skinparam componentStyle rectangle
skinparam defaultTextAlignment center
package "Kubernetes 集群" {
[節點 1\n(192.168.1.10)] as node1
[節點 2\n(192.168.1.11)] as node2
[節點 3\n(192.168.1.12)] as node3
package "Pod 實例" {
[Pod A\n(app: web)] as podA
[Pod B\n(app: web)] as podB
}
}
cloud "外部網絡" {
[用戶 / 客戶端] as client
[云負載均衡器\n(公網IP: 203.0.113.10)] as lb
}
node "Service 抽象層" {
[ClusterIP\n10.96.45.67:80] as clusterip
note right of clusterip
集群內部虛擬 IP
僅可在集群內訪問
end note
}
' 連接關系
client --> lb : 訪問服務\n(203.0.113.10:80)
lb --> node1 : 轉發流量\n到 NodePort 31234
lb --> node2 : 轉發流量\n到 NodePort 31234
lb --> node3 : 轉發流量\n到 NodePort 31234
node1 --> clusterip : kube-proxy\n將流量 DNAT 到 ClusterIP
node2 --> clusterip : kube-proxy\n將流量 DNAT 到 ClusterIP
node3 --> clusterip : kube-proxy\n將流量 DNAT 到 ClusterIP
clusterip --> podA : 通過 iptables/IPVS\n負載均衡到 Pod
clusterip --> podB : 通過 iptables/IPVS\n負載均衡到 Pod
' Service 類型說明
note top of lb
<b>LoadBalancer 類型</b>
? 云平臺自動創建外部負載均衡器
? 底層依賴 NodePort
end note
note bottom of node1
<b>NodePort 類型</b>
? 在每個節點開放端口(30000-32767)
? 可通過 <節點IP>:<NodePort> 訪問
end note
note right of clusterip
<b>ClusterIP 類型</b>
? 默認 Service 類型
? 僅限集群內部訪問
end note
@enduml
3.4 ExternalName
前面介紹的幾種Service都是將自己暴露出去,將pod的服務以更加便捷形式提供給外界使用。那么反過來呢,如何讓外界服務更加便捷給內部使用呢?那么便是ExternalName Service提供的功能了。
ExternalName Service 會在集群內部創建一個 DNS 別名(CNAME),指向你指定的外部服務地址(如 api.example.com),讓 Pod 可以像訪問內部服務一樣訪問外部服務。本質上來說,就是給外部服務取了一個DNS層面的別名。

浙公網安備 33010602011771號