Có một cám dỗ rất lớn khi viết về hiệu năng: đo được phần dễ, rồi lấp phần khó bằng một con số mượn ở đâu đó và trình bày như thể mình đã đo. Bài này tôi cố tình không làm vậy — và chính chỗ không đo được lại dạy ra bài học đáng giá nhất. HPA thêm pod; Cluster Autoscaler thêm node khi không còn chỗ cho pod. Tôi đo hai đầu của vòng lặp đó trên cụm thật, và nói rõ khúc giữa vì sao tôi để trống.

Tín hiệu kích hoạt: pod Pending vì thiếu tài nguyên

6 pod × 10 CPU trên cụm 3 worker × 16 CPU:

pod đầu tiên vào Pending sau       0,1 giây
event FailedScheduling sau         0,2 giây
kết quả:  3 đang tạo,  3 Pending

0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 Insufficient cpu

Hai phần mười giây để tín hiệu xuất hiện. Đây chính là thứ Cluster Autoscaler theo dõi: pod Pending vì không node nào đủ chỗ.

Và đó là một điều kiện hẹp hơn nhiều người nghĩ. Pod Pending vì vết (taint) hoặc nodeSelector không khớp thì thêm node không giải quyết được gì, nên Cluster Autoscaler bỏ qua chúng. Thông báo ở trên nêu rõ cả hai loại: 1 node(s) had untolerated taint(s) và 3 Insufficient cpu. Chỉ vế thứ hai kích hoạt việc thêm node. Hệ quả thực dụng: nếu pod nằm Pending mãi mà cụm không lớn thêm, hãy đọc dòng FailedScheduling trước. Lý do thường không phải "hết tài nguyên".

Chiều thu gọn: cordon rồi drain

trước:  1 sc-worker   1 sc-worker2   1 sc-worker3

kubectl cordon + drain sc-worker3   ->  14,3 giây

sc-worker3   Ready,SchedulingDisabled
sau:  2 sc-worker   1 sc-worker2

Pod trên node bị drain được xếp lại ở node khác, và cả quá trình mất 14,3 giây. Đây đúng là hai bước Cluster Autoscaler thực hiện trước khi trả node về cho nhà cung cấp:

  1. cordon — đánh dấu node không nhận pod mới. Tức thì.
  2. drain — đuổi pod hiện có, tôn trọng PodDisruptionBudget.

PodDisruptionBudget là chỗ việc thu gọn hay bị kẹt. Một PDB minAvailable: 3 trên deployment 3 bản khiến drain không bao giờ xong — Cluster Autoscaler sẽ thử mãi và node không bao giờ được trả về. Đây là nguyên nhân phổ biến nhất của "cụm không bao giờ thu gọn, tôi trả tiền cho node rỗng".

Phần tôi không đo được

Việc thật sự tạo ra một máy mới cần nhà cung cấp đám mây. kind chạy node bằng container Docker và không có API để Cluster Autoscaler gọi, nên tôi không dựng được phép đo đầu-cuối. Hai con số quan trọng nhất — thời gian nhà cung cấp cấp máy, và thời gian node mới gia nhập cụm — tôi không có số đo riêng, và không mượn số của người khác rồi gọi đó là phép đo.

Cái đo được và đáng nhớ: hai đầu của vòng lặp đều rất nhanh — 0,2 giây để sinh tín hiệu, 14,3 giây để dọn một node. Toàn bộ thời gian chờ nằm ở khúc giữa, tức phần thuộc về nhà cung cấp hạ tầng. Nghĩa là khi tối ưu thời gian phản ứng của cụm, đừng nhìn vào Kubernetes — nhìn vào thời gian khởi động máy và thời gian kéo ảnh (phần 48 đo được 6–16 giây cho ảnh).

Ba cơ chế co giãn, ba tầng khác nhau

Đối tượng Điều kiện
HPA (phần 66) số pod chỉ số vượt ngưỡng
VPA (phần 67) requests của pod mức dùng thật khác requests
Cluster Autoscaler số node pod Pending vì hết chỗ

Chúng hoạt động nối tiếp: HPA thêm pod → không đủ chỗ → pod Pending → Cluster Autoscaler thêm node → pod được xếp. Nên thời gian phản ứng đầu-cuối của một đỉnh tải là tổng của cả chuỗi: 30 giây HPA (phần 66) cộng thời gian cấp máy cộng thời gian kéo ảnh.

