1. 程式人生 > 其它 >Web自動化之元素定位

Web自動化之元素定位

運行於Pod中的容器化應用絕大多數是服務類的守護程序,例如envoy和demoapp等,它們受控於控制器資源物件,在自願或非自願中斷後只能由重構的、具有同樣功能的新Pod物件所取代,屬非可再生類元件。在Kubernetes應用編排的動態、彈性管理模型下,Service資源用於為此類Pod物件提供一個固定、統一的訪問介面及負載均衡能力,並支援新一代DNS系統的服務發現功能,解決了客戶端發現並訪問容器化應用的難題。然而,Service物件的IP地址都僅在Kubernetes叢集內可達,它們無法接入叢集外部的訪問流量。在解決此類問題時,除了可以在單一節點上做埠(hostPort)暴露及讓Pod資源共享使用工作節點的網路名稱空間(hostNetwork)之外,更推薦使用者使用NodePort或LoadBalancer型別的Service資源,或者是有七層負載均衡能力的Ingress資源。

一、Service資源及其實現模型

1、service的資源概述

Service是Kubernetes的核心資源型別之一,通常被看作微服務的一種實現。它事實上是一種抽象:通過規則定義出由多個Pod物件組合而成的邏輯集合,以及訪問這組Pod的策略。Service關聯Pod資源的規則要藉助標籤選擇器完成。

作為一款容器編排系統,託管在Kubernetes之上、以Pod形式執行的應用程序的生命週期通常受控於Deployment或StatefulSet一類的控制器,由於節點故障或驅離等原因導致Pod物件中斷後,會由控制器自動建立的新物件所取代,而擴縮容或更新操作更是會帶來Pod物件的群體變動。因為編排系統需要確保服務在編排操作導致的應用Pod動態變動的過程中始終可訪問,所以Kubernetes提出了滿足這一關鍵需求的解決方案,即核心資源型別——Service。

app1的Pod作為客戶端訪問app2相關的Pod應用時,IP的變動或應用規模的縮減會導致客戶端訪問錯誤,而Pod規模的擴容又會使客戶端無法有效使用新增的Pod物件,影響達成規模擴充套件的目的。

Service資源基於標籤選擇器把篩選出的一組Pod物件定義成一個邏輯組合,並通過自己的IP地址和埠將請求分發給該組內的Pod物件,如下圖所示。Service向客戶端隱藏了真實的處理使用者請求的Pod資源,使得客戶端的請求看上去是由Service直接處理並進行響應。

Service物件的IP地址(可稱為ClusterIP或ServiceIP)是虛擬IP地址,由Kubernetes系統在Service物件建立時在專用網路(Service Network)地址中自動分配或由使用者手動指定,並且在Service物件的生命週期中保持不變。Service基於埠過濾到達其IP地址的客戶端請求,並根據定義將請求轉發至其後端的Pod物件的相應埠之上,因此這種代理機制也稱為“埠代理”或四層代理,工作於TCP/IP協議棧的傳輸層。Service物件會通過API Server持續監視(watch)標籤選擇器匹配到的後端Pod物件,並實時跟蹤這些Pod物件的變動情況,例如IP地址變動以及Pod物件的增加或刪除等。不過,Service並不直接連線至Pod物件,它們之間還有一箇中間層——Endpoints資源物件,該資源物件是一個由IP地址和埠組成的列表,這些IP地址和埠則來自由Service的標籤選擇器匹配到的Pod物件。這也是很多場景中會使用“Service的後端端點”這一術語的原因。預設情況下,建立Service資源物件時,其關聯的Endpoints物件會被自動建立。

本質上來講,一個Service物件對應於工作節點核心之中的一組iptables或/和ipvs規則,這些規則能夠將到達Service物件的ClusterIP的流量排程轉發至相應Endpoint物件指向的IP地址和埠之上。核心中的iptables或ipvs規則的作用域僅為其所在工作節點的一個主機,因而生效於叢集範圍內的Service物件就需要在每個工作節點上都生成相關規則,從而確保任一節點上發往該Service物件請求的流量都能被正確轉發。

每個工作節點的kube-proxy元件通過API Server持續監控著各Service及其關聯的Pod物件,並將Service物件的建立或變動實時反映至當前工作節點上相應的iptables或ipvs規則上。客戶端、Service及Pod物件的關係如下圖所示。

提示
Netfilter是Linux核心中用於管理網路報文的框架,它具有網路地址轉換(NAT)、報文改動和報文過濾等防火牆功能,使用者可藉助使用者空間的iptables等工具按需自由定製規則使用其各項功能。
ipvs是藉助於Netfilter實現的網路請求報文排程框架,支援rr、wrr、lc、wlc、sh、sed和nq等10餘種排程演算法,使用者空間的命令列工具是ipvsadm,用於管理工作於ipvs之上的排程規則。

Service物件的ClusterIP事實上是用於生成iptables或ipvs規則時使用的IP地址,它僅用於實現Kubernetes叢集網路內部通訊,且僅能夠以規則中定義的轉發服務的請求作為目標地址予以響應,這也是它之所以被稱作虛擬IP的原因之一。kube-proxy把請求代理至相應端點的方式有3種:userspace、iptables和ipvs。

2.1 userpace代理模式

userspace是指Linux作業系統的使用者空間。在這種模型中,kube-proxy負責跟蹤API Server上Service和Endpoints物件的變動(建立或移除),並據此調整Service資源的定義。對於每個Service物件,它會隨機開啟一個本地埠(運行於使用者空間的kube-proxy程序負責監聽),任何到達此代理埠的連線請求都將被代理至當前Service資源後端的各Pod物件,至於哪個Pod物件會被選中則取決於當前Service資源的排程方式,預設排程演算法是輪詢(round-robin)。userspace代理模型工作邏輯如圖所示。另外,此類Service物件還會建立iptables規則以捕獲任何到達ClusterIP和埠的流量。在Kubernetes 1.1版本之前,userspace是預設的代理模型。

