---
# System prepended metadata

title: 實測 API Server 會 cache K8s metadata

---

# 實測 API Server 會 cache K8s metadata

<style>
.indent-title-1{
  margin-left: 1em;
}
.indent-title-2{
  margin-left: 2em;
}
.indent-title-3{
  margin-left: 3em;
}
</style>

## API Server data flow

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

```shell!
  +--------+              +---------------+                 +------------+
  | 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`

<div class="indent-title-2">

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

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

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

</div>


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

<div class="indent-title-2">

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

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

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

</div>

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

<div class="indent-title-2">

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

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

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

</div>

### 總結一下 

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

設定 ListOptions 時，resourceVersion 有三種設定方法：

1. 不設定(不傳遞 ListOptions 或不設定 resourceVersion 欄位)，此時會直接從 etcd 讀取，此時資料是最新的
1. 設定為`0`，此時會從 API Server Cache 中取得數據
1. 設定為指定的 `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

- https://izsk.me/2023/12/14/Kubernetes-listOption-1/

