Try   HackMD

實測 API Server 會 cache K8s metadata

API Server data flow

API Server 可以視為在 etcd 前面的一个代理(proxy)

  +--------+              +---------------+                 +------------+
  | Client | -----------> | Proxy (cache) | --------------> | Data store |
  +--------+              +---------------+                 +------------+

infra services               apiserver                         etcd

絕大部分情況下,Api Server 直接從本地快取提供服務(因為它快取了叢集全量資料);

某些特殊情況,例如如下比較常見的 2 種:

  1. 客戶端明確要求從 etcd 讀取資料(追求最高的資料準確性),
  2. apiserver 本地快取還沒建好
    apiserver 就只能將請求轉寄給 etcd, 這裡就要特別注意了客戶端 LIST 參數設定不當也可能會走到這個邏輯

常見的 List 請求可歸類為兩種:

  1. List 全量資料:開銷主要花在資料傳輸;
  2. List 指定用 label 或欄位(field)過濾,只需要匹配的資料。

這裡要特別說明的是第二種情況,也就是 list 請求帶了過濾條件,不要以為 list 帶了過濾條件就不會全量查詢 etcd ,也分兩種情況:

  1. 大部分情況下,apiserver 會用自己的快取做過濾,走快取的操作很快,直接從 apiserver 的記憶體就可以返回,因此耗時主要花在資料網路傳輸
  2. 需要將請求轉給 etcd 的情況

注意,etcd 只是 Key/Value(KV) 存儲,並不理解 label/field 訊息,因此在 etcd 層面無法處理過濾請求。 實際的過程是:apiserver 從 etcd 拉全量數據,然後在記憶體做過濾,再回傳給客戶端。因此除了資料傳輸開銷(網路頻寬),這種情況下還會佔用大量 apiserver CPU 和記憶體

以幾個常見的 LIST 請求為例:

  1. LIST api/v1/pods?limit=500&timeout=30s&resourceVersion=0

這個請求是獲取 K8s 上的所有 pods

這裡同時傳了兩個參數,但 resourceVersion=0 會導致 apiserver 忽略 limit=500, 所以客戶端拿到的是全量 pods 的資料。

但由於指定了 resourceVersion=0, 所以雖然是全量數據,但是會直接從 apiserver 的快取中返回。

  1. LIST api/v1/pods?fieldSelector=spec.nodeName%3Dtesting

這個請求是獲取 testing node 上的所有 pods%3D=的轉義)。

根據 nodename 做過濾,給人的感覺可能是資料量不太大,但其實背後要比看起來複雜:

首先,這裡沒有指定 resourceVersion=0,導致 apiserver 跳過緩存,直接去 etcd 讀資料; 其次,etcd 只是 KV 存儲,沒有按 label/field 過濾功能(只處理limit/continue),所以 apiserver 是從 etcd 拉全量的 pods 數據,然後在記憶體做 fieldselector 過濾,開銷也是很大的,這種行為是要避免的,除非對資料準確度有極高要求,特意要繞過 apiserver 快取。

  1. LIST api/v1/pods?filedSelector=spec.nodeName%3Dnode1&resourceVersion=0

跟 2 的差別是加上了 resourceVersion=0,因此 apiserver 會從快取讀取數據,效能會有量級的提升。

但要注意,雖然實際上回傳給客戶端的可能只有幾百 KB 到上百 MB , 但 apiserver 需要處理的資料量可能是幾個 GB。

以上可以看到,不同的 LIST 操作產生的影響是不一樣的,而客戶端看到資料還有可能只是 apiserver/etcd 處理資料的一小部分。如果基礎服務大規模啟動或重啟, 就極有可能把控制平面打爆。

總結一下

resourceVersion 作用: 保證客戶端資料一致性與順序性,樂觀鎖,實現並發控制

設定 ListOptions 時,resourceVersion 有三種設定方法:

  1. 不設定(不傳遞 ListOptions 或不設定 resourceVersion 欄位),此時會直接從 etcd 讀取,此時資料是最新的
  2. 設定為0,此時會從 API Server Cache 中取得數據
  3. 設定為指定的 resourceVersion,取得 resourceVersion 大於指定版本的所有資源對象

開始實作

# 進到 Control-plane node
$ ssh <contol-plane node>

# 檢視原本 default namespace 有的 pods
$ kubectl get pods
NAME                                 READY   STATUS    RESTARTS        AGE
nginx-5c9848d864-t7mj7               1/1     Running   2 (7h43m ago)   8d
synergy-leverager-5469477c9d-flrst   0/1     Error     0               8d
synergy-leverager-5469477c9d-gs6sv   1/1     Running   2 (7h43m ago)   8d

# 將 etcd manifests 移出 static pod 目錄區
$ mv /etc/kubernetes/manifests/etcd.yaml ~

# 確認 etcd 去世
$ crictl ps --name etcd -a
ONTAINER           IMAGE               CREATED             STATE               NAME                ATTEMPT             POD ID              POD                 NAMESPACE
56e144e375402       a9e7e6b294baf       5 minutes ago       Exited              etcd                0                   0353cbdc58e9a       etcd-cka            kube-system

