Topology spread, pod overhead và scheduling readiness

K
Kai··6 min read

Bài 35 cho thấy podAntiAffinity cứng có một điểm yếu: với topologyKey: hostname, mỗi node tối đa một pod — bản sao thứ ba trên cụm hai node là Pending ngay. Đó thường là quá gắt: thực ra ta chỉ muốn pod rải đều, chứ không cấm hai pod chung node. Bài này đào ba cơ chế scheduling tinh hơn, khép phần điều khiển scheduler từ phía pod: topology spread (rải uyển chuyển bằng maxSkew), pod overhead (cộng tài nguyên cho sandbox runtime), và scheduling readiness (giữ pod chưa cho xếp lịch).

Topology spread: rải đều mà không cứng nhắc

Tài liệu: "You can use topology spread constraints to control how Pods are spread across your cluster among failure-domains such as regions, zones, nodes ... This can help to achieve high availability as well as efficient resource utilization." Khác anti-affinity ("cấm chung domain"), topology spread nói "chênh lệch giữa các domain không quá maxSkew". Các trường chính:

  • maxSkew"describes the degree to which Pods may be unevenly distributed" — độ lệch tối đa cho phép giữa domain đông nhất và domain ít nhất.
  • topologyKey — nhãn node định nghĩa "domain" (mỗi cặp <key,value> là một domain).
  • whenUnsatisfiableDoNotSchedule (cứng, mặc định) hay ScheduleAnyway (mềm, "prioritizing nodes that minimize the skew").
  • labelSelector — chọn nhóm pod để tính skew.

Một Deployment 4 bản sao, maxSkew: 1 theo hostname:

spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: DoNotSchedule
    labelSelector: {matchLabels: {app: ts}}
kubectl get pods -l app=ts -o wide | awk '{print $7}' | sort | uniq -c
   2 worker-0
   2 worker-1

Bốn bản sao rải 2 + 2 — skew 0, đều tăm tắp. Khác biệt với Bài 35 nằm ở đây: anti-affinity cứng cấm bản thứ ba (chỉ 1/node, bản 3 Pending), còn topology spread cho nhiều pod mỗi node miễn chênh lệch ≤ maxSkew. Nên 4 bản sao chạy hết, vẫn rải đều. Nếu lệch quá maxSkewwhenUnsatisfiable: DoNotSchedule thì pod mới Pending (giống anti-affinity); đổi sang ScheduleAnyway thì scheduler cố rải nhưng không treo pod. Đây là công cụ rải HA đúng liều — chặt khi cần (DoNotSchedule), nới khi muốn (ScheduleAnyway), thay vì lựa chọn nhị phân của anti-affinity. (Còn minDomains, nodeAffinityPolicy, nodeTaintsPolicy để tinh chỉnh thêm phạm vi tính skew.)

Pod overhead: tài nguyên cho cái sandbox

Mỗi pod tốn tài nguyên ngoài container của nó: cái sandbox (pause container, network namespace) và — với runtime cách ly như Kata Containers / gVisor — cả một lớp ảo hóa nhẹ. Nếu scheduler chỉ tính request của container, nó sẽ đánh giá thấp tài nguyên pod thực sự ngốn. Pod overhead sửa việc đó: gắn một khoản cố định vào pod thông qua RuntimeClass.

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata: {name: overhead-demo}
handler: runc                       # khớp runtime của containerd (Bài 10)
overhead:
  podFixed: {cpu: "100m", memory: "64Mi"}
---
apiVersion: v1
kind: Pod
metadata: {name: oh-pod}
spec:
  runtimeClassName: overhead-demo
  containers:
  - name: c
    image: busybox:1.36
    command: ["sleep","3600"]
    resources: {requests: {cpu: "100m", memory: "64Mi"}}

Pod khai runtimeClassName ấy được admission tiêm spec.overhead:

kubectl get pod oh-pod -o jsonpath='{.spec.overhead}'
{"cpu":"100m","memory":"64Mi"}

Và khoản overhead này được cộng vào khi tính tài nguyên pod cho scheduling và accounting. Kiểm chứng trên node nơi pod chạy:

kubectl describe node worker-1 | sed -n '/Allocated resources/,/Events/p' | grep -iE 'cpu|memory'
  cpu     300m (15%)
  memory  198Mi (5%)

