Disruption và PodDisruptionBudget
Bài 22 chạm tới chuyện pod bị evict khi node cạn tài nguyên. Đó chỉ là một trong nhiều cách pod biến mất. Bài này khép Part III bằng cái nhìn hệ thống về disruption (pod ngừng chạy) và công cụ duy nhất giúp ta kiểm soát được một phần của nó: PodDisruptionBudget. Mấu chốt cả bài nằm ở chỗ phân biệt hai kiểu disruption, vì PDB chỉ canh được đúng một kiểu.
Hai kiểu disruption
Tài liệu chia disruption làm hai nhóm theo việc có ai chủ động gây ra hay không.
Không tự nguyện (involuntary) — những thứ ập đến ngoài ý muốn, trích nguyên văn danh sách:
"a hardware failure of the physical machine backing the node; cluster administrator deletes VM (instance) by mistake; cloud provider or hypervisor failure makes VM disappear; a kernel panic; the node disappears from the cluster due to cluster network partition; eviction of a pod due to the node being out-of-resources."
Cú OOM/eviction do thiếu RAM ở Bài 22 nằm đúng nhóm này. Không ai quyết định cho những việc đó xảy ra; cách phòng là dàn bản sao ra nhiều node, nhiều vùng, để một node chết không kéo sập cả dịch vụ.
Tự nguyện (voluntary) — do chính người vận hành hay quản trị cluster khởi xướng:
"deleting the deployment or other controller that manages the pod; updating a deployment's pod template causing a restart; directly deleting a pod" và phía quản trị: "Draining a node for repair or upgrade; Draining a node from a cluster to scale the cluster down; Removing a pod from a node to permit something else to fit on that node."
Việc rút node đi bảo trì hay nâng cấp (chính là kubectl drain, ta sẽ dùng thật ở bài upgrade cluster) là disruption tự nguyện điển hình. Vì nó chủ động, ta có thể đặt luật cho nó, và đó là việc của PDB.
PodDisruptionBudget chỉ canh disruption tự nguyện
Định nghĩa gọn của tài liệu:
"A PDB limits the number of Pods of a replicated application that are down simultaneously from voluntary disruptions."
Đọc kỹ chữ "voluntary": PDB không ngăn được node chết hay OOM. Tài liệu nói thẳng:
"Involuntary disruptions cannot be prevented by PDBs; however they do count against the budget."
Tức node chết vẫn làm pod sập như thường, PDB bất lực, nó chỉ tính cú đó vào ngân sách. Cái PDB thực sự làm là chặn các công cụ rút pod tự nguyện (như kubectl drain) khi việc rút sẽ kéo số bản sao khỏe xuống dưới mức cho phép. Cơ chế: các công cụ đó không xóa thẳng pod mà gọi Eviction API, và PDB gác ngay tại API đó.
PDB có ba trường chính: selector (chọn pod nào), và một trong hai minAvailable / maxUnavailable. Tài liệu nhấn: "You can specify only one of maxUnavailable and minAvailable". minAvailable: 2 nghĩa là "luôn phải còn ít nhất 2 bản sao khỏe"; với Deployment 3 bản sao thì điều đó cho phép rút tối đa 1 cái cùng lúc.
Dựng và quan sát một PDB
Deploy 3 bản sao kèm một PDB minAvailable: 2:
apiVersion: apps/v1
kind: Deployment
metadata: {name: pdb-demo}
spec:
replicas: 3
selector: {matchLabels: {app: pdb-demo}}
template:
metadata: {labels: {app: pdb-demo}}
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep","3600"]
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: {name: pdb-demo}
spec:
minAvailable: 2
selector: {matchLabels: {app: pdb-demo}}
kubectl get pods -l app=pdb-demo
kubectl get pdb pdb-demo
kubectl get pdb pdb-demo -o jsonpath='currentHealthy={.status.currentHealthy} desiredHealthy={.status.desiredHealthy} disruptionsAllowed={.status.disruptionsAllowed} expectedPods={.status.expectedPods}{"\n"}'
NAME READY STATUS RESTARTS AGE
pdb-demo-84645454b8-km7bc 1/1 Running 0 9s
pdb-demo-84645454b8-t9ks9 1/1 Running 0 9s
pdb-demo-84645454b8-zpr9z 1/1 Running 0 9s
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
pdb-demo 2 N/A 1 9s
currentHealthy=3 desiredHealthy=2 disruptionsAllowed=1 expectedPods=3
Đọc status như đọc một bài toán: expectedPods=3 (Deployment muốn 3), desiredHealthy=2 (PDB đòi tối thiểu 2), currentHealthy=3 (đang có 3 khỏe), nên disruptionsAllowed=1, tức được phép rút 1 cái mà vẫn còn ≥2. Cột ALLOWED DISRUPTIONS ở kubectl get pdb chính là con số này. Nó là cái cân động: rút một cái thì nó tụt về 0 cho tới khi có bản sao khỏe bù vào.
Eviction API và cú HTTP 429
Giờ kiểm chứng PDB chặn thật. Thay vì kubectl drain cả node (sẽ động tới cả pod hệ thống), ta gọi thẳng Eviction API cho từng pod, đây đúng là API mà drain dùng bên dưới. Một yêu cầu evict là một object Eviction POST tới subresource .../pods/<tên>/eviction. Thử evict hai pod liên tiếp:
P1=pdb-demo-84645454b8-km7bc ; P2=pdb-demo-84645454b8-t9ks9
echo '{"apiVersion":"policy/v1","kind":"Eviction","metadata":{"name":"'$P1'","namespace":"default"}}' > /tmp/ev1.json
echo '{"apiVersion":"policy/v1","kind":"Eviction","metadata":{"name":"'$P2'","namespace":"default"}}' > /tmp/ev2.json
kubectl create --raw /api/v1/namespaces/default/pods/$P1/eviction -f /tmp/ev1.json
kubectl create --raw /api/v1/namespaces/default/pods/$P2/eviction -f /tmp/ev2.json
# evict #1:
{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Success","code":201}
# evict #2 (ngay sau đó):
Error from server (TooManyRequests): Cannot evict pod as it would violate the pod's disruption budget.
Đây là cả bài học gói trong hai dòng. Cú evict #1 thành công (code 201) vì lúc đó disruptionsAllowed=1: rút 1 cái còn lại 2, vẫn đủ. Ngay sau đó budget tụt về 0, nên cú evict #2 bị từ chối với TooManyRequests, tức HTTP 429 mà tài liệu mô tả: khi disruptionsAllowed=0, Eviction API trả 429 thay vì cho rút. Thông điệp nói thẳng lý do: "Cannot evict pod as it would violate the pod's disruption budget." Nếu đây là một node đang drain, lệnh drain sẽ dừng lại chờ tại đây thay vì rút bừa cái thứ hai, đúng thứ ta muốn cho một dịch vụ cần luôn còn ≥2 bản sao.
So sánh với việc xóa thẳng: kubectl delete pod không qua Eviction API nên không bị PDB chặn (tài liệu: "deleting deployments or pods bypasses Pod Disruption Budgets"). PDB không phải khóa chống xóa; nó là luật cho các công cụ biết điều gọi qua Eviction API.
Bù bản sao và budget hồi phục
Sau cú evict #1, controller của Deployment (Bài 24 sẽ đào kỹ) lập tức nhận ra thiếu pod và tạo bản thay thế:
kubectl get pods -l app=pdb-demo
kubectl get pdb pdb-demo -o jsonpath='disruptionsAllowed={.status.disruptionsAllowed} currentHealthy={.status.currentHealthy}{"\n"}'
NAME READY STATUS RESTARTS AGE
pdb-demo-84645454b8-km7bc 1/1 Terminating 0 36s
pdb-demo-84645454b8-qjnvk 1/1 Running 0 13s <- bản bù
pdb-demo-84645454b8-t9ks9 1/1 Running 0 36s
pdb-demo-84645454b8-zpr9z 1/1 Running 0 36s
disruptionsAllowed=1 currentHealthy=3
Pod km7bc đang Terminating, nhưng qjnvk đã lên thay và currentHealthy quay về 3 → disruptionsAllowed lại là 1. Đây là vòng: rút một cái → budget về 0 → controller bù → bản bù khỏe → budget về 1 → mới được rút tiếp. Một kubectl drain tôn trọng PDB sẽ đi đúng nhịp này, rút từng pod một và chờ bản bù khỏe giữa mỗi lần, nên dịch vụ không bao giờ tụt dưới 2 bản sao trong suốt quá trình bảo trì node. Lưu ý từ tài liệu: bản thân Deployment/StatefulSet khi rolling update thì không bị PDB giới hạn, PDB chỉ gác đường Eviction API.
🧹 Dọn dẹp
kubectl delete deployment pdb-demo
kubectl delete pdb pdb-demo
Xóa Deployment kéo theo toàn bộ pod; xóa PDB là object cuối. Cụm về lại hai pod CoreDNS. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 23-pdb.
Tổng kết
Pod biến mất theo hai kiểu: không tự nguyện (node chết, kernel panic, OOM, không ngăn được, chỉ phòng bằng cách rải bản sao) và tự nguyện (xóa controller, hay quản trị drain node để bảo trì/nâng cấp). PodDisruptionBudget chỉ canh kiểu tự nguyện, và canh tại Eviction API: với minAvailable: 2 trên Deployment 3 bản sao, disruptionsAllowed là 1; evict cái đầu thành công (201), evict cái thứ hai ngay sau đó bị chặn bằng HTTP 429 "would violate the pod's disruption budget"; controller bù bản sao, budget hồi về 1 rồi mới cho rút tiếp. PDB không chặn kubectl delete thẳng và không giới hạn rolling update; nó là luật để công cụ rút node biết dừng đúng lúc, giữ dịch vụ luôn đủ bản sao khỏe khi bảo trì.
Hết Part III. Ba bài tới gom thành Part IV về controller: bắt đầu ở Bài 24 với Deployment, đào cơ chế rollout/rollback và vai trò ReplicaSet đứng giữa (ta đã thoáng thấy nó tạo pod bù ở bài này), rồi tới StatefulSet, DaemonSet, Job/CronJob.