APISIX Ingress 是如何支援上千個 Pod 副本的應用

Apache_APISIX 發表於 2022-11-28
作者:容鑫,Apache APISIX Committer

在 K8s 中為什麼會遇到上千個 Pod 副本的應用場景?

在 Kubernetes 中,Pod 是最小的排程單元。應用程式實際是以 Pod 在執行的,通常情況下出於可擴充套件性和降低爆炸半徑等方面的考慮,只會給 Pod 設定有限的資源。那麼對於大流量的場景,一般都是透過水平擴容的方式進行應對。

例如電商行業在進行促銷活動或秒殺搶購活動時,業務流量相對較大。為了應對這種場景,通常會設定彈性擴容。在活動進行時,服務會進行彈性伸縮直到能夠承載流量,這時會基於彈性擴容的策略,為業務增加副本數,也就是 Pod 會變多。

每個 Pod 都有各自唯一的 IP ,但同時 Pod 的 IP 也不是固定的。為了及時追蹤 Pod IP 的變化,從而進行負載均衡,Endpoints API 提供了在 Kubernetes 中跟蹤網路端點的一種簡單而直接的方法。
但隨著 Kubernetes 叢集和服務逐漸開始為更多的後端 Pod 進行處理和傳送請求,比如上文提到大流量場景下,Pod 數量會被不斷擴容,Endpoints API 也將變得越大。這種情況下,Endpoints API 侷限性變得越來越明顯,甚至成為效能瓶頸。

為了解決這個侷限性問題,在 Kubernetes v1.21 的版本中引入了對 Endpointslice API 的支援,解決了 Endpoints API 處理大量網路端點帶來的效能問題,同時提供了可擴充套件和可伸縮的能力。
透過下圖我們可以明顯看到它們之間的區別:

  • Endpoints 在流量高峰時的變化:
    APISIX Ingress 是如何支援上千個 Pod 副本的應用
  • Endpointslices 在流量高峰時的變化:
    APISIX Ingress 是如何支援上千個 Pod 副本的應用

在 Kubernetes 中,應用之間是如何進行相互訪問的呢?Endpoints 和 Endpointslice 具體區別又是什麼?和 Pod 有著什麼樣的關係?APISIX Ingress 中為什麼要支援這些特性,以及如何進行安裝和使用?本文將著重介紹這些問題。

Kubernetes 中如何訪問應用

在 Kubernetes 中,每個 Pod 都有其自己唯一的 IP 地址。通常情況下,Service 透過 selector 和一組 Pod 建立關聯,並提供了相同的 DNS 名,並可以在它之間進行負載均衡。Kubernetes 叢集內不同應用之間可透過 DNS 進行相互訪問。

在 Service 建立時,Kubernetes 會根據 Service 關聯一個 Endpoints 資源,若 Service 沒有定義 selector 欄位,將不會自動建立 Endpoints。

什麼是 Endpoints

Endpoints 是 Kubernetes 中的一個資源物件,儲存在 etcd 中,用來記錄一個 Service 對應一組 Pod 的訪問地址,一個 Service 只有一個 Endpoints 資源。Endpoints 資源會去觀測 Pod 集合,只要服務中的某個 Pod 發生變更,Endpoints 就會進行同步更新。
比如部署 3 個 httpbin 副本,檢視 Pod 的情況,包括 IP 資訊。

$ kubectl get pods -o wide
NAME                                 READY   STATUS    RESTARTS        AGE   IP            NODE             NOMINATED NODE   READINESS GATES
httpbin-deployment-fdd7d8dfb-8sxxq   1/1     Running   0               49m   10.1.36.133   docker-desktop   <none>           <none>
httpbin-deployment-fdd7d8dfb-bjw99   1/1     Running   4 (5h39m ago)   23d   10.1.36.125   docker-desktop   <none>           <none>
httpbin-deployment-fdd7d8dfb-r5nf9   1/1     Running   0               49m   10.1.36.131   docker-desktop   <none>           <none>

建立 httpbin 服務,並檢視應 Endpoints 端點情況。

$ kubectl get endpoints httpbin
NAME      ENDPOINTS                                      AGE
httpbin   10.1.36.125:80,10.1.36.131:80,10.1.36.133:80   23d

從上述示例可以看到,Endpoints 中 httpbin 資源物件的所有網路端點,分別對應了每個 Pod 的 IP 地址。

當然, Endpoints 也有它的一些不足之處,比如:

  1. Endpoints 具有容量限制,如果某個 Endpoints 資源中埠的個數超過 1000,那麼 Endpoints 控制器會將其截斷為 1000。
  2. 一個 Service 只有一個 Endpoints 資源,這意味著它需要為支援相應服務的每個 Pod 儲存 IP 等網路資訊。這導致 Endpoints 資源變的十分巨大,其中一個端點發生了變更,將會導致整個 Endpoints 資源更新。當業務需要進行頻繁端點更新時,一個巨大的 API 資源被相互傳遞,而這會影響到 Kubernetes 元件的效能,並且會產生大量的網路流量和額外的處理。

