七、Kubernetes資源對象之Service基礎


運行於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)NodePort NodePort類型是對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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM