前情提要

前幾天在開發 KubeRay 專案的時候,從 issue 的留言區 學到了一個 Kubernetes 的知識,原來 Eviction 還有分 Node-pressure EvictionAPI-initiated Eviction 兩種。API-initiated Eviction 是直接 call API,或是使用像是 kubectl drain 之類的指令,特性是不管怎樣最終以這種方式被 Evict 的 Pod 會被 delete 掉,通常就會在另一個 node 上被重新 create。但是如果是 Node-pressure Eviction 的話,kubelet 只會把 Pod 的 Phase 設成 Failed,而不會把 Pod delete 掉,所以 controller 沒有特別處理的話,Pod 就不會在另一個 node 上面被重新 create。

現在簡述一下這個 issue 的問題:當 KubeRay operator 創出來的某個 Pod 的所在的 node 如果遇到 disk 容量不夠,該 Pod 會被 Evict,稍後 disk 容量被清出來之後,該 Pod 仍然處於 Failed 狀態,也沒有被重新 create 在其他 node 上。

現在我需要 reproduce 這個 issue,但重點來了,既然這兩種 Eviction 的行為不同,這表示我不能直接用 kubectl drain 之類的指令去 reproduce 出這個 issue 的情境,必須確確實實的弄出 Node-pressure Eviction。但是我也沒有 cluster 可以使用,我都是用個人電腦開發的,這導致我很難 reproduce 出這個 issue。原因是如果是在本機開發 Kubernetes 相關的應用程式,大部分會使用 minikubekind 或是 k3d 。我要模擬的情境必須要有多個 node,因此先排出 minikube,雖然 它現在也支援 multiple nodes 了,但是它還是比較常用來開發 single node 的情境。而 kind 和 k3d 上使用 Docker container 去當作 Kubernetes nodes,而我的作業系統是 Linux Mint,Docker 是原生的 Docker,不像 macOS 的 Docker 是跑在虛擬機裡的,如果我要搞出 Node-pressure 的情境,因為資源(memory、disk 等)都是跟本機共用的,所以我如果真的把某個 node 搞到有 pressure 我的電腦大概也不能用了。

經過瘋狂 Google 過後,我發現 Docker 可以設定 Runtime 的 Memory 限制 ,還有 k3d 有個 –agents-memory 的 flag 可以設定 agent node 的 memory ,我才找到 重現這個 issue 的辦法

步驟

首先創一個 k3d cluster,有 2 個 agent nodes,每個 agent node 有 3GB 的 memory,並且在剩餘可用 memory 少於 1GiB 的時候就會開始觸發 Pod Eviction。

1
2
3
4
5
6
k3d cluster create \
  --agents 2 \
  --k3s - arg "--disable=traefik@server:0" \
  --agents - memory 3g \
  --k3s - arg "--kubelet-arg=eviction-hard=memory.available<1Gi@agent:0" \
  --k3s - arg "--kubelet-arg=eviction-hard=memory.available<1Gi@agent:1"

檢查所有 Node 的記憶體

1
kubectl get nodes -o custom-columns=NAME:.metadata.name,CAPACITY_MEMORY:.status.capacity.memory,ALLOCATABLE_MEMORY:.status.allocatable.memory

輸出:

# NAME                       CAPACITY_MEMORY   ALLOCATABLE_MEMORY
# k3d-k3s-default-agent-1    3221225Ki         2172649Ki
# k3d-k3s-default-agent-0    3221225Ki         2172649Ki
# k3d-k3s-default-server-0   32590664Ki        32590664Ki

可以看到 agent 0 和 agent 1 都有 3GB 的記憶體,但可以 allocate 的記憶體是 2GB,因為剩餘記憶體不足 1GiB 的時候就會觸發 Pod Eviction。

接著把 agent 0 和 agent 1 上面加上 taint,這樣之後的 Pod 都只會被 deploy 到 server-0 這個 node 上面。

1
2
kubectl taint nodes k3d-k3s-default-agent-0 k3d=noschedule:NoSchedule
kubectl taint nodes k3d-k3s-default-agent-1 k3d=noschedule:NoSchedule

安裝 KubeRay operator,這時 operator Pod 會跑在 server-0 這個 node 上。

1
helm install kuberay-operator kuberay/kuberay-operator --namespace ray-system --version 1.1.1 --create-namespace

移除 agent 0 和 agent 1 的 taint,為 server 0 加上 taint,這樣之後的 Pod 就不會被 deploy 到 server 0 上面。

1
2
3
kubectl taint nodes k3d-k3s-default-server-0 k3d=noschedule:NoSchedule
kubectl taint nodes k3d-k3s-default-agent-0 k3d=noschedule:NoSchedule-
kubectl taint nodes k3d-k3s-default-agent-1 k3d=noschedule:NoSchedule-

安裝 RayCluster custom resource,安裝完後 KubeRay operator 會創出一個 head pod 和一個 worker pod,因為在 helm chart 裡面 head pod 的 memory resource request 是 2G,worker pod 是 1G,而 agent 0 和 agent 1 都只有 2G 的可分配記憶體,所以這兩個 Pod 一定不會在同一個 node 上面。

1
helm install raycluster kuberay/ray-cluster --version 1.1.1

接下來要進入 head pod 所在的 node 裡面執行記憶體壓力測試,一樣 Google 完發現大家常用的是 stress-ng ,所以就用它。我們必須讓 head pod 裡面有 stress-ng 可以用,最簡單的方式是把靜態編譯過的 stress-ng 直接複製到 head pod 裡面,這樣就不用管 head pod 的 base image 是什麼、缺了什麼 dependency 之類的。至於要怎麼取得靜態編譯過的執行檔,可以自己編譯,但我比較懶,我直接去把 一個裡面帶有靜態編譯執行檔的 docker image 裡面的執行檔複製出來 。假設 head pod 的名字是 raycluster-kuberay-head-ldg9f

1
kubectl cp ./stress-ng raycluster-kuberay-head-ldg9f:/home/ray

開一個在 head pod 上面的 shell

1
kubectl exec -it raycluster-kuberay-head-ldg9f -- bash

模擬記憶體不足情形

1
./stress-ng --vm 4 --vm-bytes 2G --vm-keep

如此一來就可以看到 head pod 被 Evict 掉,而且是 Node-pressure Eviction。