配的目標後端Pod物件。因請求報文在核心空間和使用者空間來回轉發,所以必然導致模型效率不高。

2.2 iptables代理模式

建Service物件的操作會觸發叢集中的每個kube-proxy並將其轉換為定義在所屬節點上的iptables規則,用於轉發工作介面接收到的、與此Service資源ClusterIP和埠相關的流量。客戶端發來請求將直接由相關的iptables規則進行目標地址轉換(DNAT)後根據演算法排程並轉發至叢集內的Pod物件之上,而無須再經由kube-proxy程序進行處理,因而稱為iptables代理模型,如圖所示。對於每個Endpoints物件,Service資源會為其建立iptables規則並指向其iptables地址和埠,而流量轉發到多個Endpoint物件之上的預設排程機制是隨機演算法。iptables代理模型由Kubernetes v1.1版本引入,並於v1.2版本成為預設的型別。

在iptables代理模型中,Service的服務發現和負載均衡功能都使用iptables規則實現,而無須將流量在使用者空間和核心空間來回切換,因此更為高效和可靠,但是效能一般,而且受規模影響較大,僅適用於少量Service規模的叢集。

2.3 ipvs代理模式

Kubernetes自v1.9版本起引入ipvs代理模型,且自v1.11版本起成為預設設定。在此種模型中,kube-proxy跟蹤API Server上Service和Endpoints物件的變動,並據此來呼叫netlink介面建立或變更ipvs(NAT)規則,如圖所示。它與iptables規則的不同之處僅在於客戶端請求流量的排程功能由ipvs實現,餘下的其他功能仍由iptables完成。

ipvs代理模型中Service的服務發現和負載均衡功能均基於核心中的ipvs規則實現。類似於iptables,ipvs也構建於核心中的netfilter之上,但它使用hash表作為底層資料結構且工作於核心空間,因此具有流量轉發速度快、規則同步效能好的特性,適用於存在大量Service資源且對效能要求較高的場景。ipvs代理模型支援rr、lc、dh、sh、sed和nq等多種排程演算法。

3、service資源型別

無論哪一種代理模型,Service資源都可統一根據其工作邏輯分為ClusterIP、NodePort、LoadBalancer和ExternalName這4種類型。

(1)ClusterIP通過叢集內部IP地址暴露服務,ClusterIP地址僅在叢集內部可達,因而無法被叢集外部的客戶端訪問。此為預設的Service型別。

(2)NodePortNodePort型別是對ClusterIP型別Service資源的擴充套件,它支援通過特定的節點埠接入叢集外部的請求流量,並分發給後端的Server Pod處理和響應。因此,這種型別的Service既可以被叢集內部客戶端通過ClusterIP直接訪問,也可以通過套接字<NodeIP>:<NodePort>與叢集外部客戶端進行通訊,如圖所示。顯然,若叢集外部的請求報文首先到的節點並非Service排程的目標Server Pod所在的節點,該請求必然因需要額外的轉發過程(躍點)和更多的處理步驟而產生更多延遲。

叢集外部客戶端對NodePort發起的請求報文源地址並非叢集內部地址,而請求報文又可能被收到報文的節點(例如圖中的Y節點)轉發至叢集中的另一個節點(例如圖中的X節點)上的Pod物件(例如圖中的Server Pod 1),因此,為避免X節點直接將響應報文傳送給外部客戶端,Y節點需要先將收到的報文的源地址轉為請求報文的目標IP(自身的節點IP)後再進行後續處理過程。

(3)LoadBalancer這種型別的Service依賴於部署在IaaS雲端計算服務之上並且能夠呼叫其API介面建立軟體負載均衡器的Kubernetes叢集環境。LoadBalancer Service構建在NodePort型別的基礎上,通過雲服務商提供的軟負載均衡器將服務暴露到叢集外部,因此它也會具有NodePort和ClusterIP。簡言之,建立LoadBalancer型別的Service物件時會在叢集上建立一個NodePort型別的Service,並額外觸發Kubernetes呼叫底層的IaaS服務的API建立一個軟體負載均衡器,而叢集外部的請求流量會先路由至該負載均衡器,並由該負載均衡器排程至各節點上該Service物件的NodePort,如圖所示。該Service型別的優勢在於,它能夠把來自叢集外部客戶端的請求排程至所有節點(或部分節點)的NodePort之上,而不是讓客戶端自行決定連線哪個節點,也避免了因客戶端指定的節點故障而導致的服務不可用。

(4)ExternalName通過將Service對映至由externalName欄位的內容指定的主機名來暴露服務,此主機名需要被DNS服務解析至CNAME型別的記錄中。換言之,此種類型不是定義由Kubernetes叢集提供的服務,而是把叢集外部的某服務以DNS CNAME記錄的方式對映到叢集內,從而讓叢集內的Pod資源能夠訪問外部服務的一種實現方式,如圖所示。因此,這種型別的Service沒有ClusterIP和NodePort,沒有標籤選擇器用於選擇Pod資源,也不會有Endpoints存在。

總體來說,若需要將Service資源釋出至叢集外部,應該將其配置為NodePort或Load-Balancer型別,而若要把外部的服務釋出於叢集內部供Pod物件使用,則需要定義一個ExternalName型別的Service資源,只是這種型別的實現要依賴於v1.7及更高版本的Kubernetes。

二、應用service資源

Service是Kubernetes核心API群組(core)中的標準資源型別之一。Service資源配置規範中常用的欄位及意義如下所示。

apiVersion: v1
kind: Service
metadata:
  name: …
  namespace: …