什麼是 Endpointslices

Endpointslices 為 Endpoints 提供了一種可擴縮和可擴充的替代方案,緩解處理大量網路端點帶來的效能問題,還能為一些諸如拓撲路由的額外功能提供一個可擴充套件的平臺。該特性在 Kubernetes v1.21+ 的版本中已提供支援。

EndpointSlice 旨在透過分片的方式來解決此問題,並沒有使用單個 Endpoints 資源跟蹤服務的所有網路端點,而是將它們拆分為多個較小的 EndpointSlice。

預設情況下,控制面建立和管理的 EndpointSlice 將包含不超過 100 個端點。 你可以使用 kube-controller-manager--max-endpoints-per-slice 標誌設定此值,其最大值為 1000。

為什麼需要 Endpointslices

首先,我們考慮具有 2000 個 Pod 的服務它最終可能具有 1.0 MB 的 Endpoints 資源。在生產環境中,如果該服務發生滾動更新或節點遷移,那麼 Endpoints 資源將會頻繁變更。

想象一下,如果滾動更新會導致全部 Pod 都被替換,由於 etcd 具有最大請求大小限制,Kubernetes 對 Endpoints 最大容量限制為 1000,如果網路端點數量超出了 1000,那麼多出來的網路端點,將不會被 Endpoints 資源記錄。

當然也可能因為一些需求,需要多次進行滾動更新,那麼這個巨大的 API 資源物件將會 Kubernetes 元件中來回傳遞,極大影響了 Kubernetes 元件的效能。

如果使用了 Endpointslices,假設一個服務後端有 2000 個 Pod。如果將配置修改為每個 Endpointslices 儲存 100 個端點,最終將獲得 20 個 Endpointslices。新增或刪除 Pod 時,只需要更新其中 1 個 Endpointslice 資源即可,這樣操作後,可擴充套件性和網路可伸縮有了很大的提升。

比起在流量高峰時,服務為了承載流量,擴容出大量的 Pod,Endpoints 資源會被頻繁更新,兩個使用場景的差異就變得非常明顯。更重要的是,既然服務的所有 Pod IP 都不需要儲存在單個資源中,那麼我們就不必擔心 etcd 中儲存的物件的大小限制。

Endpoints VS Endpointslice

因為 Endpointslice 是在 Kubernetes v1.21+ 的版本得到支援,所以該結論是基於 Kubernetes v1.21+ 版本。

透過上文的描述我們總結一下兩種資源的適用情況。

Endpoints 適用場景:

  • 有彈性伸縮需求,Pod 數量較少,傳遞資源不會造成大量網路流量和額外處理。
  • 無彈性伸縮需求,Pod 數量不會太多。哪怕 Pod 數量是固定,但是總是要滾動更新或者出現故障的。

Endpointslice 適用場景:

  • 有彈性需求,且 Pod 數量較多(幾百上千)。
  • Pod 數量很多(幾百上千),因為 Endpoints 網路端點最大數量限制為 1000,所以超過 1000 的 Pod 必須得用 Endpointslice。

在 APISIX Ingress 中的實踐

APISIX Ingress Controller 是一個 Ingress 控制器的實現。可以將使用者配置的規則轉換為 Apache APISIX中的規則,從而使用 APISIX 完成具體的流量承載。

在具體實現過程中,APISIX Ingress 透過 watch Endpoints 或 Endpointslice 資源的變化,從而讓 APISIX 能夠對 Pod 進行負載均衡和健康檢查等。為了能夠支援 Kubernetes v1.16+ 的版本,APISIX Ingress 在安裝時,預設使用 Endpoints 的特性。

如果你的叢集版本為 Kubernetes v1.21+,在安裝 APISIX Ingress 時,需要指定 watchEndpointSlice=true 標誌來開啟 Endpointslice 特性的支援。

注意:在叢集的版本為 Kubernetes v1.21+ 時,我們推薦使用 Endpointslice 特性。否則當服務中 Pod 數量超過 --max-endpoints-per-slice 標誌設定的值時,由於 APISIX Ingress watch 的是 Endpoints 資源物件,則會導致配置的丟失。開啟此特性,就不額外關注 --max-endpoints-per-slice 的值。

以下將為你簡單介紹如何在 APISIX Ingress 中應用 Endpointslice 特性。

建立包含 20 個副本的 Service

使用 kubectl apply -f httpbin-deploy.yaml 命令,在 Kuernetes 中部署 httpbin 應用服務,並建立 20 個 Pod 副本。具體 htppbin-deploy.yaml 檔案配置可參考下方程式碼。

# htppbin-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin-deployment
spec:
  replicas: 20
  selector:
    matchLabels:
      app: httpbin-deployment
  strategy:
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: httpbin-deployment
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - livenessProbe:
            failureThreshold: 3
            initialDelaySeconds: 2
            periodSeconds: 5
            successThreshold: 1
            tcpSocket:
              port: 80
            timeoutSeconds: 2
          readinessProbe:
            failureThreshold: 3
            initialDelaySeconds: 2
            periodSeconds: 5
            successThreshold: 1
            tcpSocket:
              port: 80
            timeoutSeconds: 2
          image: "kennethreitz/httpbin:latest"
          imagePullPolicy: IfNotPresent
          name: httpbin-deployment
          ports:
            - containerPort: 80
              name: "http"
              protocol: "TCP"