Bóc tách: oh-pod góp 100m (container) + 100m (overhead) = 200m CPU và 64Mi + 64Mi = 128Mi memory; cộng CoreDNS thường trú trên node (100m/70Mi) ra đúng 300m/198Mi. Overhead thực sự được tính — scheduler biết pod chiếm 200m chứ không phải 100m. Với runtime sandbox nặng (Kata/gVisor) khoản này lớn hơn nhiều, và bỏ qua nó sẽ khiến node bị nhồi quá tải. (Cụm ta dùng runc nên overhead thật gần 0; ở đây ta giả lập một khoản để thấy cơ chế.)

Scheduling readiness: giữ pod chưa cho xếp lịch

Đôi khi pod đã tạo nhưng chưa nên được xếp lịch — đang chờ một tài nguyên ngoài (quota được duyệt, một Secret được tạo, một bước chuẩn bị xong). Để pod đó vào hàng đợi scheduler ngay sẽ làm scheduler (và Cluster Autoscaler) quay vòng vô ích trên một pod chắc chắn Pending. schedulingGates giải việc này. Tài liệu: "By specifying/removing a Pod's .spec.schedulingGates, you can control when a Pod is ready to be considered for scheduling."

apiVersion: v1
kind: Pod
metadata: {name: gated}
spec:
  schedulingGates:
  - {name: kkloud.io/wait-for-config}
  containers: [{name: c, image: busybox:1.36, command: ["sleep","3600"]}]
kubectl get pod gated -o wide
kubectl get pod gated -o jsonpath='{.spec.schedulingGates}'
NAME    READY   STATUS            NODE
gated   0/1     SchedulingGated   <none>

[{"name":"kkloud.io/wait-for-config"}]

Pod ở trạng thái đặc biệt SchedulingGated — scheduler bỏ qua nó hoàn toàn (nodeName rỗng, không có cả event FailedScheduling vì scheduler còn chưa xét tới). Khác với Pending (scheduler đã xét nhưng không xếp được), SchedulingGated là "chưa tới lượt xét". Khi điều kiện ngoài đã xong, một controller (hoặc ta) gỡ gate — lưu ý quy tắc: "each schedulingGate can be removed ... but addition of a new scheduling gate is disallowed" (chỉ gỡ, không thêm sau khi tạo):

kubectl patch pod gated --type=json -p='[{"op":"remove","path":"/spec/schedulingGates"}]'
kubectl get pod gated -o wide
NAME    READY   STATUS    NODE
gated   1/1     Running   worker-1

Gate rỗng, scheduler lập tức nhận pod và bind. Đây là cái "van" để một hệ thống bên ngoài kiểm soát thời điểm pod bước vào scheduler — nền cho các pattern như gang scheduling (chờ đủ cả nhóm), hay chờ tài nguyên đặc thù sẵn sàng, mà không làm scheduler bận rộn vô ích.

🧹 Dọn dẹp

kubectl delete deployment ts
kubectl delete pod gated oh-pod --now
kubectl delete runtimeclass overhead-demo

Object trong cluster, xóa là sạch. Cụm về lại hai pod CoreDNS, hai node Ready. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 36-topology-overhead-gates.

Tổng kết

Ba cơ chế scheduling tinh. Topology spread rải pod theo maxSkew — chênh lệch giữa domain đông và ít nhất không quá ngưỡng (ta thấy 4 bản sao → 2+2 đều, cho nhiều pod/node, khác anti-affinity cứng của Bài 35 chỉ 1/node); whenUnsatisfiable chọn cứng (DoNotSchedule) hay mềm (ScheduleAnyway). Pod overhead qua RuntimeClass cộng một khoản cố định (podFixed) cho sandbox/runtime vào tài nguyên pod — ta thấy oh-pod đóng góp 200m CPU (100 container + 100 overhead) vào node, đúng accounting. schedulingGates giữ pod ở trạng thái SchedulingGated (scheduler bỏ qua, khác Pending) tới khi gate được gỡ — van để hệ thống ngoài kiểm soát thời điểm pod vào scheduler. Cùng affinity/taint (Bài 35), đây là bộ điều khiển scheduler đầy đủ từ phía pod.

Bài 37 sang priority và preemption: pod có priorityClassName cao có thể trục xuất pod ưu tiên thấp khi node hết chỗ — chính cái bước PostFilter ("Preemption is not helpful") mà ta thấy ở Bài 34, giờ làm cho nó helpful.