# 確認 api/v1/namespaces/kube-system/pods?limit=500 這 API Call 要跟 ETCD 拿資料的話,會得不到資料
$ kubectl get pods --field-selector=spec.nodeName=testing --request-timeout=3s -v 6
I0517 17:01:27.466943  686541 loader.go:402] Config loaded from file:  /root/.kube/config
I0517 17:01:27.467316  686541 envvar.go:172] "Feature gate default state" feature="ClientsAllowCBOR" enabled=false
I0517 17:01:27.467357  686541 envvar.go:172] "Feature gate default state" feature="ClientsPreferCBOR" enabled=false
I0517 17:01:27.467363  686541 envvar.go:172] "Feature gate default state" feature="InformerResourceVersion" enabled=false
I0517 17:01:27.467367  686541 envvar.go:172] "Feature gate default state" feature="WatchListClient" enabled=false
I0517 17:01:30.474745  686541 round_trippers.go:560] GET https://172.16.8.121:6443/api/v1/namespaces/default/pods?fieldSelector=spec.nodeName%3Dtesting&limit=500&timeout=3s  in 3002 milliseconds
I0517 17:01:30.474847  686541 helpers.go:264] Connection error: Get https://172.16.8.121:6443/api/v1/namespaces/default/pods?fieldSelector=spec.nodeName%3Dtesting&limit=500&timeout=3s: context deadline exceeded (Client.Timeout exceeded while awaiting headers)
Unable to connect to the server: context deadline exceeded (Client.Timeout exceeded while awaiting headers)

# 確認添加 resourceVersion=0 參數一定會跟 API Server 本地的 cache 拿資料
$ kubectl get --raw '/api/v1/namespaces/default/pods?resourceVersion=0' | jq
...
        "containers": [
          {
            "name": "synergy-leverager",
            "image": "quay.io/flysangel/library/busybox",
            "command": [
              "/bin/sh"
            ],
            "args": [
              "-c",
              "i=0;\nmkdir -p /var/log;\nwhile true;\ndo\n  echo \"$(date) INFO $i\" | tee -a /var/log/synergy-leverager.log;\n  i=$((i+1));\n  sleep 1;\ndone\n"
            ],
            "resources": {},
            "volumeMounts": [
              {
                "name": "kube-api-access-tm6cz",
                "readOnly": true,
                "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
              }
            ],
            "terminationMessagePath": "/dev/termination-log",
            "terminationMessagePolicy": "File",
            "imagePullPolicy": "Always"
          }
        ],
        "restartPolicy": "Always",
        "terminationGracePeriodSeconds": 30,
        "dnsPolicy": "ClusterFirst",
        "serviceAccountName": "default",
        "serviceAccount": "default",
        "nodeName": "cka",
        "securityContext": {},
        "schedulerName": "default-scheduler",
        "tolerations": [
          {
            "key": "node.kubernetes.io/not-ready",
            "operator": "Exists",
            "effect": "NoExecute",
            "tolerationSeconds": 300
          },
          {
            "key": "node.kubernetes.io/unreachable",
            "operator": "Exists",
            "effect": "NoExecute",
            "tolerationSeconds": 300
          }
        ],
        "priority": 0,
        "enableServiceLinks": true,
        "preemptionPolicy": "PreemptLowerPriority"
      },
      "status": {
        "phase": "Running",
        "conditions": [
          {
            "type": "PodReadyToStartContainers",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2025-05-17T01:08:12Z"
          },
          {
            "type": "Initialized",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2025-05-08T17:22:59Z"
          },
          {
            "type": "Ready",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2025-05-17T01:08:12Z"
          },
          {
            "type": "ContainersReady",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2025-05-17T01:08:12Z"
          },
          {
            "type": "PodScheduled",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2025-05-08T17:22:59Z"
          }
        ],
        "hostIP": "172.16.8.121",
        "hostIPs": [
          {
            "ip": "172.16.8.121"
          }
        ],
        "podIP": "10.244.16.78",
        "podIPs": [
          {
            "ip": "10.244.16.78"
          }
        ],
        "startTime": "2025-05-08T17:22:59Z",
        "containerStatuses": [
          {
            "name": "synergy-leverager",
            "state": {
              "running": {
                "startedAt": "2025-05-17T01:08:11Z"
              }
            },
            "lastState": {
              "terminated": {
                "exitCode": 255,
                "reason": "Unknown",
                "startedAt": "2025-05-10T01:14:26Z",
                "finishedAt": "2025-05-17T01:07:21Z",
                "containerID": "containerd://01341ba2dccb4cee04f8429bf91840e2c7d39dc28f2eaa01ac20c0df86ae8aef"
              }
            },
            "ready": true,
            "restartCount": 2,
            "image": "quay.io/flysangel/library/busybox:latest",
            "imageID": "quay.io/flysangel/library/busybox@sha256:ad9fa4d07136a83e69a54ef00102f579d04eba431932de3b0f098cc5d5948f9f",
            "containerID": "containerd://8e48c93e8098ce2e77e6a48ae1fe218ee18be6397b5198bf7b7bbe0939d72308",
            "started": true,
            "volumeMounts": [
              {
                "name": "kube-api-access-tm6cz",
                "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
                "readOnly": true,
                "recursiveReadOnly": "Disabled"
              }
            ]
          }
        ],
        "qosClass": "BestEffort"
      }
    }
  ]
}

REf