---

apiVersion: v1
kind: Service
metadata:
  name: httpbin
spec:
  selector:
    app: httpbin-deployment
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 80
  type: ClusterIP

透過 APISIX Ingress 代理

第一步,使用 Helm 安裝 APISIX Ingress。
透過 ·--set ingress-controller.config.kubernetes.watchEndpointSlice=true` 開啟 Endpointslice 特性的支援。

helm repo add apisix https://charts.apiseven.com
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update


kubectl create ns ingress-apisix
helm install apisix apisix/apisix \
  --set gateway.type=NodePort \
  --set ingress-controller.enabled=true \
  --namespace ingress-apisix \
  --set ingress-controller.config.apisix.serviceNamespace=ingress-apisix \
  --set ingress-controller.config.kubernetes.watchEndpointSlice=true 

第二步,使用 CRD 資源進行代理。注意,APISIX Ingress 中對 Endpoints 和 Endpointslice 特性的支援,使用者是感知不到的,它們的配置都是一致的。

apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: httpbin-route
spec:
  http:
  - name: rule
    match:
      hosts:
      - httpbin.org
      paths:
      - /get
    backends:
       - serviceName: httpbin
         servicePort: 80

第三步,進入 APISIX Pod 中檢視。可以看到在 APISIX 中的 upstream 物件 nodes 欄位包含了 20 個 Pod 的 IP 地址。

kubectl exec -it ${Pod for APISIX} -n ingress-apisix -- curl "http://127.0.0.1:9180/apisix/admin/upstreams" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
{
    "action": "get",
    "count": 1,
    "node": {
        "key": "\/apisix\/upstreams",
        "nodes": [
            {
                "value": {
                    "hash_on": "vars",
                    "desc": "Created by apisix-ingress-controller, DO NOT modify it manually",
                    "pass_host": "pass",
                    "nodes": [
                        {
                            "weight": 100,
                            "host": "10.1.36.100",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.101",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.102",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.103",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.104",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.109",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.92",
                            "priority": 0,
                            "port": 80
                        }
                        ... // 以下省略 13 個節點
                        // 10.1.36.118
                        // 10.1.36.115
                        // 10.1.36.116
                        // 10.1.36.106
                        // 10.1.36.113
                        // 10.1.36.111
                        // 10.1.36.108
                        // 10.1.36.114
                        // 10.1.36.107
                        // 10.1.36.110
                        // 10.1.36.105
                        // 10.1.36.112
                        // 10.1.36.117
                    ],
                    "labels": {
                        "managed-by": "apisix-ingress-controller"
                    },
                    "type": "roundrobin",
                    "name": "default_httpbin_80",
                    "scheme": "http"
                },
                "key": "\/apisix\/upstreams\/5ce57b8e"
            }
        ],
        "dir": true
    }
}

上述資訊與 Endpointslice 中的網路端點相對應。

addressType: IPv4
apiVersion: discovery.k8s.io/v1
endpoints:
- addresses:
  - 10.1.36.92
  ...
- addresses:
  - 10.1.36.100
  ...
- addresses:
  - 10.1.36.104
  ...
- addresses:
  - 10.1.36.102
  ...
- addresses:
  - 10.1.36.101
  ...
- addresses:
  - 10.1.36.103
  ...
- addresses:
  - 10.1.36.109
  ...
- addresses:
  - 10.1.36.118
  ...
- addresses:
  - 10.1.36.115
  ...
- addresses:
  - 10.1.36.116
  ...
- addresses:
  - 10.1.36.106
  ...
- addresses:
  - 10.1.36.113
  ...
- addresses:
  - 10.1.36.111
  ...
- addresses:
  - 10.1.36.108
  ...
- addresses:
  - 10.1.36.114
  ...
- addresses:
  - 10.1.36.107
  ...
- addresses:
  - 10.1.36.110
  ...
- addresses:
  - 10.1.36.105
  ...
- addresses:
  - 10.1.36.112
  ...
- addresses:
  - 10.1.36.117
  ...
kind: EndpointSlice
metadata:
  labels:
    endpointslice.kubernetes.io/managed-by: endpointslice-controller.k8s.io
    kubernetes.io/service-name: httpbin
  name: httpbin-dkvtr
  namespace: default
ports:
- name: http
  port: 80
  protocol: TCP

總結

本文介紹了 Kubernetes 需要部署大量 Pod 的場景和遇到的問題,對比了 Endpoints 和 Endpointslice 之間區別,以及如何安裝 APISIX Ingress 來使用 Endpointslice 的特性。

如果你的叢集版本為 Kubernetes v1.21+,推薦在安裝 APISIX Ingress 時開啟 Endpointslice 特性支援,這樣就不用關注 --max-endpoints-per-slice 標誌設定的值,從而避免配置丟失。