1. 程式人生 > 其它 >Kubernetes之Service介紹

Kubernetes之Service介紹

為什麼需要Service

在應用建立一個Nginx的Pod集合,由3個Pod組成,每個容器的埠埠號都是80;

  1. 編輯nginx-deployment.yaml;
apiVersion:apps/v1
kind:Deployment
metadata:
name:nginx-deployment
spec:
selector:
matchLabels:
app:backend
replicas:3
template:
metadata:
labels:
app:backend
spec:
containers:
-name:nginx
image:nginx:latest
resources:
limits:
memory:"128Mi"
cpu:"128m"
ports:
-containerPort:80
  1. 建立Deployment資源;
kubectlapply-fnginx-deployment.yaml
  1. 檢視Pod的IP;
kubectlgetpods-owide
  1. 通過IP訪問Pod;
curl10.100.1.92:80

這樣就會存在問題,由於Pod的生命是有限的,如果Pod重啟IP有可能會發生變化。如果我們的服務都是將Pod的IP地址寫死,Pod的IP發生變化以後,後端服務也將會不可用。當然我們可以通過手動更新如nginx的upstream配置來改變後端的服務IP。也可以通過Consul,ZooKeeper、etcd等工具,把我們的服務註冊到這些服務發現中心,然後讓這些工具動態的去更新Nginx的配置就可以了,我們完全不用去手工的操作了。

Kubernetes提供了Service物件,它定義了一組Pod的邏輯集合和一個用於訪問它們的策略,這個概念和微服務非常類似。一個Serivce下面包含的Pod集合一般是由Label Selector來決定的。這樣就可以不用去管後端的Pod如何變化,只需要指定Service的地址就可以了,這廝因為我們在中間添加了一層服務發現的中介軟體,Pod銷燬或者重啟後,把這個Pod的地址註冊到這個服務發現中心去。Service的這種抽象就可以幫我們達到這種解耦的目的。

Service原理初探

我們以前面建立的nginx為案例,通過建立一個Service來給3個Pod做負載:

  1. 建立Service方式有兩種,一種是通過kubectl expose命令來建立,另外一個種通過yaml檔案來建立,這裡我們採用yaml方式來建立,建立一個nginx-service.yaml檔案;
apiVersion:v1
kind:Service
metadata:
name:nginx-service
spec:
#定義後端pod標籤為app=backend
selector:
app:backend
ports:
#service埠號
-port:80
#pod的埠號
targetPort:80
  1. 建立Service物件;
kubectlapply-fnginx-service.yaml
  1. 檢視Service的IP地址;
kubectlgetsvc
  1. 通過ServiceIP和埠號訪問;
curl 10.96.165.211:80
  1. 檢視Service的Endpoint的列表;
kubectldescribesvcnginx-service

這裡我們可以初步看出來Service的服務發現離不開Endpoint物件,Endpoint是Kubernetes中的一個資源物件,儲存在Etcd中,用來記錄一個Service對應的所有Pod的訪問地址。Service配置Selector,Endpoint Controller才會自動建立對應的Endpoint物件;否則不會建立Endpoint物件。Endpoint Controller主要有以下作用:

  1. 負責生成和維護所有endpoint物件的控制器
  2. 負責監聽Service和對應Pod的變化,監聽到Service被刪除,則刪除和該Service同名的Endpoint物件;監聽到新的Service被建立,則根據新建Service資訊獲取相關Pod列表,然後建立對應Endpoint物件;監聽到Service被更新,則根據更新後的Service資訊獲取相關Pod列表,然後更新對應Endpoint物件;監聽到Pod事件,則更新對應的Service的Endpoint物件,將Pod IP記錄Eendpoint中;

Endpoint完成服務發現,真正從服務IP到後端Pod的負載均衡的實現則是由每個Node節點上的kube-proxy負責實現的,kube-proxy會監聽Service和Endpoints的更新並呼叫其代理模組在主機上重新整理路由轉發規則,從而實現動態跟新服務列表,整體訪問情況可以參考下圖,這是第一代kube-proxy實現方式,現在的實現方式有所調整,但是我覺得這個是最容易讓人明白的方式。

下圖是第二代或者第三代實現方式,實現方式:

Service負載均衡

Service路由轉發都是由 kube-proxy 元件來實現的,Service 僅以一種 ClusterIP 的形式存在,kube-proxy 將Service訪問請求轉發到後端的多個Pod的例項上,kube-proxy 的路由轉發規則是通過其後端的代理模組實現的,kube-proxy 的代理模組目前有三種實現方案:

userspace

在userspace(使用者空間代理)模式下,kube-proxy程序是一個真實的TCP/UDP代理,負責從Service到Pod的訪問流量的轉發,如下圖:

userspace模式下kube-proxy通過API Server的Watch介面實時跟蹤Service與Endpoint的變更資訊,來實現動態更新iptables規則;對每個Service它都為其所在Node節點開放一個埠,作為其服務代理埠;發往該埠的請求會採用一定的策略轉發給與該服務對應的後端Pod實體。kube-proxy同時會在本地節點設定 iptables 規則,這個規則用於捕獲通過Cluter IP和Port訪問Service請求,並將這些轉發到對應的埠上,如果採用DNS形式,前面還有DNS解析層。

userspace該模式下最大的問題是,Service的請求會先從使用者空間進入核心iptables,然後再回到使用者空間,最後在由kube-proxy完成後端Endpoints的選擇和代理工作,這樣需要使用者空間和核心態一直來回切換,這樣帶來的系統開銷會很大。