Với đỉnh tải nhanh, chuỗi này quá chậm. Cách xử lý thông dụng là pod giữ chỗ: chạy vài pod ưu tiên thấp không làm gì, chiếm sẵn tài nguyên. Khi tải thật tới, pod ưu tiên cao đuổi chúng đi (phần 47 đã đo cơ chế đuổi này) và chạy ngay, trong khi Cluster Autoscaler thong thả cấp node mới cho pod giữ chỗ.

Bốn thứ chặn việc thu gọn

Cluster Autoscaler không xoá node nếu trên đó còn:

  • Pod không có bộ điều khiển — pod trần tạo bằng kubectl run, không thuộc Deployment hay Job nào.
  • Pod dùng lưu trữ cục bộ (emptyDir) mà không khai cho phép mất.
  • Pod bị PodDisruptionBudget chặn.
  • Pod trong kube-system không có PDB.

Ba cái đầu đều nằm trong tay bạn, và cả ba đều là lý do thật khiến hoá đơn không giảm khi tải giảm.

metadata:
  annotations:
    cluster-autoscaler.kubernetes.io/safe-to-evict: "true"

Chú thích này nói với Cluster Autoscaler rằng pod chuyển đi được, kể cả khi nó dùng emptyDir. Chiều ngược lại — "false" — ghim một pod tại chỗ, và một pod như vậy giữ nguyên cả node.

Mẫu số chung: đo trung thực phần đo được, và độ trễ luôn nằm ở khúc bạn không nhìn

Bài này gói hai kỷ luật mà tôi coi trọng hơn bất kỳ con số nào trong đó. Thứ nhất là sự trung thực khi đo: đo hết phần đo được, gọi tên rõ phần không đo được, và tuyệt đối không mượn một con số rồi trình bày như của mình. Tôi có 0,2 giây và 14,3 giây thật; khúc giữa — nhà cung cấp cấp máy — tôi để trống chứ không lấp bằng số vay mượn. Đây là ranh giới giữa một phép đo và một câu chuyện: một benchmark chỉ chạy đường nhanh rồi kết luận cho cả hệ, một con số đo trên máy tí hon rồi ngoại suy lên quy mô production, đều là cùng một cái bẫy. Một phép đo bạn chưa thực hiện không phải là một phép đo — và nói thẳng "chỗ này tôi chưa đo" làm phần còn lại đáng tin hơn, không kém đi.

Thứ hai, bài học rơi ra từ chính chỗ trống đó: độ trễ (và chi phí) gần như luôn nằm ở khúc bạn không kiểm soát và không nhìn tới. Hai đầu của vòng lặp Kubernetes đều nhanh; toàn bộ sự chờ nằm ở nhà cung cấp hạ tầng. Đây đúng là định luật Amdahl mặc áo vận hành: tối ưu phần đã nhanh sẵn (0,2 giây của scheduler) thì được gần như không gì; phải đi tìm chỗ thời gian thật sự trôi qua — mà chỗ đó thường ngoài mã của bạn: mạng, cơ sở dữ liệu, thời gian khởi động máy, thời gian kéo ảnh. Cùng lý do một lập trình viên tối ưu một vòng lặp trong khi 95% thời gian nằm ở một truy vấn CSDL, hay mài giũa thuật toán trong khi nghẽn thật là một lời gọi mạng đồng bộ. Và trong một chuỗi nối tiếp (HPA → cấp máy → kéo ảnh), thời gian đầu-cuối là tổng, nên khâu chậm nhất định đoạt — thường không phải khâu bạn viết ra.

Mẫu số chung: báo cáo trung thực đúng cái mình đo được và nói rõ chỗ chưa đo; rồi trước khi tối ưu bất cứ thứ gì, hãy tìm khúc thời gian thật sự trôi qua — nó thường nằm ở phần bạn không viết, không nhìn, và không kiểm soát.

Muốn tự soi, tìm thứ đang chặn việc thu gọn trên cụm của bạn:

# pod không thuộc bộ điều khiển nào
kubectl get pods -A -o json | python3 -c '
import sys,json
for p in json.load(sys.stdin)["items"]:
    if not p["metadata"].get("ownerReferences"):
        print(p["metadata"]["namespace"], p["metadata"]["name"])
'

# PodDisruptionBudget không cho phép đuổi ai
kubectl get pdb -A -o custom-columns=\
NS:.metadata.namespace,TEN:.metadata.name,\
CHO_PHEP_DUOI:.status.disruptionsAllowed

Cột CHO_PHEP_DUOI bằng 0 nghĩa là PDB đó đang chặn mọi việc đuổi — và node nào có pod của nó sẽ không bao giờ được trả về.

Phần sau đo RBAC: ai được làm gì, và cách tìm ra quyền thừa.