Skip to content

HorizontalPodAutoscaler — Control Loop & Thuật Toán

Vì sao phải hiểu loop trước khi cấu hình metric

Phần lớn các đội triển khai HPA bằng một dòng kubectl autoscale deployment ... --cpu-percent=70 --min=3 --max=20 rồi coi như xong. Khi nó hoạt động đúng, không ai để ý. Khi nó hành xử "kỳ lạ" — scale lên rồi đứng yên, không scale xuống dù tải đã giảm, hay dao động quanh một con số — đội vận hành không có mô hình tinh thần để truy nguyên, vì họ chưa bao giờ nhìn HPA như cái nó thực sự là: một control loop rời rạc, chạy theo chu kỳ, tính một phép chia, làm tròn lên, và áp một loạt cơ chế dampening để không phản ứng thái quá.

Hiểu loop này tới mức có thể tự tính tay ra desiredReplicas cho một trạng thái cluster bất kỳ là điều kiện cần để debug HPA. Vì HPA là tất định: với cùng metric đầu vào, cùng cấu hình, nó luôn ra cùng quyết định. "Sự bí ẩn" luôn nằm ở khoảng cách giữa cái bạn nghĩ nó làm và cái nó thật sự làm. File này xóa khoảng cách đó.

Control loop: rời rạc, không liên tục

Điểm hiểu lầm đầu tiên và phổ biến nhất: HPA không phản ứng tức thời với metric. Nó là một loop chạy theo chu kỳ cố định. Theo tài liệu Kubernetes, controller chạy như một control loop ngắt quãng (không liên tục), và khoảng thời gian giữa hai lần chạy được đặt bởi tham số --horizontal-pod-autoscaler-sync-period của kube-controller-manager, mặc định 15 giây (Horizontal Pod Autoscaling).

Điều này có hệ quả trực tiếp tới production:

  • Độ trễ phản ứng tối thiểu là một chu kỳ. Ngay cả trong trường hợp lý tưởng nhất, từ lúc metric vượt ngưỡng đến lúc HPA hành động có thể mất tới ~15 giây, cộng thêm độ trễ của pipeline metric (metrics-server scrape, Cloud Monitoring ingestion). Với một spike traffic dốc, 15 giây đã đủ để gây lỗi.
  • Bạn không sửa được chu kỳ này trên GKE.kube-controller-manager chạy trên control plane do Google quản lý, bạn không có quyền đổi --horizontal-pod-autoscaler-sync-period. Đòn bẩy duy nhất ở phía bạn là behavior policy (file 2) và việc bật Performance HPA Profile (giữ chu kỳ ~15s ngay cả khi có hàng nghìn HPA object).

Trên GKE, mỗi lần loop chạy, HPA controller phát ra log quyết định. Đây là khác biệt vận hành lớn so với Kubernetes thuần: GKE ghi lại một final recommendation mỗi 15 giây cho mỗi HPA object, cho bạn dấu vết kiểm toán đầy đủ về mọi quyết định (sẽ phân tích kỹ ở cuối file).

Công thức cốt lõi: một phép chia và một hàm trần

Toàn bộ HPA quy về một công thức duy nhất. Theo tài liệu Kubernetes, số replica mong muốn được tính như sau:

desiredReplicas = ceil[currentReplicas × (currentMetricValue / desiredMetricValue)]

(Algorithm details)

Trong đó:

  • currentReplicas là số replica đang chạy (chính xác hơn: số Pod được tính vào phép đo, xem phần dampening bên dưới).
  • currentMetricValue là giá trị metric hiện tại, thường là trung bình trên tất cả Pod khi dùng averageUtilization hoặc averageValue.
  • desiredMetricValue là target bạn đặt.
  • ceil[] là hàm trần — luôn làm tròn lên.

Vài ví dụ để khóa trực giác:

currentReplicascurrentMetricdesiredMetricTỷ lệceildesiredReplicas
4200m100m2.0ceil(8.0)8
450m100m0.5ceil(2.0)2
370%50%1.4ceil(4.2)5
1048%50%0.96ceil(9.6)10 (xem tolerance)

Hàng cuối là điểm tinh tế: tỷ lệ 0.96 rất gần 1.0. Nếu HPA hành động mỗi lần tỷ lệ lệch khỏi 1.0 dù chỉ chút, nó sẽ liên tục thêm/bớt một Pod khi metric dao động quanh target. Đây là lý do tồn tại của tolerance.

Hàm trần luôn làm tròn lên — và vì sao điều đó quan trọng

ceil nghĩa là HPA luôn thiên về nhiều Pod hơn một chút. Tỷ lệ 1.01 với 100 replica ra ceil(101) = 101. Nhưng tỷ lệ 1.001 với 3 replica ra ceil(3.003) = 4 — một bước nhảy 33%. Ở số replica nhỏ, hàm trần khiến mỗi thay đổi metric nhỏ có thể gây thay đổi replica tương đối lớn. Đây là một lý do nữa vì sao chạy HPA với minReplicas quá thấp (1–2) thường gây dao động: độ phân giải replica quá thô.

Tolerance: vùng chết chống dao động vi mô

Theo tài liệu Kubernetes, control plane bỏ qua mọi hành động scale nếu tỷ lệ đủ gần 1.0, trong một tolerance cấu hình được, mặc định 0.1 (Tolerance). Nghĩa là HPA chỉ hành động khi tỷ lệ currentMetricValue / desiredMetricValue lệch khỏi 1.0 hơn ±10%.

Diễn giải cụ thể với target 50%:

  • Metric trong khoảng ~45%–55% (tỷ lệ 0.9–1.1): không hành động.
  • Metric > 55%: cân nhắc scale-up.
  • Metric < 45%: cân nhắc scale-down.

Tolerance là cơ chế phòng thủ đầu tiên chống flapping. Nếu không có nó, một service ổn định ở đúng 50% nhưng dao động tự nhiên ±2% sẽ khiến HPA liên tục tính ra desiredReplicas lệch và churn Pod vô nghĩa. 10% là một vùng chết đủ rộng để hấp thụ nhiễu đo lường thông thường.

Hệ quả thiết kế: đừng đặt target quá sát ngưỡng nguy hiểm. Nếu service bắt đầu xuống cấp ở 80% CPU và bạn đặt target 75%, tolerance 10% nghĩa là HPA có thể để metric trôi tới ~82% trước khi scale-up — đã quá ngưỡng xuống cấp. Đặt target ở mức có đủ headroom cho cả tolerance lẫn độ trễ scale-up end-to-end.

Dampening: HPA cố tình "bảo thủ" với Pod chưa ổn định

Đây là phần mà gần như mọi người bỏ qua, và là nguồn của những hành vi "khó hiểu" nhất. HPA không đơn giản lấy trung bình metric của tất cả Pod. Nó phân loại Pod và áp dụng giả định bảo thủ tùy theo trạng thái, để không phản ứng thái quá dựa trên dữ liệu chưa đáng tin.

Theo tài liệu Kubernetes, cơ chế như sau (Algorithm details):

  • Pod chưa Ready (đang khởi động): được để sang một bên. Khi tính scale-up, HPA giả định Pod này tiêu thụ 0% — tức là không tin Pod mới đã gánh tải. Điều này ngăn HPA "thấy" Pod mới còn nguội (CPU thấp) và vội kết luận đã đủ công suất.
  • Pod thiếu metric: HPA giả định bảo thủ theo chiều ngược với hành động dự kiến — 100% khi đang định scale-down (giả định Pod đó đang tải nặng, đừng vội bỏ), 0% khi đang định scale-up (đừng vội thêm dựa trên Pod không có dữ liệu).
  • Pod Failed: bị loại hoàn toàn khỏi phép tính.
  • Pod đang terminating: bỏ qua với per-pod resource metric.

Hai tham số chu kỳ kiểm soát "khi nào một Pod được coi là đủ ổn định để tin metric của nó":

  • --horizontal-pod-autoscaler-initial-readiness-delay, mặc định 30 giây: cửa sổ ngay sau khi Pod chuyển Ready, trong đó metric vẫn được xem là chưa đáng tin.
  • --horizontal-pod-autoscaler-cpu-initialization-period, mặc định 5 phút (300 giây): với CPU, trong giai đoạn khởi tạo này HPA xử lý đặc biệt các Pod chưa Ready để tránh bị nhiễu bởi CPU spike lúc warm-up (JIT compile, cache priming, connection pool init...).

Hệ quả production cực kỳ quan trọng: HPA cố tình chậm tin Pod mới. Khi scale-up, nó giả định Pod mới chưa gánh tải, nên nó có xu hướng tiếp tục tính ra số replica cao cho đến khi Pod mới thực sự Ready và bắt đầu kéo metric trung bình xuống. Đây là thiết kế đúng — nó ngăn HPA scale-up một Pod, thấy CPU trung bình tụt (vì Pod mới còn nguội), rồi tưởng đã đủ. Nhưng nó cũng có nghĩa là nếu readiness probe của bạn cấu hình sai (Pod báo Ready quá sớm khi chưa thật sự phục vụ được), HPA sẽ tin nhầm và scale sai.

Nhiều metric: luôn chọn cái lớn nhất

Một HPA có thể được cấu hình với nhiều metric (ví dụ vừa CPU vừa một custom metric về độ sâu hàng đợi). Theo tài liệu, khi có nhiều metric, HPA tính desiredReplicas độc lập cho từng metric, rồi chọn giá trị lớn nhất (Scaling on multiple metrics). Tài liệu GKE diễn đạt cùng nguyên tắc: "scale lớn nhất được chọn cho hành động autoscale", và nếu một metric không khả dụng, HPA scale lên chứ không scale xuống (GKE HPA).

Đây là quy tắc "an toàn trước, tiết kiệm sau": với nhiều tín hiệu, HPA luôn chọn tín hiệu đòi hỏi nhiều công suất nhất. Hệ quả thiết kế:

  • Thêm metric vào HPA chỉ có thể làm nó scale-up nhiều hơn hoặc bằng, không bao giờ ít hơn. Một custom metric "nhạy" hơn CPU sẽ chi phối hành vi.
  • Khi debug "vì sao HPA scale lên dù CPU thấp", luôn kiểm tra tất cả metric — rất có thể một metric khác (queue depth, RPS) đang là metric chi phối.

Stabilization window: bộ nhớ ngắn hạn chống flapping

Tolerance chống dao động vi mô trong một chu kỳ. Stabilization window chống dao động ở quy mô nhiều chu kỳ — đặc biệt là scale-down. Theo tài liệu GKE, để tránh thrashing, controller chọn recommendation lớn nhất trong vòng 5 phút gần nhất (GKE HPA). Trong Kubernetes, đây là stabilizationWindowSeconds, với mặc định 300 giây (5 phút) cho scale-down.

Cơ chế: thay vì hành động ngay theo desiredReplicas mới tính, HPA giữ một cửa sổ các recommendation gần đây và:

  • Khi scale-down: dùng giá trị lớn nhất trong cửa sổ. Nghĩa là nó chỉ scale xuống khi tải đã giảm và giữ ở mức thấp suốt cả cửa sổ. Một lần tải giảm nhất thời sẽ không kích hoạt scale-down.
  • Khi scale-up (mặc định cửa sổ 0): hành động gần như ngay, vì rủi ro của việc thiếu công suất lớn hơn rủi ro của over-provisioning tạm thời.

Sự bất đối xứng này — scale-up nhanh, scale-down chậm — là triết lý mặc định đúng đắn cho hầu hết workload phục vụ traffic người dùng: thà thừa công suất 5 phút còn hơn thiếu công suất 1 giây. Bạn điều chỉnh cửa sổ này qua behavior policy (file 2), nhưng phải hiểu mặc định trước khi đổi.

Hệ quả production hay bị hiểu lầm: "vì sao HPA của tôi không scale xuống dù CPU đã thấp 2 phút rồi?" — vì cửa sổ stabilization scale-down mặc định là 5 phút, và nó dùng giá trị lớn nhất trong cửa sổ đó. Phải đợi đủ 5 phút tải thấp liên tục.

Đọc log HPA trên GKE: atomic vs final recommendation

Đây là siêu năng lực debug mà chỉ GKE cho bạn. Trên Kubernetes thuần, để hiểu vì sao HPA ra một quyết định bạn phải đọc kubectl describe hpa (chỉ cho ảnh chụp hiện tại) hoặc bật log verbose của controller (không truy cập được trên control plane managed). GKE thay vào đó ghi mọi quyết định HPA vào Cloud Logging.

Theo tài liệu GKE (View HPA events), log nằm ở projects/PROJECT_ID/logs/container.googleapis.com%2Fhpa-controller, với resource type k8s_control_plane_componentcomponent_name: hpa-controller. Có hai loại log quyết định:

Atomic recommendation

Mô tả recommendation dựa trên một metric riêng lẻ được cấu hình trên HPA. Phát ra mỗi 15 giây cho mỗi metric của mỗi HPA. Các trường chính:

  • startTime: thời điểm HPA bắt đầu tính.
  • hpa: tên đối tượng HPA.
  • podCount: tổng số Pod (Ready, chưa Ready, bị bỏ qua) — cực hữu ích để thấy bao nhiêu Pod thực sự được tính.
  • metric: spec, status, timestamp của sample mới nhất và "tuổi" của nó — nếu sample quá cũ, đây là dấu hiệu pipeline metric trục trặc.
  • summary: chứa replicas (số đề xuất), dampening (hướng dampening: up/down/none), và override (lý do nếu giá trị bị ghi đè).

Final recommendation

Tổng hợp các atomic recommendation qua tất cả metric thành quyết định cuối cùng cho HPA object. Phát ra mỗi 15 giây cho mỗi HPA. Các trường chính:

  • configuredSize: số replica trước đó.
  • replicas: số replica đề xuất cuối cùng.
  • targetRef: tham chiếu workload đích.
  • topLevelOverride & topLevelLimit: lý do điều chỉnh ở cấp cao nhất.
  • normalization: chi tiết stabilization và limitation, với số replica trước và sau chuẩn hóa — đây là nơi bạn thấy chính xác stabilization window đã "kìm" một quyết định scale-down như thế nào.
  • actuationTime & actuationLatencySeconds: thời điểm và độ trễ thực thi.

Cách đọc trong thực tế

Quy tắc đếm: với 2 HPA, mỗi cái theo dõi 3 metric, mỗi 15 giây bạn nhận 8 entry = 6 atomic (3 metric × 2 HPA) + 2 final (1 mỗi HPA). Khi debug "vì sao HPA scale/không scale", quy trình là:

  1. Đọc final recommendation để biết quyết định cuối và xem normalization — có phải stabilization đang giữ ở mức cao không?
  2. Đọc các atomic recommendation để biết metric nào chi phối (nhớ quy tắc "chọn lớn nhất"). Nếu một custom metric đang ép số replica cao, atomic của nó sẽ lộ ra ngay.
  3. Xem dampeningpodCount để hiểu HPA đang tính trên bao nhiêu Pod và có đang áp dampening cho Pod chưa Ready không.

Một query Cloud Logging điển hình:

resource.type="k8s_control_plane_component"
resource.labels.component_name="hpa-controller"
resource.labels.cluster_name="CLUSTER_NAME"
jsonPayload.targetRef.name="my-deployment"

Lưu ý vận hành: các log này thuộc nhóm System logs. Theo tài liệu GKE, System logs cần thiết cho troubleshooting và support — nếu bạn tắt System logs để tiết kiệm, bạn mất luôn khả năng debug HPA này, và support của GKE chỉ ở mức best-effort.

Debug bằng status conditions

Bên cạnh log, bản thân đối tượng HPA mang ba condition trong status.conditions, đọc qua kubectl describe hpa. Đây là ảnh chụp nhanh nhưng là điểm khởi đầu debug nhanh nhất:

  • AbleToScale: HPA có thể thực hiện hành động scale không (đã lấy được thông tin scale target, không trong backoff). False thường nghĩa là vấn đề ở việc truy cập đối tượng scale (Deployment).
  • ScalingActive: HPA có đang tích cực tính toán và scale không. False với reason như FailedGetResourceMetric nghĩa là pipeline metric đang hỏng — metrics-server không trả CPU, hoặc Custom Metrics Adapter không khả dụng. Đây là nguyên nhân số một của "HPA không làm gì cả".
  • ScalingLimited: HPA có đang bị chặn bởi minReplicas/maxReplicas không. True với reason TooManyReplicas nghĩa là HPA muốn scale cao hơn maxReplicas — một tín hiệu mạnh rằng cluster đang thiếu công suất và bạn cần nâng trần. TooFewReplicas nghĩa là đang chạm sàn.

Quy trình debug 30 giây với một HPA "không hoạt động":

  1. kubectl describe hpa X → nhìn ScalingActive. Nếu False → metric pipeline hỏng, không phải lỗi HPA.
  2. Nếu ScalingLimited=True / TooManyReplicas → HPA đang muốn nhiều hơn maxReplicas. Vấn đề là capacity hoặc trần, không phải HPA.
  3. Nếu cả hai ổn nhưng vẫn không scale như mong đợi → sang log atomic/final recommendation để xem stabilization và metric chi phối.

Worked example: lần theo một quyết định scale từ đầu tới cuối

Để khóa toàn bộ mô hình tinh thần, hãy lần theo một chu kỳ HPA cụ thể. Cấu hình: target CPU averageUtilization: 50, requests.cpu: 200m, đang chạy 4 replica.

Trạng thái: 3 Pod Ready dùng trung bình 180m CPU, 1 Pod vừa mới Ready 10 giây trước (trong initial-readiness-delay 30s) dùng 40m.

  1. Phân loại Pod: Pod mới (10s < 30s) được để sang một bên cho mục đích dampening scale-up — HPA không tin metric thấp của nó. HPA tính trên 3 Pod ổn định.
  2. Tính utilization: trung bình 180m / 200m request = 90% trên các Pod ổn định.
  3. Tính tỷ lệ: 90% / 50% = 1.8.
  4. Kiểm tra tolerance: |1.8 − 1.0| = 0.8 > 0.1 → vượt tolerance, HPA hành động.
  5. Tính desiredReplicas: ceil(4 × 1.8) = ceil(7.2) = 8.
  6. Kiểm tra behavior/stabilization scale-up: cửa sổ scale-up mặc định 0 → hành động ngay (trừ khi behavior giới hạn tốc độ, file 2).
  7. Áp giới hạn min/max: nếu maxReplicas ≥ 8 → đặt 8; nếu maxReplicas = 6 → đặt 6 và set condition ScalingLimited=True, TooManyReplicas.
  8. Ghi log: phát atomic recommendation (cho metric CPU) với podCountdampening, và final recommendation với configuredSize: 4, replicas: 8, normalization cho biết có bị giới hạn không.

Chu kỳ sau (15s): 4 Pod mới đang khởi động (chưa Ready) → vẫn bị dampening (giả định 0% khi scale-up) → HPA tiếp tục tính trên các Pod ổn định, có thể vẫn thấy utilization cao → giữ hoặc tăng tiếp. Chỉ khi Pod mới Ready và bắt đầu gánh tải, utilization trung bình mới giảm và HPA ổn định ở số replica đủ. Đây chính là cơ chế "chậm tin Pod mới" hoạt động đúng — nó ngăn HPA dừng scale quá sớm.

Tự tính tay được ví dụ này nghĩa là bạn có thể đọc bất kỳ kubectl describe hpa hay log final recommendation nào và biết chính xác con số đến từ đâu.

Tolerance cấu hình được per-HPA (phiên bản mới)

Mặc định tolerance 0.1 là toàn cục cho controller. Ở các phiên bản Kubernetes mới hơn, tolerance có thể cấu hình per-HPA qua behavior.scaleUp.tolerancebehavior.scaleDown.tolerance (alpha/beta tùy phiên bản). Điều này giải quyết một hạn chế thực tế: một số workload cần tolerance chặt hơn (phản ứng với thay đổi nhỏ) còn số khác cần lỏng hơn (chịu nhiễu lớn). Khi khả dụng trên phiên bản GKE của bạn, đây là đòn bẩy tinh để điều chỉnh độ nhạy mà không đụng tới cấu hình control plane. Kiểm tra phiên bản và trạng thái feature gate trước khi dựa vào nó cho production.

Anti-patterns thường gặp

  • Đặt minReplicas: 1 cho service production. Ở 1 replica, độ phân giải replica quá thô (mỗi bước là ±100%), hàm trần khuếch đại nhiễu, và bạn không có HA. Tối thiểu 2–3 cho mọi service phục vụ traffic.
  • Đặt target quá sát ngưỡng xuống cấp. Quên rằng tolerance 10% + độ trễ scale-up end-to-end nghĩa là metric thực tế sẽ vượt target một khoảng trước khi công suất mới online. Để headroom.
  • Tin readiness probe báo Ready quá sớm. HPA dùng trạng thái Ready để quyết định Pod nào được tính. Probe sai → HPA tính trên Pod chưa thật sự phục vụ → quyết định sai.
  • Không bao giờ đọc log hpa-controller. Mọi sự cố HPA trở thành phỏng đoán. Log này là nguồn sự thật duy nhất về quyết định của HPA trên GKE.
  • Đổ lỗi cho HPA khi nguyên nhân là metric pipeline. ScalingActive=False nghĩa là HPA hoàn toàn lành mạnh nhưng không lấy được metric. Sửa pipeline, không sửa HPA.

References