Khi bạn gạt cần điều hoà, máy thổi lạnh gần như tức thì, nhưng nó không tắt ngay lúc phòng vừa đủ mát — nó chờ một chút, vì bật-tắt liên tục vừa hại máy vừa làm nhiệt độ nhấp nhô khó chịu. Cái "chờ một chút" đó có tên: vùng chết (hysteresis). HPA của Kubernetes hành xử y hệt, và bất đối xứng của nó — nhanh tay bật, chậm tay tắt — không phải lỗi mà là một quyết định thiết kế đáng học. Bài này đo cả hai chiều, và chúng chênh nhau gần chín lần.
Mở rộng: 30 giây
Mục tiêu 50% CPU, min 1 max 8. Tăng tải đột ngột:
| Thời điểm | CPU | Hiện có | Mong muốn | Pod chạy |
|---|---|---|---|---|
| 137 s | 43% | 2 | 2 | 2 |
| 152 s | 107% | 2 | 4 | 4 |
| 167 s | 165% | 4 | 7 | 7 |
| 182 s | 107% | 7 | 8 | 8 |
| 197 s | 47% | 8 | 8 | 8 |
Từ lúc CPU vượt ngưỡng tới lúc đủ 8 bản: 30 giây.
Chú ý HPA không nhảy từng bước. Nó tính thẳng số bản cần thiết:
số bản mong muốn = ceil( số bản hiện tại × CPU hiện tại / CPU mục tiêu )
Ở giây 152: ceil(2 × 107 / 50) = ceil(4,28) = 4. Ở giây 167: ceil(4 × 165 / 50) = 14, nhưng bị chặn ở maxReplicas sau khi qua giới hạn tốc độ mặc định — kết quả là 7. Công thức này là lý do HPA phản ứng nhanh với đỉnh tải lớn: gấp mười lần tải thì nó nhân mười số bản trong một bước, không tăng từng cái một.
Thu gọn: 258 giây
Bỏ hết tải ở giây 235:
| Thời điểm | CPU | Hiện có | Mong muốn |
|---|---|---|---|
| 250 s | 5% | 8 | 8 |
| 311 s | 0% | 8 | 8 |
| 432 s | 0% | 8 | 8 |
| 478 s | 0% | 8 | 8 |
| 493 s | 0% | 8 | 6 |
258 giây — bốn phút rưỡi giữ 8 bản ở 0% CPU. Bất đối xứng 8,6 lần so với chiều mở rộng.
Vì sao lệch nhau nhiều đến vậy
Đây là thiết kế có chủ ý, và lập luận đằng sau rất thẳng thắn:
Mở rộng chậm → người dùng chờ, yêu cầu hỏng. Rất tệ. Thu gọn chậm → trả thừa tiền vài phút. Chấp nhận được.
Cơ chế là stabilizationWindowSeconds:
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # mặc định
scaleUp:
stabilizationWindowSeconds: 0 # mặc định
Với scaleDown, HPA nhìn lại 5 phút gần nhất và lấy số bản lớn nhất trong khoảng đó. Một đỉnh tải sẽ giữ số bản cao thêm năm phút sau khi nó qua đi. Không có cửa sổ này, HPA sẽ dao động: tải giảm → thu gọn → mỗi bản gánh nhiều hơn → CPU tăng → mở rộng → tải chia lại → thu gọn. Vòng lặp đó tốn hơn nhiều so với việc giữ thừa vài bản trong năm phút.
Chỉnh được nếu tải của bạn có hình dạng khác:
behavior:
scaleDown:
stabilizationWindowSeconds: 60
policies: [{type: Percent, value: 50, periodSeconds: 60}]
Nhưng hạ nó xuống dưới 60 giây thường là sai — dao động tốn hơn số tiền tiết kiệm được.
HPA cần requests.cpu
resources: {requests: {cpu: 100m}}
HPA tính phần trăm so với requests, không so với năng lực node. Không khai requests thì nó báo <unknown> và không bao giờ co giãn — không lỗi, không cảnh báo, HPA cứ nằm đó. Đây là nguyên nhân số một của "HPA của tôi không chạy". Và nó nối với phần 47: requests quyết định cả việc xếp chỗ lẫn ngưỡng co giãn. Khai requests quá cao thì HPA thấy phần trăm thấp và không bao giờ mở rộng, dù pod đang chậm.
metrics-server là bắt buộc
HPA đọc chỉ số qua metrics.k8s.io, và API đó do metrics-server cung cấp. Không cài thì HPA không có gì để đọc. Trong kind, metrics-server cần thêm cờ vì chứng chỉ kubelet là tự ký:
kubectl -n kube-system patch deployment metrics-server --type=json \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-",
"value":"--kubelet-insecure-tls"}]'
metrics-server lấy mẫu mỗi 15 giây mặc định, nên đó là độ trễ sàn của mọi phản ứng HPA — cộng vào 30 giây đo được ở trên.
Ba loại chỉ số
Resource — CPU hoặc bộ nhớ. Đơn giản nhất, và là thứ 90% trường hợp dùng. Chú ý: co giãn theo bộ nhớ hiếm khi đúng. Phần lớn ứng dụng không trả lại bộ nhớ khi tải giảm (JVM, Go), nên HPA theo RAM chỉ mở rộng mà không bao giờ thu gọn.
Pods / Object — chỉ số tuỳ chọn từ ứng dụng: số yêu cầu mỗi giây, độ dài hàng đợi. Cần một adapter chỉ số.
External — chỉ số từ ngoài cụm: độ trễ hàng đợi Kafka (phần 32), độ sâu hàng đợi SQS. Đây thường là chỉ số đúng cho hệ thống xử lý bất đồng bộ — CPU chỉ là chỉ báo gián tiếp.
HPA và replicas trong manifest
Nếu Deployment khai replicas: 3 và HPA đang đặt 8, thì mỗi lần bạn kubectl apply lại manifest, số bản tụt về 3 rồi HPA lại kéo lên 8. Cách chữa: bỏ hẳn replicas khỏi manifest khi đã có HPA. Kubernetes giữ nguyên giá trị hiện tại. Với GitOps, thêm chú thích để công cụ bỏ qua trường đó — Argo CD có ignoreDifferences.
Mẫu số chung: sai mỗi chiều một giá khác nhau thì phản ứng khác nhau, và tín hiệu điều khiển phải đảo được
Bài này gói hai nguyên tắc của mọi hệ tự điều chỉnh. Thứ nhất: khi cái giá của việc sai khác nhau theo chiều, hãy phản ứng bất đối xứng — nhanh về phía đắt, chậm về phía rẻ — và thêm một vùng chờ để hệ không rung. Mở rộng chậm thì người dùng khổ (đắt), thu gọn chậm chỉ tốn ít tiền vài phút (rẻ), nên HPA phóng lên trong 30 giây mà rút về trong 258. Đây đúng là circuit breaker — mở tức thì khi phát hiện lỗi, nhưng đóng lại từ tốn qua trạng thái half-open; là bộ điều nhiệt có vùng chết để khỏi bật-tắt liên tục; là connection pool nở nhanh dưới tải và co chậm. Cái stabilizationWindow ở đây chính là hysteresis: một khoảng đệm giữa ngưỡng-bật và ngưỡng-tắt để chặn hiện tượng nhấp nháy (flapping) mà một ngưỡng đơn luôn gây ra. Nguyên tắc: đo cái giá của sai-số theo từng hướng, rồi thiên vị phản ứng về hướng đắt hơn.
Thứ hai, tinh tế hơn: tín hiệu bạn dùng để điều khiển một quyết định hai chiều phải đi được cả hai chiều. Co giãn theo bộ nhớ hỏng vì JVM và Go giữ RAM đã cấp và không trả lại — tín hiệu chỉ tăng, nên vòng điều khiển chỉ biết mở rộng, không bao giờ thu gọn. Cùng cái bẫy với việc lái một quyết định bằng một bộ đếm cộng dồn (chỉ lên) thay vì một đồng hồ tức thời (lên xuống được), hay dò rò rỉ bằng một chỉ số chỉ thấy tăng. Muốn một cơ chế co-giãn hai chiều hoạt động, tín hiệu của nó phải phản ánh trạng thái hiện tại và giảm được khi tải giảm — CPU và độ dài hàng đợi thoả, bộ nhớ đã cấp thì không.
Mẫu số chung: khi hậu quả của sai lệch không đối xứng, đừng phản ứng đối xứng — nhanh về phía nguy hiểm, chậm về phía tốn kém, kèm vùng chờ chống rung; và chỉ lái một vòng điều khiển hai chiều bằng một tín hiệu thật sự đảo được cả hai hướng.
Muốn tự kiểm, xem HPA của bạn có đọc được chỉ số không:
kubectl get hpa -A
Cột TARGETS hiện <unknown> nghĩa là một trong hai: metrics-server chưa chạy, hoặc pod đích không khai requests. Cả hai đều khiến HPA tồn tại mà không làm gì — và nó trông giống hệt một HPA đang bình thường vì tải chưa cao.
Phần sau đo RBAC: ai được làm gì, và cách tìm ra quyền thừa.