spec:
  type <string>                 # Service型別,預設為ClusterIP(NodePort、ClusterIP、LoadBalancer、ExternalName)
  selector <map[string]string>  # 等值型別的標籤選擇器,內含“與”邏輯
  ports:                       # Service的埠物件列表
  - name <string>               # 埠名稱
    protocol <string>           # 協議,目前僅支援TCP、UDP和SCTP,預設為TCP
    port <integer>              # Service的埠號
    targetPort  <string>        # 後端目標程序的埠號或名稱,名稱需由Pod規範定義
    nodePort <integer>          # 節點埠號,僅適用於NodePort和LoadBalancer型別
  clusterIP  <string>           # Service的叢集IP,建議由系統自動分配
  externalTrafficPolicy  <string> # 外部流量策略處理方式,Local表示由當前節點處理,
                               # Cluster表示向叢集範圍內排程
  loadBalancerIP  <string>        # 外部負載均衡器使用的IP地址,僅適用於LoadBlancer
  externalName <string>           # 外部服務名稱,該名稱將作為Service的DNS CNAME值

不同Service型別所支援使用的配置欄位有著明顯的區別,具體使用時應該根據計劃使用的型別進行選擇。

1、應用ClusrerIP Service資源

建立Service物件的常用方法有兩種:一是利用此前曾使用過的kubectl create service命令建立,另一個則是利用資源配置清單建立。Service資源物件的期望狀態定義在spec欄位中,較為常用的內嵌欄位為selector和ports,用於定義標籤選擇器和服務埠。下面的配置清單是定義在services-clusterip-demo.yaml中的一個Service資源示例:

apiVersion: v1
kind: Service
metadata:
  name: demoapp-svc
  namespace: default
spec:
  selector:
    app: demoapp
  ports:
  - name: http  #埠名稱標識
    protocol: TCP #協議,支援TCP\UDP\SCTP,預設為TCP
    port: 80      #Service自身的埠號
    targetPort: 80  #目標埠號,即endpoint上定義的埠號

Service資源的spec.selector僅支援以對映(字典)格式定義的等值型別的標籤選擇器,例如上面示例中的app: demoapp。定義服務埠的欄位spec.ports的值則是一個物件列表,它主要定義Service物件自身的埠與目標後端埠的對映關係。我們可以將示例中的Service物件創建於叢集中,通過其詳細描述瞭解其特性,如下面的命令及結果所示。

root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl apply -f services-clusterip-demo.yaml 
service/demoapp-svc created

root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl get services -n default
NAME          TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
demoapp-svc   ClusterIP   10.68.106.128   <none>        80/TCP    50s
kubernetes    ClusterIP   10.68.0.1       <none>        443/TCP   3d13h


root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl describe services demoapp-svc -n default
Name:              demoapp-svc
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=demoapp
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.68.106.128
IPs:               10.68.106.128
Port:              http  80/TCP
TargetPort:        80/TCP
Endpoints:         <none>  #沒有與標籤app=demoapp匹配的pod物件
Session Affinity:  None
Events:            <none>

上面命令中的結果顯示,demoapp-svc預設設定為ClusterIP型別,並得到一個自動分配的IP地址10.68.106.128。建立Service物件的同時會建立一個與之同名且擁有相同標籤選擇器的Endpoint物件,若該標籤選擇器無法匹配到任何Pod物件的標籤,則Endpoint物件無任何可用端點資料,於是Service物件的Endpoints欄位值便成了<none>

Service物件自身只是iptables或ipvs規則,它並不能處理客戶端的服務請求,而是需要把請求報文通過目標地址轉換(DNAT)後轉發至後端某個Server Pod,這意味著沒有可用的後端端點的Service物件是無法響應客戶端任何服務請求的,如下面從叢集節點上發起的請求命令結果所示。

root@k8s-master01:/apps/k8s-yaml/srv-case# curl 10.96.97.89
curl: (28) Failed to connect to 10.96.97.89 port 80: Connection timed out

下面使用命令式命令手動建立一個與該Service物件具有相同標籤選擇器的Deployment物件demoapp,它預設會自動建立一個擁有標籤app: demoapp的Pod物件。

#新增一個有app=demoapp標籤的deployments
root@k8s-master01:/apps/k8s-yaml/srv-case# vim nginx-demoapp.yaml
apiVersion: apps/v1
kind: Deployment 
metadata:
  name: nginx-demoapp
  labels:
    app: demoapp
spec:
  replicas: 1
  selector:    
    matchLabels:
      app: demoapp
  template:    
    metadata:
      labels:
        app: demoapp 
    spec: 
      containers:
      - name: nginx
        image: harbor.ywx.net/k8s-baseimages/demoapp:v1.0
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 80
#執行該yaml檔案
root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl apply -f nginx-demoapp.yaml 
deployment.apps/nginx-demoapp created
root@k8s-master01:~# kubectl get pod -n default -o wide
NAME                             READY   STATUS    RESTARTS   AGE     IP               NODE             
nginx-demoapp-5b5cb85747-wnl7z   1/1     Running   0          6m53s   172.20.135.163   172.168.33.212   



#驗證pod是否有app=demoapp標籤
root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl get pods -n default -l app=demoapp
NAME                             READY   STATUS    RESTARTS   AGE
nginx-demoapp-5b5cb85747-wnl7z   1/1     Running   0          2m5s
#驗證service把帶有app=demoapp的pod新增到endpoints
root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl describe services demoapp-svc -n default
Name:              demoapp-svc
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=demoapp
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.68.106.128
IPs:               10.68.106.128
Port:              http  80/TCP
TargetPort:        80/TCP
Endpoints:         172.20.135.163:80  #帶有app=demoapp的pod已經新增到了endpoints
Session Affinity:  None
Events:            <none>