iptables

Kubernetes從1.2版本開始,將iptables作為kube-proxy的預設模式。iptables模式下的kube-proxy不再起到Proxy的作用,其核心功能:通過API Server的Watch介面實時跟蹤Service與Endpoint的變更資訊,並更新對應的iptables規則,Client的請求流量則通過iptables的NAT機制“直接路由”到目標Pod。

與第1代的userspace模式相比,iptables模式完全工作在核心態,不用再經過使用者態的kube-proxy中轉,因而效能更強。iptables模式雖然實現起來簡單,但存在無法避免的缺陷:在叢集中的Service和Pod大量增加以後,iptables中的規則會急速膨脹,導致效能顯著下降,在某些極端情況下甚至會出現規則丟失的情況,並且這種故障難以重現與排查。

IPVS

Kubernetes從1.8版本開始引入第3代的IPVS(IPVirtualServer)模式,IPVS在Kubernetes1.11中升級為GA穩定版。iptables與IPVS雖然都是基於Netfilter實現的,但因為定位不同,二者有著本質的差別:iptables是為防火牆而設計的;IPVS則專門用於高效能負載均衡,並使用更高效的資料結構(Hash表),允許幾乎無限的規模擴張。由此解決掉了iptables的弊病。

與iptables相比,IPVS擁有以下明顯優勢:

  1. 為大型叢集提供了更好的可擴充套件性和效能;

  2. 支援比iptables更復雜的複製均衡演算法(最小負載、最少連線、加權等);

  3. 支援伺服器健康檢查和連線重試等功能;

  4. 可以動態修改ipset的集合,即使iptables的規則正在使用這個集合;

由於IPVS無法提供包過濾、airpin-masqueradetricks(地址偽裝)、SNAT等功能,因此在某些場景(如NodePort的實現)下還要與iptables搭配使用。

在IPVS模式下,kube-proxy又做了重要的升級,即使用iptables的擴充套件ipset,而不是直接呼叫iptables來生成規則鏈。iptables規則鏈是一個線性的資料結構,ipset則引入了帶索引的資料結構,因此當規則很多時,也可以很高效地查詢和匹配。

會話保持機制

Service支援通過設定sessionAffinity實現基於客戶端的IP的會話保持,首次將某個客戶端來源的IP發起請求轉發到後端某個Pod上,之後從相同的客戶端IP發起的請求都被轉發到相同的Pod上,配置引數為spec.sessionAffinity,也可以設定會話保持的最長時間,在此之後重新設定訪問規則,通過配置spec.sessionAffinityConfig.clientIP.timeoutSeconds來實現,可以參考以下配置:

Service支援通過設定sessionAffinity實現基於客戶端的IP的會話保持,首次將某個客戶端來源的IP發起請求轉發到後端某個Pod上,之後從相同的客戶端IP發起的請求都被轉發到相同的Pod上,配置引數為spec.sessionAffinity,也可以設定會話保持的最長時間,在此之後重新設定訪問規則,通過配置spec.sessionAffinityConfig.clientIP.timeoutSeconds來實現,可以參考以下配置:

apiVersion:v1
kind:Service
metadata:
name:nginx-service
spec:
sessionAffinity:ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds:1000
#定義後端pod標籤為app=backend
selector:
app:backend
ports:
#service埠號
-port:80
#pod的埠號
targetPort:80

Service型別

Service支援的型別也就是Kubernetes 中將服務暴露的方式,預設有四種 ClusterIP、NodePort、LoadBalancer、ExternelName,下面會詳細介紹每種型別Service的使用場景。

ClusterIP

ClusterIP型別的Service是Kubernetes叢集預設的服務暴露方式,它只能用於叢集內部通訊,可以被各 Pod 訪問,也可以手動指定ClusterIP,不過要確保該IP在Kubernetes叢集設定ClusterIP的範圍內部,並且沒有被其他Service使用,整個訪問流程如下:

叢集內部整體結構可參考以下模型:

NodePort

對於我們來說,不全是叢集內訪問,也需要叢集外業務訪問。那麼ClusterIP就滿足不了了。NodePort是一種對外提供訪問的方式,NodePort型別的Service可以讓Kubemetes叢集每個節點上保留一個相同的埠,外部訪問連線首先訪問節點IP:Port,然後將這些連線轉發給服務對應的Pod。整體的訪問流程:

叢集內部整體結構可參考以下模型:

LoadBalancer

LoadBalancer型別的Service其實是NodePort型別Service的擴充套件,通過一個特定的LoadBalancer訪問Service,這個LoadBalancer將請求轉發到節點的NodePort,可以理解為埠的Nginx負載均衡器。

LoadBalancer本身不是屬於Kubernetes的元件,這部分通常是由具體廠商(雲服務提供商)提供,不同廠商的Kubernetes叢集與LoadBalancer的對接實現各不相同,被提供的負載均衡器的資訊將會通過Service的status.loadBalancer欄位被髮布出去。

整體結構可參考以下模型:

ExternalName

ExternalName型別的服務用於將叢集外的服務定義為Kubernetes叢集的Service,並且通過externalName欄位指定外部服務的地址,可以是域名也可以是IP格式。叢集內部的客戶端可以通過這個Service訪問外部服務,這種型別的服務沒有後端Pod,因此也不需要設定Label Selector。

整體結構可參考以下模型: