一、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之上的調度規則。
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是默認的代理模型。
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版本成為默認的類型。
2.3 ipvs代理模式
Kubernetes自v1.9版本起引入ipvs代理模型,且自v1.11版本起成為默認設置。在此種模型中,kube-proxy跟蹤API Server上Service和Endpoints對象的變動,並據此來調用netlink接口創建或變更ipvs(NAT)規則,如圖所示。它與iptables規則的不同之處僅在於客戶端請求流量的調度功能由ipvs實現,余下的其他功能仍由iptables完成。
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所在的節點,該請求必然因需要額外的轉發過程(躍點)和更多的處理步驟而產生更多延遲。
(3)LoadBalancer 這種類型的Service依賴於部署在IaaS雲計算服務之上並且能夠調用其API接口創建軟件負載均衡器的Kubernetes集群環境。LoadBalancer Service構建在NodePort類型的基礎上,通過雲服務商提供的軟負載均衡器將服務暴露到集群外部,因此它也會具有NodePort和ClusterIP。簡言之,創建LoadBalancer類型的Service對象時會在集群上創建一個NodePort類型的Service,並額外觸發Kubernetes調用底層的IaaS服務的API創建一個軟件負載均衡器,而集群外部的請求流量會先路由至該負載均衡器,並由該負載均衡器調度至各節點上該Service對象的NodePort,如圖所示。該Service類型的優勢在於,它能夠把來自集群外部客戶端的請求調度至所有節點(或部分節點)的NodePort之上,而不是讓客戶端自行決定連接哪個節點,也避免了因客戶端指定的節點故障而導致的服務不可用。
二、應用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值
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>
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
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對象時將直接丟棄該報文。
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,有需要的用戶需要自行部署。
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地址可結合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訪問集群服務
三、Service的Endpoint資源
在信息技術領域,端點是指通過LAN或WAN連接的能夠用於網絡通信的硬件設備,它在廣義上可以指代任何與網絡連接的設備。在Kubernetes語境中,端點通常代表Pod或節點上能夠建立網絡通信的套接字,並由專用的資源類型Endpoint進行定義和跟蹤。
1、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對象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
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。
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。