Service物件demoapp-svc通過API Server獲知這種匹配變動後,會立即建立一個以該Pod物件的IP和埠為列表項的名為demoapp-svc的Endpoints物件,而該Service物件詳細描述資訊中的Endpoint欄位便以此列表項為值,如下面的命令結果所示。

root@k8s-master01:~# kubectl get endpoints demoapp-svc 
NAME          ENDPOINTS           AGE
demoapp-svc   172.20.135.163:80   16m
      
root@k8s-master01:~# kubectl describe services demoapp-svc 
Name:              demoapp-svc
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=demoapp
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.68.106.128
IPs:               10.68.106.128
Port:              http  80/TCP
TargetPort:        80/TCP
Endpoints:         172.20.135.163:80
Session Affinity:  None
Events:            <none>

擴充套件Deployment物件demoapp的應用規模引起的變動也將立即反映到相關的Endpoint和Service物件之上,例如將deployments/demoapp物件的副本擴充套件至3個,再來驗證services/demoapp-svc的端點資訊,如下面的命令及結果所示。

root@k8s-master01:~# kubectl scale deployment nginx-demoapp --replicas=3
deployment.apps/nginx-demoapp scaled

root@k8s-master01:~# kubectl get pods -n default -o wide
NAME                             READY   STATUS    RESTARTS   AGE    IP               NODE             NOMINATED NODE   READINESS GATES
nginx-demoapp-54cc8d5bff-jls24   1/1     Running   0          49s    172.20.135.167   172.168.33.212   
nginx-demoapp-54cc8d5bff-qf9j8   1/1     Running   0          101s   172.20.85.250    172.168.33.210   
nginx-demoapp-54cc8d5bff-zgsmh   1/1     Running   0          49s    172.20.135.166   172.168.33.212   
        


root@k8s-master01:~# kubectl describe services demoapp-svc -n default
Name:              demoapp-svc
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=demoapp
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.68.106.128
IPs:               10.68.106.128
Port:              http  80/TCP
TargetPort:        80/TCP
Endpoints:         172.20.135.166:80,172.20.135.167:80,172.20.85.250:80
Session Affinity:  None
Events:            <none>

接下來可於叢集中的某節點上再次向服務物件demoapp-svc發起訪問請求以進行測試,多次的訪問請求還可評估負載均衡演算法的排程效果,如下面的命令及結果所示。

root@k8s-master01:~# while true; do curl -s 10.68.106.128/hostname; sleep 2;done
ServerName: nginx-demoapp-54cc8d5bff-jls24
ServerName: nginx-demoapp-54cc8d5bff-zgsmh
ServerName: nginx-demoapp-54cc8d5bff-qf9j8

kubeadm部署的Kubernetes叢集的Service代理模型預設為iptables,它使用隨機排程演算法,因此Service會把客戶端請求隨機排程至其關聯的某個後端Pod物件。請求取樣次數越多,其排程效果也越接近演算法的目標效果。

2、應用NodePort Service資源

部署Kubernetes集群系統時會預留一個埠範圍,專用於分配給需要用到NodePort的Service物件,該埠範圍預設為30000~32767。與Cluster型別的Service資源的一個顯著不同之處在於,NodePort型別的Service資源需要顯式定義.spec.type欄位值為NodePort,必要時還可以手動指定具體的節點埠號。例如下面的配置清單(services-nodeport-demo.yaml)中定義的Service資源物件demoapp-nodeport-svc,它使用了NodePort型別,且人為指定了32223這個節點埠。

kind: Service
apiVersion: v1
metadata:
  name: demoapp-nodeport-svc
spec:
  type: NodePort
  selector:
    app: demoapp  #選擇帶有app=demoapp標籤的pod加入該svc的endpoints
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 32223

實踐中,並不鼓勵使用者自定義節點埠,除非能事先確定它不會與某個現存的Service資源產生衝突。無論如何,只要沒有特別需要,留給系統自動配置總是較好的選擇。將配置清單中定義的Service物件demoapp-nodeport-svc創建於叢集之上,以便通過詳細描述瞭解其狀態細節。

root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl apply -f service-nodeport-demo.yaml 
service/demoapp-nodeport-svc created

root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl get svc -n default
NAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
demoapp-nodeport-svc   NodePort    10.68.76.207    <none>        80:32223/TCP   27s
demoapp-svc            ClusterIP   10.68.106.128   <none>        80/TCP         31m
kubernetes             ClusterIP   10.68.0.1       <none>        443/TCP        3d14h


root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl describe svc demoapp-nodeport-svc 
Name:                     demoapp-nodeport-svc
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=demoapp
Type:                     NodePort
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.68.76.207
IPs:                      10.68.76.207
Port:                     http  80/TCP
TargetPort:               80/TCP
NodePort:                 http  32223/TCP
Endpoints:                172.20.135.166:80,172.20.135.167:80,172.20.85.250:80
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

#注意:nodeport模式會在所有節點上開啟32223埠

命令結果顯示,該Service物件用於排程叢集外部流量時使用預設的Cluster策略,該策略優先考慮負載均衡效果,哪怕目標Pod物件位於另外的節點之上而帶來額外的網路躍點,因而針對該NodePort的請求將會被分散排程至該Serivce物件關聯的所有端點之上。可以在叢集外的某節點上對任一工作節點的NodePort埠發起HTTP請求以進行測試。以節點k8s-node03為例,我們以如下命令向它的IP地址172.168.32.206的32223埠發起多次請求。

root@k8s-master01:/apps/k8s-yaml/srv-case# while true; do curl -s 172.168.33.212:32223; sleep 2;done
iKubernetes demoapp v1.0 !! ClientIP: 172.20.135.128, ServerName: nginx-demoapp-54cc8d5bff-qf9j8, ServerIP: 172.20.85.250!
iKubernetes demoapp v1.0 !! ClientIP: 172.168.33.212, ServerName: nginx-demoapp-54cc8d5bff-jls24, ServerIP: 172.20.135.167!
iKubernetes demoapp v1.0 !! ClientIP: 172.168.33.212, ServerName: nginx-demoapp-54cc8d5bff-zgsmh, ServerIP: 172.20.135.166!

上面命令的結果顯示出外部客戶端的請求被排程至該Service物件的每一個後端Pod之上,而這些Pod物件可能會分散於叢集中的不同節點。命令結果還顯示,請求報文的客戶端IP地址是最先接收到請求報文的節點上用於叢集內部通訊的IP地址,而非外部客戶端地址,這也能夠在Pod物件的應用訪問日誌中得到進一步驗證,如下所示。

root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl logs nginx-demoapp-54cc8d5bff-zgsmh
 * Running on http://0.0.0.0:80/ (Press CTRL+C to quit)
172.20.32.128 - - [03/Oct/2021 02:29:54] "GET /hostname HTTP/1.1" 200 -
172.168.33.212 - - [03/Oct/2021 02:37:26] "GET / HTTP/1.1" 200 -
172.168.33.212 - - [03/Oct/2021 02:37:32] "GET / HTTP/1.1" 200 -
#第一個報文172.20.32.128為叢集內部地址

叢集外部客戶端對NodePort發起的請求報文源地址並非叢集內部地址,而請求報文又可能被收到報文的節點轉發至叢集中的另一個節點上的Pod物件,因此,為避免X節點直接將響應報文傳送給外部客戶端,Y節點需要先將收到的報文的源地址轉為請求報文的目標IP(自身的節點IP)後再進行後續處理過程。這樣才能確保Server Pod的響應報文必須由最先接收到請求報文的節點進行響應,因此NodePort型別的Service物件會對請求報文同時進行源地址轉換(SNAT)和目標地址轉換(DNAT)操作。

另一個外部流量策略Local則僅會將流量排程至請求的目標節點本地執行的Pod物件之上,以減少網路躍點,降低網路延遲,但當請求報文指向的節點本地不存在目標Service相關的Pod物件時將直接丟棄該報文。下面先把demoapp-nodeport-svc的外部流量策略修改為Local,而後再進行訪問測試。簡單起見,這裡使用kubectl patch命令來修改Service物件的流量策略。

root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl patch services/demoapp-nodeport-svc -p '{"spec": {"externalTrafficPolicy": "Local"}}'  
service/demoapp-nodeport-svc patched
#或者直接修改yaml檔案中externalTrafficPolicy

root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl describe svc demoapp-nodeport-svc
Name:                     demoapp-nodeport-svc
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=demoapp
Type:                     NodePort
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.68.76.207
IPs:                      10.68.76.207
Port:                     http  80/TCP
TargetPort:               80/TCP
NodePort:                 http  32223/TCP
Endpoints:                172.20.135.166:80,172.20.135.167:80,172.20.85.250:80
Session Affinity:         None
External Traffic Policy:  Local  #本地有service相關的pod就直接轉發,沒有則直接丟棄報文
Events:                   <none>

-p選項中指定的補丁是一個JSON格式的配置清單片段,它引用了spec.externalTrafficPolicy欄位,併為其賦一個新的值。配置完成後,我們再次發起測試請求時會看到,請求都被排程給了目標節點本地執行的Pod物件。另外,Local策略下無須在叢集中轉發流量至其他節點,也就不用再對請求報文進行源地址轉換,Server Pod所看到的客戶端IP就是外部客戶端的真實地址。

root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl get pod -n default -o wide
NAME                             READY   STATUS    RESTARTS   AGE   IP               NODE             
nginx-demoapp-54cc8d5bff-jls24   1/1     Running   0          21m   172.20.135.167   172.168.33.212    
nginx-demoapp-54cc8d5bff-qf9j8   1/1     Running   0          22m   172.20.85.250    172.168.33.210    
nginx-demoapp-54cc8d5bff-zgsmh   1/1     Running   0          21m   172.20.135.166   172.168.33.212    

訪問:k8s-node03,在k8s-node03上有servicex相關的pod節點

root@k8s-master01:/apps/k8s-yaml/srv-case# while true; do curl -s 172.168.33.212:32223; sleep 2 ;done
iKubernetes demoapp v1.0 !! ClientIP: 172.168.33.207, ServerName: nginx-demoapp-54cc8d5bff-jls24, ServerIP: 172.20.135.167!
iKubernetes demoapp v1.0 !! ClientIP: 172.168.33.207, ServerName: nginx-demoapp-54cc8d5bff-zgsmh, ServerIP: 172.20.135.166!
iKubernetes demoapp v1.0 !! ClientIP: 172.168.33.207, ServerName: nginx-demoapp-54cc8d5bff-jls24, ServerIP: 172.20.135.167!

訪問:k8s-node02,在k8s-node02上沒有servicex相關的pod節點

root@k8s-master01:/apps/k8s-yaml/srv-case# while true; do curl -s 172.168.33.211:32223; sleep 2 ;done

因為k8s-node02上沒有services的pod資源,當請求報文指向的節點本地不存在目標Service相關的Pod物件時將直接丟棄該報文。

NodePort型別的Service資源同樣會被配置ClusterIP,以確保叢集內的客戶端對該服務的訪問請求可在叢集範圍的通訊中完成。

3、應用LoadBalancer Service資源

