為什么需要 service
在 kubernetes 中,當創建帶有多個副本的 deployment 時,kubernetes 會創建出多個 pod,此時即一個服務后端有多個容器,那么在 kubernetes 中負載均衡怎么做,容器漂移后 ip 也會發生變化,如何做服務發現以及會話保持?這就是 service 的作用,service 是一組具有相同 label pod 集合的抽象,集群內外的各個服務可以通過 service 進行互相通信,當創建一個 service 對象時也會對應創建一個 endpoint 對象,endpoint 是用來做容器發現的,service 只是將多個 pod 進行關聯,實際的路由轉發都是由 kubernetes 中的 kube-proxy 組件來實現,因此,service 必須結合 kube-proxy 使用,kube-proxy 組件可以運行在 kubernetes 集群中的每一個節點上也可以只運行在單獨的幾個節點上,其會根據 service 和 endpoints 的變動來改變節點上 iptables 或者 ipvs 中保存的路由規則。
service 的工作原理
endpoints controller 是負責生成和維護所有 endpoints 對象的控制器,監聽 service 和對應 pod 的變化,更新對應 service 的 endpoints 對象。當用戶創建 service 后 endpoints controller 會監聽 pod 的狀態,當 pod 處于 running 且準備就緒時,endpoints controller 會將 pod ip 記錄到 endpoints 對象中,因此,service 的容器發現是通過 endpoints 來實現的。而 kube-proxy 會監聽 service 和 endpoints 的更新并調用其代理模塊在主機上刷新路由轉發規則。
service 的負載均衡
上文已經提到 service 實際的路由轉發都是由 kube-proxy 組件來實現的,service 僅以一種 VIP(ClusterIP) 的形式存在,kube-proxy 主要實現了集群內部從 pod 到 service 和集群外部從 nodePort 到 service 的訪問,kube-proxy 的路由轉發規則是通過其后端的代理模塊實現的,kube-proxy 的代理模塊目前有四種實現方案,userspace、iptables、ipvs、kernelspace,其發展歷程如下所示:
- kubernetes v1.0:services 僅是一個“4層”代理,代理模塊只有 userspace
- kubernetes v1.1:Ingress API 出現,其代理“7層”服務,并且增加了 iptables 代理模塊
- kubernetes v1.2:iptables 成為默認代理模式
- kubernetes v1.8:引入 ipvs 代理模塊
- kubernetes v1.9:ipvs 代理模塊成為 beta 版本
- kubernetes v1.11:ipvs 代理模式 GA
在每種模式下都有自己的負載均衡策略,下文會詳解介紹。
userspace 模式
在 userspace 模式下,訪問服務的請求到達節點后首先進入內核 iptables,然后回到用戶空間,由 kube-proxy 轉發到后端的 pod,這樣流量從用戶空間進出內核帶來的性能損耗是不可接受的,所以也就有了 iptables 模式。
為什么 userspace 模式要建立 iptables 規則,因為 kube-proxy 監聽的端口在用戶空間,這個端口不是服務的訪問端口也不是服務的 nodePort,因此需要一層 iptables 把訪問服務的連接重定向給 kube-proxy 服務。
iptables 模式
iptables 模式是目前默認的代理方式,基于 netfilter 實現。當客戶端請求 service 的 ClusterIP 時,根據 iptables 規則路由到各 pod 上,iptables 使用 DNAT 來完成轉發,其采用了隨機數實現負載均衡。
iptables 模式與 userspace 模式最大的區別在于,iptables 模塊使用 DNAT 模塊實現了 service 入口地址到 pod 實際地址的轉換,免去了一次內核態到用戶態的切換,另一個與 userspace 代理模式不同的是,如果 iptables 代理最初選擇的那個 pod 沒有響應,它不會自動重試其他 pod。
iptables 模式最主要的問題是在 service 數量大的時候會產生太多的 iptables 規則,使用非增量式更新會引入一定的時延,大規模情況下有明顯的性能問題。
ipvs 模式
當集群規模比較大時,iptables 規則刷新會非常慢,難以支持大規模集群,因其底層路由表的實現是鏈表,對路由規則的增刪改查都要涉及遍歷一次鏈表,ipvs 的問世正是解決此問題的,ipvs 是 LVS 的負載均衡模塊,與 iptables 比較像的是,ipvs 的實現雖然也基于 netfilter 的鉤子函數,但是它卻使用哈希表作為底層的數據結構并且工作在內核態,也就是說 ipvs 在重定向流量和同步代理規則有著更好的性能,幾乎允許無限的規模擴張。
ipvs 支持三種負載均衡模式:DR模式(Direct Routing)、NAT 模式(Network Address Translation)、Tunneling(也稱 ipip 模式)。三種模式中只有 NAT 支持端口映射,所以 ipvs 使用 NAT 模式。linux 內核原生的 ipvs 只支持 DNAT,當在數據包過濾,SNAT 和支持 NodePort 類型的服務這幾個場景中ipvs 還是會使用 iptables。
此外,ipvs 也支持更多的負載均衡算法,例如:
- rr:round-robin/輪詢
- lc:least connection/最少連接
- dh:destination hashing/目標哈希
- sh:source hashing/源哈希
- sed:shortest expected delay/預計延遲時間最短
- nq:never queue/從不排隊
userspace、iptables、ipvs 三種模式中默認的負載均衡策略都是通過 round-robin 算法來選擇后端 pod 的,在 service 中可以通過設置 service.spec.sessionAffinity
的值實現基于客戶端 ip 的會話親和性,service.spec.sessionAffinity
的值默認為"None",可以設置為 "ClientIP",此外也可以使用 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds
設置會話保持時間。kernelspace 主要是在 windows 下使用的,本文暫且不談。
service 的類型
service 支持的類型也就是 kubernetes 中服務暴露的方式,默認有四種 ClusterIP、NodePort、LoadBalancer、ExternelName,此外還有 Ingress,下面會詳細介紹每種類型 service 的具體使用場景。
ClusterIP
ClusterIP 類型的 service 是 kubernetes 集群默認的服務暴露方式,它只能用于集群內部通信,可以被各 pod 訪問,其訪問方式為:
pod ---> ClusterIP:ServicePort --> (iptables)DNAT --> PodIP:containePort
ClusterIP Service 類型的結構如下圖所示:
NodePort
如果你想要在集群外訪問集群內部的服務,可以使用這種類型的 service,NodePort 類型的 service 會在集群內部署了 kube-proxy 的節點打開一個指定的端口,之后所有的流量直接發送到這個端口,然后會被轉發到 service 后端真實的服務進行訪問。Nodeport 構建在 ClusterIP 上,其訪問鏈路如下所示:
client ---> NodeIP:NodePort ---> ClusterIP:ServicePort ---> (iptables)DNAT ---> PodIP:containePort
其對應具體的 iptables 規則會在后文進行講解。
NodePort service 類型的結構如下圖所示:
LoadBalancer
LoadBalancer 類型的 service 通常和云廠商的 LB 結合一起使用,用于將集群內部的服務暴露到外網,云廠商的 LoadBalancer 會給用戶分配一個 IP,之后通過該 IP 的流量會轉發到你的 service 上。
LoadBalancer service 類型的結構如下圖所示:
ExternelName
通過 CNAME 將 service 與 externalName 的值(比如:foo.bar.example.com)映射起來,這種方式用的比較少。
Ingress
Ingress 其實不是 service 的一個類型,但是它可以作用于多個 service,被稱為 service 的 service,作為集群內部服務的入口,Ingress 作用在七層,可以根據不同的 url,將請求轉發到不同的 service 上。
Ingress 的結構如下圖所示:
service 的服務發現
雖然 service 的 endpoints 解決了容器發現問題,但不提前知道 service 的 Cluster IP,怎么發現 service 服務呢?service 當前支持兩種類型的服務發現機制,一種是通過環境變量,另一種是通過 DNS。在這兩種方案中,建議使用后者。
環境變量
當一個 pod 創建完成之后,kubelet 會在該 pod 中注冊該集群已經創建的所有 service 相關的環境變量,但是需要注意的是,在 service 創建之前的所有 pod 是不會注冊該環境變量的,所以在平時使用時,建議通過 DNS 的方式進行 service 之間的服務發現。
DNS
可以在集群中部署 CoreDNS 服務(舊版本的 kubernetes 群使用的是 kubeDNS), 來達到集群內部的 pod 通過DNS 的方式進行集群內部各個服務之間的通訊。
當前 kubernetes 集群默認使用 CoreDNS 作為默認的 DNS 服務,主要原因是 CoreDNS 是基于 Plugin 的方式進行擴展的,簡單,靈活,并且不完全被Kubernetes所捆綁。
Service 的使用
ClusterIP 方式
apiVersion: v1
kind: Service
metadata:
name: my-nginx
spec:
clusterIP: 10.105.146.177
ports:
- port: 80
protocol: TCP
targetPort: 8080
selector:
app: my-nginx
sessionAffinity: None
type: ClusterIP
NodePort 方式
apiVersion: v1
kind: Service
metadata:
name: my-nginx
spec:
ports:
- nodePort: 30090
port: 80
protocol: TCP
targetPort: 8080
selector:
app: my-nginx
sessionAffinity: None
type: NodePort
其中 nodeport
字段表示通過 nodeport 方式訪問的端口,port
表示通過 service 方式訪問的端口,targetPort
表示 container port。
Headless service(就是沒有 Cluster IP 的 service )
當不需要負載均衡以及單獨的 ClusterIP 時,可以通過指定 spec.clusterIP
的值為 None
來創建 Headless service,它會給一個集群內部的每個成員提供一個唯一的 DNS 域名來作為每個成員的網絡標識,集群內部成員之間使用域名通信。
apiVersion: v1
kind: Service
metadata:
name: my-nginx
spec:
clusterIP: None
ports:
- nodePort: 30090
port: 80
protocol: TCP
targetPort: 8080
selector:
app: my-nginx
總結
本文主要講了 kubernetes 中 service 的原理、實現以及使用方式,service 目前主要有 5 種服務暴露方式,service 的容器發現是通過 endpoints 來實現的,其服務發現主要是通過 DNS 實現的,其負載均衡以及流量轉發是通過 kube-proxy 實現的。在后面的文章我會繼續介紹 kube-proxy 的設計及實現。
參考: