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)注微信公眾號 :山河已無恙