Kubernetes: 通過無頭服務(wù)(Headless Service)實(shí)現(xiàn)客戶端負(fù)載均衡
寫在前面
分享一些 K8s 中 Headless Service 的筆記
博文內(nèi)容涉及:
Headless Service 的簡單介紹
Headless Service 創(chuàng)建
集群內(nèi)/外 獲取 Headless Service對應(yīng) Pod 列表的 Demo
理解不足小伙伴幫忙指正
在認(rèn)識一經(jīng)出現(xiàn)時(shí),情欲就引退。 -----昂克敵·杜伯隆:《鄔布涅伽研究》第二卷第216頁 -----《作為意志和表象的世界》
Headless Service 簡單介紹
在某些場景中,如果我們希望自己控制 Pod 實(shí)例的負(fù)載均衡 策略,或者希望直接和 Pod 交互但是又不希望通過端口映射的方式,比如數(shù)據(jù)庫根據(jù)情況做一些讀寫分離,或者一些應(yīng)用在客戶端做流量控制等,不使用 Service 提供的由 Kube-proxy 代理實(shí)現(xiàn)的默認(rèn)負(fù)載均衡的功能。希望明確是由那幾個(gè) pod 提供能力,即直接通過 Pod 發(fā)布服務(wù), 而不是只有一個(gè) 集群 IP Cluster IP 或者使用 NodePort、LoadBalancer、ExternalName 來發(fā)布服務(wù)。
這個(gè)時(shí)候,K8s 提供了 Headless Service ,即不為 Service 設(shè)置 ClusterIP(入口IP地址),也叫 無頭服務(wù),這里分兩種情況
有選擇器
第一種是有對應(yīng)的服務(wù)能力提供者,即通過標(biāo)簽選擇器選擇了對應(yīng)的后端能力,比如 pod,deployment,statefulset 等
在這種情況下,會通過Label Selector將被選擇的后端 Pod 列表返回給調(diào)用的客戶端, K8s 不會為這樣的 Service 分配任何 IP, DNS 會為這些Service 的 Name 添加一系列的 A(AAA)記錄(IP 地址指向),直接指向后端映射的 Pod。 當(dāng)然前提是 通過 標(biāo)簽選擇器選擇到了對應(yīng)的 pod。
Kube-prosy 不會處理這類型的 Service ,沒有負(fù)載均衡機(jī)制也沒有請求映射,這里 Endpoint Controller 任然會創(chuàng)建 pod 對應(yīng)的 Endpoint , 同時(shí) Kubernetes 控制平面會在 Kubernetes API 中創(chuàng)建 EndpointSlice 對象
EndpointSlices 表示針對服務(wù)的后端網(wǎng)絡(luò)端點(diǎn)的子集(切片),這是在 1.21 版本才出現(xiàn)的,提供了一種簡單的方法來跟蹤 Kubernetes 集群中的網(wǎng)絡(luò)端點(diǎn)(network endpoints)。EndpointSlices 為 Endpoints 提供了一種可擴(kuò)縮和可拓展的替代方案。
在 Kubernetes 中,EndpointSlice 包含對一組網(wǎng)絡(luò)端點(diǎn)的引用。 控制面會自動為設(shè)置了選擇算符的 Kubernetes Service 創(chuàng)建 EndpointSlice,EndpointSlice 將包含對與 Service 選擇算符匹配的所有 Pod 的引用。 EndpointSlice 通過唯一的協(xié)議、端口號和 Service 名稱將網(wǎng)絡(luò)端點(diǎn)組織在一起
Headless Service 通過暴露的 Endpoints 列表 應(yīng)用可以通過編碼實(shí)現(xiàn)客戶端的負(fù)載均衡。
沒有選擇器
第二種是沒有對應(yīng)的服務(wù)能力提供者,即沒有通過選擇運(yùn)算符來獲取當(dāng)前 集群的能力,這個(gè)時(shí)候,系統(tǒng)不會創(chuàng)建對應(yīng) Endpoint ,也不會創(chuàng)建對應(yīng)的 EndpointSlice.
這種情況下,DNS 系統(tǒng)會查找和配置以下之一
對于 type: ExternalName 服務(wù),查找和配置其 CNAME 記錄
對所有其他類型的服務(wù),針對 Service 的就緒端點(diǎn)的所有 IP 地址,查找和配置 DNS A / AAAA 條記錄
對于 IPv4 端點(diǎn),DNS 系統(tǒng)創(chuàng)建 A 條記錄。
對于 IPv6 端點(diǎn),DNS 系統(tǒng)創(chuàng)建 AAAA 條記錄。
Headless Service 創(chuàng)建
定義一個(gè) Service 里面的 ClusterIP 為 None ,并且擁有 Selector 標(biāo)簽選擇器,這樣的 Service 為 Headlsee Service
查看當(dāng)前 k8s 中是否存在 Headlsee
┌──[root@vms81.liruilongs.github.io]-[~]
└─$kubectl get svc -A | grep None
awx awx-demo-postgres-13 ClusterIP None <none> 5432/TCP 52d
kube-system liruilong-kube-prometheus-kubelet ClusterIP None <none> 10250/TCP,10255/TCP,4194/TCP 324d
一般情況下 SatefulSet 需要 Headless Service 來實(shí)現(xiàn) Pod 的網(wǎng)絡(luò)的一致性(必須創(chuàng)建此服務(wù)),為客戶端返回多個(gè)服務(wù)端地址。可以看到當(dāng)前的集群中有兩個(gè) Headless Service, 一個(gè)是有狀態(tài)應(yīng)用(SatefulSet) postgres 數(shù)據(jù)庫創(chuàng)建,一個(gè)是搭建 prometheus 集群監(jiān)控創(chuàng)建的。
┌──[root@vms81.liruilongs.github.io]-[~]
└─$kubectl get svc awx-demo-postgres-13 -o yaml
apiVersion: v1
kind: Service
metadata:
..................
name: awx-demo-postgres-13
namespace: awx
spec:
clusterIP: None
clusterIPs:
- None
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- port: 5432
protocol: TCP
targetPort: 5432
selector:
app.kubernetes.io/component: database
app.kubernetes.io/instance: postgres-13-awx-demo
app.kubernetes.io/managed-by: awx-operator
app.kubernetes.io/name: postgres-13
app.kubernetes.io/part-of: awx-demo
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
這里我們可以大概的看到一個(gè) Headless 資源文件定義,對這樣的 Service 進(jìn)行訪問,得到的就是一個(gè) 符合選擇器的全部的 Pod 列表,然后客戶端去自行的處理這些 Pod 列表。上面的 Service 中,客戶端訪問 postgres 數(shù)據(jù)庫,會返回符合當(dāng)前選擇器的所有 postgres pod。
下面我們來看幾個(gè)實(shí)際的 Demo
有狀態(tài) Headless 服務(wù)
對于有狀態(tài)服務(wù)開來講,需要創(chuàng)建 StatefulSet 為其提供能力,資源文件定義,只是一個(gè) Demo ,所以我們這里沒有定義 存儲卷相關(guān)。
apiVersion: apps/v1
kind: StatefulSet
metadata:
creationTimestamp: null
labels:
app: web-headless
name: web
spec:
serviceName: web-headless
replicas: 3
selector:
matchLabels:
app: web-headless
template:
metadata:
creationTimestamp: null
labels:
app: web-headless
spec:
containers:
- image: nginx
name: nginx-web
ports:
- containerPort: 80
name: nginx-web
resources: {}
通過資源文件創(chuàng)建有狀態(tài)的 pod
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$kubectl get statefulsets.apps web -o wide
NAME READY AGE CONTAINERS IMAGES
web 3/3 96s nginx-web nginx
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$kubectl get pods -o wide | grep web*
web-0 1/1 Running 0 2m13s 10.244.217.10 vms155.liruilongs.github.io <none> <none>
web-1 1/1 Running 0 2m11s 10.244.194.67 vms156.liruilongs.github.io <none> <none>
web-2 1/1 Running 0 114s 10.244.217.11 vms155.liruilongs.github.io <none> <none>
之后我們需要創(chuàng)建對應(yīng)的 Headless Service ,這里需要注意的是 clusterIP: None,選擇器:app: web-headless
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$cat headless.yaml
apiVersion: v1
kind: Service
metadata:
name: web-headless
labels:
app: nginx_headless
spec:
ports:
- port: 30088
targetPort: 80
name: nginx-web-headless
clusterIP: None
selector:
app: web-headless
創(chuàng)建對應(yīng)的 Headless SVC
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$kubectl apply -f headless.yaml
service/web-headless configured
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$kubectl get svc web-headless
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web-headless ClusterIP None <none> 30088/TCP 24h
可以看到,他對每個(gè) pod 都創(chuàng)建了對應(yīng)的 Endpoint
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$kubectl describe svc web-headless
Name: web-headless
Namespace: awx
Labels: app=nginx_headless
Annotations: <none>
Selector: app=web-headless
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: None
IPs: None
Port: nginx-web-headless 30088/TCP
TargetPort: 80/TCP
Endpoints: 10.244.194.67:80,10.244.217.10:80,10.244.217.11:80
Session Affinity: None
Events: <none>
可以通過不同的方式獲取 Headless Service 的 Pod 列表。
集群外獲取 Headless Service 的 Pod 列表
可以直接通過調(diào)用 Rest 接口的 方式獲取 Headless 對應(yīng)的 Endpoints,這里為了方便暴露 Rest 服務(wù),通過 kubectl proxy 做一個(gè)內(nèi)部代理。
┌──[root@vms81.liruilongs.github.io]-[~]
└─$nohup kubectl proxy --port=30021 &
[1] 109103
┌──[root@vms81.liruilongs.github.io]-[~]
└─$nohup: 忽略輸入并把輸出追加到"nohup.out"
測試一下
┌──[root@vms81.liruilongs.github.io]-[~]
└─$curl http://localhost:30021/api/ -s
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "192.168.26.81:6443"
}
]
}
對于 1.21 之前的版本獲取 endpoins,可以通過 Endpoints的方式獲取
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$kubectl describe endpoints web-headless
Name: web-headless
Namespace: awx
Labels: app=nginx_headless
service.kubernetes.io/headless=
Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2022-12-09T07:13:15Z
Subsets:
Addresses: 10.244.194.67,10.244.217.10,10.244.217.11
NotReadyAddresses: <none>
Ports:
Name Port Protocol
---- ---- --------
nginx-web-headless 80 TCP
Events: <none>
通過 curl 調(diào)用對應(yīng)的 REST 接口
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$curl -s http://localhost:30021/api/v1/namespaces/awx/endpoints/web-headless | jq .subsets
[
{
"addresses": [
{
"ip": "10.244.194.67",
"hostname": "web-1",
"nodeName": "vms156.liruilongs.github.io",
"targetRef": {
"kind": "Pod",
"namespace": "awx",
"name": "web-1",
"uid": "d71722db-1c41-44ee-a55d-0042c7d2086e",
"resourceVersion": "14843070"
}
},
{
"ip": "10.244.217.10",
"hostname": "web-0",
"nodeName": "vms155.liruilongs.github.io",
"targetRef": {
"kind": "Pod",
"namespace": "awx",
"name": "web-0",
"uid": "7fa21492-d0f5-4840-8517-a05ed04651a4",
"resourceVersion": "14843008"
}
},
{
"ip": "10.244.217.11",
"hostname": "web-2",
"nodeName": "vms155.liruilongs.github.io",
"targetRef": {
"kind": "Pod",
"namespace": "awx",
"name": "web-2",
"uid": "7b262352-b12c-4ad2-8d83-8143c71e8c27",
"resourceVersion": "14843135"
}
}
],
"ports": [
{
"name": "nginx-web-headless",
"port": 80,
"protocol": "TCP"
}
]
}
]
如果使用的 1.21 以及之后的版本,我們可以通過 EndpointSlicp 來獲取對應(yīng)的 pod 列表
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$kubectl describe endpointslices web-headless-888xr
Name: web-headless-888xr
Namespace: awx
Labels: app=nginx_headless
endpointslice.kubernetes.io/managed-by=endpointslice-controller.k8s.io
kubernetes.io/service-name=web-headless
service.kubernetes.io/headless=
Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2022-12-09T07:13:15Z
AddressType: IPv4
Ports:
Name Port Protocol
---- ---- --------
nginx-web-headless 80 TCP
Endpoints:
- Addresses: 10.244.217.10
Conditions:
Ready: true
Hostname: web-0
TargetRef: Pod/web-0
NodeName: vms155.liruilongs.github.io
Zone: <unset>
- Addresses: 10.244.194.67
Conditions:
Ready: true
Hostname: web-1
TargetRef: Pod/web-1
NodeName: vms156.liruilongs.github.io
Zone: <unset>
- Addresses: 10.244.217.11
Conditions:
Ready: true
Hostname: web-2
TargetRef: Pod/web-2
NodeName: vms155.liruilongs.github.ioweb
Zone: <unset>
Events: <none>
通過 curl 調(diào)用對應(yīng)的 REST 接口
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$curl -s http://localhost:30021/apis/discovery.k8s.io/v1/namespaces/awx/endpointslices/web-headless-888xr | jq .endpoints
[
{
"addresses": [
"10.244.217.10"
],
"conditions": {
"ready": true,
"serving": true,
"terminating": false
},
"hostname": "web-0",
"targetRef": {
"kind": "Pod",
"namespace": "awx",
"name": "web-0",
"uid": "7fa21492-d0f5-4840-8517-a05ed04651a4",
"resourceVersion": "14843008"
},
"nodeName": "vms155.liruilongs.github.io"
},
{
"addresses": [
"10.244.194.67"
],
"conditions": {
"ready": true,
"serving": true,
"terminating": false
},
"hostname": "web-1",
"targetRef": {
"kind": "Pod",
"namespace": "awx",
"name": "web-1",
"uid": "d71722db-1c41-44ee-a55d-0042c7d2086e",
"resourceVersion": "14843070"
},
"nodeName": "vms156.liruilongs.github.io"
},
{
"addresses": [
"10.244.217.11"
],
"conditions": {
"ready": true,
"serving": true,
"terminating": false
},
"hostname": "web-2",
"targetRef": {
"kind": "Pod",
"namespace": "awx",
"name": "web-2",
"uid": "7b262352-b12c-4ad2-8d83-8143c71e8c27",
"resourceVersion": "14843135"
},
"nodeName": "vms155.liruilongs.github.io"
}
]
集群內(nèi)獲取 Headless Service 的 Pod 列表
對于無頭服務(wù),客戶端可以通過連接到服務(wù)的 DNS 名稱來連接到其 pod,就像使用常規(guī)服務(wù)一樣,因?yàn)?DNS 返回 pod 的 IP,客戶端直接連接到 pod,所以不是通過服務(wù)代理。這里通過 DNS 解析獲取的 Pod 列表,Headless 服務(wù)仍然提供跨 Pod 的負(fù)載平衡,但這僅僅是通過 DNS 循環(huán)機(jī)制實(shí)現(xiàn)的負(fù)載均衡。而不是 sessionAffinity 相關(guān)配置
可以通過 對 服務(wù)的 DNS 解析來獲取 POD 列表。
創(chuàng)建一個(gè)測試 Pod
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$kubectl run tmp01 --image=tutum/dnsutils -- sleep infinity
pod/tmp01 created
同一命令空間獲取 headless Service 的 Pod 列表
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$kubectl exec tmp01 -it -- /bin/bash
root@tmp01:/# nslookup web-headless
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: web-headless.awx.svc.cluster.local
Address: 10.244.194.67
Name: web-headless.awx.svc.cluster.local
Address: 10.244.217.10
Name: web-headless.awx.svc.cluster.local
Address: 10.244.217.11
不同命名空間獲取 headless Service 的 Pod 列表
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$kubectl exec tmp01 -it -- /bin/bash
root@tmp01:/# nslookup web-headless.awx
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: web-headless.awx.svc.cluster.local
Address: 10.244.217.11
Name: web-headless.awx.svc.cluster.local
Address: 10.244.194.67
Name: web-headless.awx.svc.cluster.local
Address: 10.244.217.10
root@tmp01:/# nslookup web-headless.awx.svc.cluster.local.
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: web-headless.awx.svc.cluster.local
Address: 10.244.217.11
Name: web-headless.awx.svc.cluster.local
Address: 10.244.194.67
Name: web-headless.awx.svc.cluster.local
Address: 10.244.217.10
無狀態(tài)的 Headless 服務(wù)
關(guān)于無狀態(tài)的 Headless Service 這里我們也簡單介紹,和,有狀態(tài)的沒什么區(qū)別,對應(yīng)的 SVC 還是用之前的 web-headless, 在 StatefulSet 的基礎(chǔ)上,我們創(chuàng)建一個(gè) deloyment 提供服務(wù)能力
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$cat deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: web-headless
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web-headless
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: web-headless
spec:
containers:
- image: nginx
name: nginx-web
ports:
- containerPort: 80
name: nginx-web
resources: {}
status: {}
通過 DNS 獲取IP,可以發(fā)現(xiàn),對應(yīng)的 A 記錄增加到 6個(gè)
┌──[root@vms81.liruilongs.github.io]-[~/ansible]
└─$kubectl exec tmp01 -it -- /bin/bash
root@tmp01:/# nslookup web-headless.awx
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: web-headless.awx.svc.cluster.local
Address: 10.244.194.67
Name: web-headless.awx.svc.cluster.local
Address: 10.244.217.11
Name: web-headless.awx.svc.cluster.local
Address: 10.244.217.10
Name: web-headless.awx.svc.cluster.local
Address: 10.244.194.69
Name: web-headless.awx.svc.cluster.local
Address: 10.244.194.70
Name: web-headless.awx.svc.cluster.local
Address: 10.244.217.12
root@tmp01:/# exit
exit
關(guān)于 Headless Service 和小伙伴分享到這里, 通過 無頭服務(wù),我們可以通過 Servcie 來動態(tài)感知 Pod 副本的變化,監(jiān)聽 Pod 的狀態(tài),實(shí)現(xiàn)部分分布式集群的動態(tài)構(gòu)建, 同時(shí)在有狀態(tài)應(yīng)用中都會涉及 Headless Service。
博文參考
https://kubernetes.io/zh-cn/docs/concepts/services-networking/service/#headless-services
https://stackoverflow.com/questions/52707840/what-is-a-headless-service-what-does-it-do-accomplish-and-what-are-some-legiti
https://cloud.tencent.com/developer/article/1638722
https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoints-v1/
https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/
作者:山河已無恙
歡迎關(guān)注微信公眾號 :山河已無恙