NodePort型別的Service資源雖然能夠在叢集外部訪問,但外部客戶端必須事先得知NodePort和叢集中至少一個節點IP地址,一旦被選定的節點發生故障,客戶端還得自行選擇請求訪問其他的節點,因而一個有著固定IP地址的固定接入端點將是更好的選擇。此外,叢集節點很可能是某IaaS雲環境中僅具有私有IP地址的虛擬主機,這類地址對網際網路客戶端不可達,為此類節點接入流量也要依賴於叢集外部具有公網IP地址的負載均衡器,由其負責接入並排程外部客戶端的服務請求至叢集節點相應的NodePort之上。

IaaS雲端計算環境通常提供了LBaaS(Load Balancer as a Service)服務,它允許租戶動態地在自己的網路建立一個負載均衡裝置。部署在此類環境之上的Kubernetes叢集可藉助於CCM(Cloud Controller Manager)在建立LoadBalancer型別的Service資源時呼叫IaaS的相應API,按需創建出一個軟體負載均衡器。但CCM不會為那些非LoadBalancer型別的Service物件建立負載均衡器,而且當用戶將LoadBalancer型別的Service調整為其他型別時也將刪除此前建立的負載均衡器。

注意:kubeadm在部署Kubernetes叢集時並不會預設部署CCM,有需要的使用者需要自行部署。

對於沒有此類API可用的Kubernetes叢集,管理員也可以為NodePort型別的Service手動部署一個外部的負載均衡器(推薦使用HA配置模型),並配置將請求流量排程至各節點的NodePort之上,這種方式的缺點是管理員需要手動維護從外部負載均衡器到內部服務的對映關係。從實現方式上來說,LoadBalancer型別的Service就是在NodePort型別的基礎上請求外部管理系統的API,並在Kubernetes叢集外部額外建立一個負載均衡器,將流量排程至該NodePort Service之上。Kubernetes以非同步方式請求建立負載均衡器,並將有關配置儲存在Service物件的.status.loadBalancer欄位中。下面是定義在services-loadbalancer-demo.yam配置清單中的LoadBalancer型別Service資源,在最簡單的配置模型中,使用者僅需要修改NodePort Service服務定義中type欄位的值為LoadBalancer即可。

kind: Service
apiVersion: v1
metadata:
  name: demoapp-loadbalancer-svc
spec:
  type: LoadBalancer
  selector:
    app: demoapp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 80

Service物件的loadBalancerIP負責承接外部發來的流量,該IP地址通常由雲服務商系統動態配置,或者藉助.spec.loadBalancerIP欄位顯式指定,但有些雲服務商不支援使用者設定該IP地址,這種情況下,即便提供了也會被忽略。外部負載均衡器的流量會直接排程至Service後端的Pod物件之上,而如何排程流量則取決於雲服務商,有些環境可能還需要為Service資源的配置定義添加註解,必要時請自行參考雲服務商文件說明。另外,LoadBalancer Service還支援使用.spec. loadBalancerSourceRanges欄位指定負載均衡器允許的客戶端來源的地址範圍。

4、外部IP

若叢集中部分或全部節點除了有用於叢集通訊的節點IP地址之外,還有可用於外部通訊的IP地址,如圖中的EIP-1和EIP-2,那麼我們還可以在Service資源上啟用spec.externalIPs欄位來基於這些外部IP地址向外釋出服務。所有路由到指定的外部IP(externalIP)地址某埠的請求流量都可由該Service代理到後端Pod物件之上,如圖所示。從這個角度來說,請求流量到達外部IP與節點IP並沒有本質區別,但外部IP卻可能僅存在於一部分的叢集節點之上,而且它不受Kubernetes叢集管理,需要管理員手動介入其配置和回收等操作任務中。

外部IP地址可結合ClusterIP、NodePort或LoadBalancer任一型別的Service資源使用,而到達外部IP的請求流量會直接由相關聯的Service排程轉發至相應的後端Pod物件進行處理。假設示例Kubernetes叢集中的k8s-node01節點上擁有一個可被路由到的IP地址172.168.33.210,我們期望能夠將demoapp的服務通過該外部IP地址釋出到叢集外部,則可以使用下列配置清單(services-externalip-demo.yaml)中的Service資源實現。

kind: Service
apiVersion: v1
metadata:
  name: demoapp-externalip-svc
  namespace: default
spec:
  type: ClusterIP
  selector:
    app: demoapp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 80
  externalIPs:
  - 172.168.33.210  #叢集外部使用者可以通過172.168.33.210訪問叢集服務

不難猜測,節點k8s-node01故障也必然導致該外部IP上公開的服務不再可達,除非該IP地址可以浮動到其他節點上。如今,大多數雲服務商都支援浮動IP的功能,該IP地址可繫結在某個主機,並在其故障時通過某種觸發機制自動遷移至其他主機。在不具有浮動IP功能的環境中進行測試之前,需要先在k8s-node01上(或根據規劃的其他的節點上)手動配置172.168.34.200這個外部IP地址。而且,在模擬節點故障並手動將外部IP地址配置在其他節點進行浮動IP測試時,還需要清理之前的ARP地址快取。

三、Service的Endpoint資源

在資訊科技領域,端點是指通過LAN或WAN連線的能夠用於網路通訊的硬體裝置,它在廣義上可以指代任何與網路連線的裝置。在Kubernetes語境中,端點通常代表Pod或節點上能夠建立網路通訊的套接字,並由專用的資源型別Endpoint進行定義和跟蹤。

1、Endpoint與容器探針

Service物件藉助於Endpoint資源來跟蹤其關聯的後端端點,但Endpoint是“二等公民”,Service物件可根據標籤選擇器直接建立同名的Endpoint物件,不過使用者幾乎很少有直接使用該型別資源的需求。例如,建立下面配置清單中名為services-readiness-demo的Service物件時就會自動建立一個同名的Endpoint物件。

kind: Service
apiVersion: v1
metadata:
  name: services-readiness-demo
  namespace: default
spec:
  selector:
    app: demoapp-with-readiness
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 80
---
apiVersion: apps/v1
kind: Deployment      # 定義Deployment物件,它使用Pod模板建立Pod物件
metadata:
  name: demoapp2
spec:
  replicas: 2         # 該Deployment物件要求滿足的Pod物件數量
  selector:           # Deployment物件的標籤選擇器,用於篩選Pod物件並完成計數
    matchLabels:
      app: demoapp-with-readiness
  template:           # 由Deployment物件使用的Pod模板,用於建立足額的Pod物件
    metadata:
      labels:
        app: demoapp-with-readiness
    spec:
      containers:
      - name: demoapp
       #image: ikubernetes/demoapp:v1.0
        image: harbor.ywx.net/k8s-baseimages/demoapp:v1.0
        name: demoapp
        imagePullPolicy: IfNotPresent
        readinessProbe:
          httpGet:    # 定義探針型別和探測方式
            path: '/readyz'
            port: 80
          initialDelaySeconds: 15   # 初次檢測延遲時長
          periodSeconds: 10         # 檢測週期

Endpoint物件會根據就緒狀態把同名Service物件標籤選擇器篩選出的後端端點的IP地址分別儲存在subsets.addresses欄位和subsets.notReadyAddresses欄位中,它通過API Server持續、動態跟蹤每個端點的狀態變動,並即時反映到端點IP所屬的欄位。僅那些位於subsets.addresses欄位的端點地址可由相關的Service用作後端端點。此外,相關Service物件標籤選擇器篩選出的Pod物件數量的變動也將會導致Endpoint物件上的端點數量變動。

上面配置清單中定義Endpoint物件services-readiness-demo會篩選出Deployment物件demoapp2建立的兩個Pod物件,將它們的IP地址和服務埠建立為端點物件。但延遲15秒啟動的容器探針會導致這兩個Pod物件至少要在15秒以後才能轉為“就緒”狀態,這意味著在上面配置清單中的Service資源建立後至少15秒之內無可用後端端點,例如下面的資源建立和Endpoint資源監視命令結果中,在20秒之後,Endpoint資源services-readiness-demo才得到第一個可用的後端端點IP。

root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl apply -f services-readiness-demo.yaml 
service/services-readiness-demo created
deployment.apps/demoapp2 created


root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl get endpoints services-readiness-demo -w
NAME                      ENDPOINTS   AGE
services-readiness-demo               33s
services-readiness-demo   172.20.135.169:80   40s
services-readiness-demo   172.20.135.168:80,172.20.135.169:80   40s

root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl describe endpoints services-readiness-demo 
Name:         services-readiness-demo
Namespace:    default
Labels:       <none>
Annotations:  endpoints.kubernetes.io/last-change-trigger-time: 2021-10-03T11:07:31+08:00
Subsets:
  Addresses:          172.20.135.168,172.20.135.169
  NotReadyAddresses:  <none>
  Ports:
    Name  Port  Protocol
    ----  ----  --------
    http  80    TCP

Events:  <none>


root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl get endpoints/services-readiness-demo -o yaml
apiVersion: v1
kind: Endpoints
metadata:
  annotations:
    endpoints.kubernetes.io/last-change-trigger-time: "2021-10-03T11:07:31+08:00"
  creationTimestamp: "2021-10-03T03:06:51Z"
  name: services-readiness-demo
  namespace: default
  resourceVersion: "621584"
  uid: ef4fd249-07c4-4a2b-92f0-258a2df24302
subsets:
- addresses:
  - ip: 172.20.135.168
    nodeName: 172.168.33.212
    targetRef:
      kind: Pod
      name: demoapp2-5c8d4df55d-cxq4l
      namespace: default
      resourceVersion: "621581"
      uid: 25b0c06e-3016-4528-91f8-eec87a8417b9
  - ip: 172.20.135.169
    nodeName: 172.168.33.212
    targetRef:
      kind: Pod
      name: demoapp2-5c8d4df55d-bpzs6
      namespace: default
      resourceVersion: "621576"
      uid: 8b632422-66f0-482a-a31f-b4532362c367
  ports:
  - name: http
    port: 80
    protocol: TCP

#無 NotReadyAddresses狀態的IP

因任何原因導致的後端端點就緒狀態檢測失敗,都會觸發Endpoint物件將該端點的IP地址從subsets.addresses欄位移至subsets.notReadyAddresses欄位。例如,我們使用如下命令人為地將地址172.20.135.168的Pod物件中的容器就緒狀態檢測設定為失敗,以進行驗證。

root@k8s-master01:/apps/k8s-yaml/srv-case# curl -s -X POST -d 'readyz=FAIL' 172.20.135.168/readyz

等待至少3個檢測週期共30秒之後,獲取Endpoint物件services-readiness-demo的資源清單的命令將返回類似如下資訊。

root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl describe endpoints services-readiness-demo 
Name:         services-readiness-demo
Namespace:    default
Labels:       <none>
Annotations:  endpoints.kubernetes.io/last-change-trigger-time: 2021-10-03T11:10:51+08:00
Subsets:
  Addresses:          172.20.135.169
  NotReadyAddresses:  172.20.135.168 #172.20.135.168被設定為NotReadyAddresses狀態
  Ports:
    Name  Port  Protocol
    ----  ----  --------
    http  80    TCP

Events:  <none>


root@k8s-master01:/apps/k8s-yaml/srv-case# kubectl get endpoints/services-readiness-demo -o yaml
apiVersion: v1
kind: Endpoints
metadata:
  annotations:
    endpoints.kubernetes.io/last-change-trigger-time: "2021-10-03T11:10:51+08:00"
  creationTimestamp: "2021-10-03T03:06:51Z"
  name: services-readiness-demo
  namespace: default
  resourceVersion: "621990"
  uid: ef4fd249-07c4-4a2b-92f0-258a2df24302
subsets:
- addresses:
  - ip: 172.20.135.169
    nodeName: 172.168.33.212
    targetRef:
      kind: Pod
      name: demoapp2-5c8d4df55d-bpzs6
      namespace: default
      resourceVersion: "621576"
      uid: 8b632422-66f0-482a-a31f-b4532362c367
  notReadyAddresses:  #172.20.135.168被設定為notReadyAddresses
  - ip: 172.20.135.168
    nodeName: 172.168.33.212
    targetRef:
      kind: Pod
      name: demoapp2-5c8d4df55d-cxq4l
      namespace: default
      resourceVersion: "621987"
      uid: 25b0c06e-3016-4528-91f8-eec87a8417b9
  ports:
  - name: http
    port: 80
    protocol: TCP

該故障端點重新轉回就緒狀態後,Endpoints物件會將其移回subsets.addresses欄位中。這種處理機制確保了Service物件不會將客戶端請求流量排程給那些處於執行狀態但服務未就緒(notReady)的端點。

2、自定義Endpoint資源

除了藉助Service物件的標籤選擇器自動關聯後端端點外,Kubernetes也支援自定義Endpoint物件,使用者可通過配置清單建立具有固定數量端點的Endpoint物件,而呼叫這類Endpoint物件的同名Service物件無須再使用標籤選擇器。Endpoint資源的API規範如下。

apiVersion: v1
kind: Endpoint
metadata:                  # 物件元資料
  name:
  namespace:
subsets:                   # 端點物件的列表
- addresses:               # 處於“就緒”狀態的端點地址物件列表
  - hostname  <string>     # 端點主機名
    ip <string>            # 端點的IP地址,必選欄位
    nodeName <string>      # 節點主機名
    targetRef:            # 提供了該端點的物件引用
      apiVersion <string>  # 被引用物件所屬的API群組及版本
      kind <string>        # 被引用物件的資源型別,多為Pod
      name <string>        # 物件名稱
      namespace <string>   # 物件所屬的名稱空間
      fieldPath <string>   # 被引用的物件的欄位,在未引用整個物件時使用,通常僅引用
                           # 指定Pod物件中的單容器,例如spec.containers[1]
      uid <string>         # 物件的識別符號
  notReadyAddresses:       # 處於“未就緒”狀態的端點地址物件列表,格式與address相同
  ports:                   # 埠物件列表
  - name <string>          # 埠名稱
    port <integer>         # 埠號,必選欄位
    protocol <string>      # 協議型別,僅支援UDP、TCP和SCTP,預設為TCP
    appProtocol <string>   # 應用層協議

自定義Endpoint常將那些不是由編排程式編排的應用定義為Kubernetes系統的Service物件,從而讓客戶端像訪問叢集上的Pod應用一樣請求外部服務。例如,假設要把Kubernetes叢集外部一個可經由172.168.32.51:3306或172.168.32.52:3306任一端點訪問的MySQL資料庫服務引入叢集中,便可使用如下清單中的配置完成。

apiVersion: v1
kind: Endpoints
metadata:
  name: mysql-external #name必須與service的name一樣,才能被service呼叫
  namespace: default
subsets:
#引入叢集的外部地址和埠
- addresses:
  - ip: 172.29.9.51
  - ip: 172.29.9.52
  ports:
  - name: mysql
    port: 3306
    protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: mysql-external #name必須與endpoint的name一樣,才能被service呼叫
  namespace: default
spec:
  type: ClusterIP
  ports:
  - name: mysql
    port: 3306
    targetPort: 3306
    protocol: TCP
 #kubernetes叢集可以通過訪問該service來呼叫外部的mysql。

顯然,非經Kubernetes管理的端點,其就緒狀態難以由Endpoint通過註冊監視特定的API資源物件進行跟蹤,因而使用者需要手動維護這種呼叫關係的正確性。

Endpoint資源提供了在Kubernetes叢集上跟蹤端點的簡單途徑,但對於有著大量端點的Service來說,將所有的網路端點資訊都儲存在單個Endpoint資源中,會對Kubernetes控制平面元件產生較大的負面影響,且每次端點資源變動也會導致大量的網路流量。EndpointSlice(端點切片)通過將一個服務相關的所有端點按固定大小(預設為100個)切割為多個分片,提供了一種更具伸縮性和可擴充套件性的端點替代方案。EndpointSlice由引用的端點資源組成,類似於Endpoint,它可由使用者手動建立,也可由EndpointSlice控制器根據使用者在建立Service資源時指定的標籤選擇器篩選叢集上的Pod物件自動建立。單個EndpointSlice資源預設不能超過100個端點,小於該數量時,EndpointSlice與Endpoint存在1:1的對映關係且效能相同。EndpointSlice控制器會盡可能地填滿每一個EndpointSlice資源,但不會主動進行重新平衡,新增的端點會嘗試新增到現有的EndpointSlice資源上,若超出現有任何EndpointSlice物件的可用的空餘空間,則將建立新的EndpointSlice,而非分散填充。

EndpointSlice自Kubernetes 1.17版本開始升級為Beta版,隸屬於discovery.k8s.io這一API群組。EndpointSlice控制器會為每個Endpoint資源自動生成一個EndpointSlice資源。例如,下面的命令列出了kube-system名稱空間中的所有EndpointSlice資源,kube-dns-mbdj5來自於對kube-dns這一Endpoint資源的自動轉換。

~$ kubectl get endpointslice -n kube-system
NAME           ADDRESSTYPE        PORTS            ENDPOINTS        AGE
kube-dns-mbdj5   IPv4          53,9153,53   10.244.0.6,10.244.0.7   13d

EndpointSlice資源根據其關聯的Service與埠劃分成組,每個組隸屬於同一個Service。