Skip to content

Multidimensional Pod Autoscaling — HPA và VPA Cùng Lúc

Hai chiều của autoscaling là trực giao

Một workload có hai cách để có thêm công suất: nhiều bản sao hơn (chiều ngang, horizontal) hoặc mỗi bản sao to hơn (chiều dọc, vertical). Đây là hai trục độc lập, và cái hay là chúng giải quyết những bài toán khác nhau:

  • Chiều ngang (HPA) phù hợp với tài nguyên co giãn theo lượng yêu cầu đồng thời — điển hình là CPU và throughput. Gấp đôi traffic thì gấp đôi số replica, mỗi replica xử lý lượng request như cũ. Chiều ngang cũng cho độ bền: nhiều replica trải qua nhiều node/zone nghĩa là mất một thực thể không sập service.
  • Chiều dọc (VPA) phù hợp với tài nguyên không co giãn theo số replica — điển hình là memory. Một service giữ một cache lớn, một working set lớn, hay một heap lớn cần memory đủ cho mỗi Pod bất kể có bao nhiêu Pod. Thêm replica không làm giảm nhu cầu memory của từng Pod; chỉ tăng kích thước Pod mới giải quyết.

Vấn đề kinh điển: nhiều workload thực tế cần cả hai. Một API service vừa CPU-bound (cần scale ngang khi traffic tăng) vừa giữ một working set memory đáng kể mỗi Pod (cần memory request đúng để không OOM). Trực giác đầu tiên là "dùng HPA cho CPU và VPA cho memory". Nhưng như đã thấy ở file 2 và 3, chạy HPA và VPA độc lập trên cùng workload là một anti-pattern dao động — và Multidimensional Pod Autoscaling (MPA) chính là lời giải được Google thiết kế cho đúng tình huống này.

Nhắc lại vì sao HPA + VPA độc lập đánh nhau

Để hiểu MPA giải quyết gì, phải nắm rõ cơ chế xung đột (đã nêu ở file 2, đào sâu thêm ở đây từ góc control theory).

Cả HPA và VPA đều là bộ điều khiển vòng kín. Vấn đề không phải là chúng tồn tại cùng lúc, mà là chúng chia sẻ một biến trạng thái mà cả hai cùng thao tác mà không biết về nhau: requests của Pod.

  • HPA dùng requests làm mẫu số để tính utilization (usage / requests).
  • VPA dùng usage để đặt lại chính requests đó.

Khi hai loop chia sẻ một biến mà một bên dùng nó làm input còn bên kia dùng nó làm output, hệ trở thành coupled và dễ dao động. Cụ thể trên CPU:

  1. Tải tăng → CPU usage mỗi Pod tăng.
  2. HPA: usage/requests tăng → thêm replica.
  3. VPA: usage cao → tăng requests.cpu.
  4. Mẫu số của HPA vừa tăng → usage/requests giảm dù usage tuyệt đối không đổi.
  5. HPA: utilization giảm → bớt replica.
  6. Tải dồn lên ít Pod hơn → quay lại bước 1.

Đây không phải bug của HPA hay VPA — mỗi cái đang làm đúng nhiệm vụ. Đó là hệ quả tất yếu của việc ghép hai bộ điều khiển độc lập lên cùng một tín hiệu. Hệ không hội tụ về điểm cân bằng mà dao động quanh nó.

Cách thoát duy nhất là tách trục: để mỗi chiều scale theo một tài nguyên khác nhau, không chồng lấn. Đó chính xác là điều MPA làm.

MultidimPodAutoscaler: một controller, hai trục không chồng lấn

Theo tài liệu GKE (Multidimensional Pod autoscaling), MPA cho phép scale ngang theo CPU và scale dọc theo memory cùng lúc. Đối tượng là MultidimPodAutoscaler thuộc API group autoscaling.gke.io/v1beta1.

Mấu chốt thiết kế giải quyết xung đột: CPU và memory là hai tài nguyên khác nhau.

  • Trục ngang theo CPU: MPA điều chỉnh số replica để giữ CPU utilization trung bình mỗi replica ở mức target. Đây là vai trò HPA.
  • Trục dọc theo memory: MPA điều chỉnh requests.memory của Pod dựa trên lịch sử usage. Đây là vai trò VPA, nhưng chỉ trên memory.

Vì trục ngang nhìn CPU còn trục dọc chỉ động vào memory, không có biến trạng thái nào bị cả hai loop cùng thao tác. Vòng phản hồi dương ở trên không hình thành: việc MPA tăng requests.memory không ảnh hưởng tới phép tính CPU utilization của trục ngang. Hai trục trực giao đúng nghĩa.

Vì sao đúng cặp CPU-ngang / memory-dọc

Sự ghép cặp này không tùy tiện — nó phản ánh bản chất vật lý của hai tài nguyên (xem Chương 8):

  • CPU là compressible: thiếu CPU gây throttling (chậm) chứ không giết Pod. Scale ngang theo CPU hợp lý vì throughput tổng tỷ lệ với số Pod, và việc thêm/bớt Pod điều chỉnh tổng công suất CPU mượt mà.
  • Memory là incompressible: thiếu memory gây OOMKill (chết). Memory cần đúng cho mỗi Pod; thêm Pod không giúp một Pod đang thiếu memory. Scale dọc theo memory là cách duy nhất đúng để xử lý working set lớn.

Nếu đảo ngược (scale ngang theo memory, dọc theo CPU) sẽ vô nghĩa: thêm replica không giảm áp lực memory mỗi Pod, và scale dọc CPU thì kém hiệu quả hơn scale ngang cho tải co giãn.

Spec của MultidimPodAutoscaler

Cấu trúc spec chính (Multidimensional Pod autoscaling):

yaml
apiVersion: autoscaling.gke.io/v1beta1
kind: MultidimPodAutoscaler
metadata:
  name: my-app-mpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  goals:
    metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60      # trục ngang: giữ CPU ~60%
  constraints:
    global:
      minReplicas: 3
      maxReplicas: 20                 # giới hạn trục ngang
    containerControlledResources:
    - memory                          # trục dọc CHỈ động vào memory
    container:
    - name: '*'
      requests:
        minAllowed:
          memory: 256Mi              # cận dưới request memory
        maxAllowed:
          memory: 4Gi                # cận trên request memory
  policy:
    updateMode: Auto                  # cách áp recommendation memory

Các trường then chốt:

  • scaleTargetRef: workload đích (Deployment).
  • goals.metrics: định nghĩa target cho trục ngang — averageUtilization của CPU. Đây là phần "HPA" của MPA.
  • constraints.global.minReplicas/maxReplicas: giới hạn số replica của trục ngang.
  • constraints.containerControlledResources: liệt kê tài nguyên mà trục dọc kiểm soát — chỉ hỗ trợ memory. Đây là cơ chế thực thi tính trực giao: trục dọc bị giới hạn cứng vào memory.
  • constraints.container[].requests.minAllowed/maxAllowed: cận trên/dưới cho requests.memory, ngăn recommendation chạy quá đà.
  • policy.updateMode: cách áp recommendation memory (ví dụ Auto), tương tự update mode của VPA.

Yêu cầu

  • Cluster GKE version 1.19.4-gke.1700 trở lên.
  • Trên Standard cluster, phải bật Vertical Pod Autoscaling ở cấp cluster trước (--enable-vertical-pod-autoscaling). Trên Autopilot, VPA đã bật sẵn nên MPA dùng được ngay.
  • Tính năng ở mức Beta (autoscaling.gke.io/v1beta1), theo điều khoản Pre-GA.

Khi nào dùng MPA, khi nào không

MPA không phải lựa chọn mặc định cho mọi workload. Nó giải quyết một tình huống cụ thể; với phần lớn workload, HPA đơn thuần là đủ và đơn giản hơn.

Dùng MPA khi

  • Workload vừa CPU-bound vừa có nhu cầu memory đáng kể và khó đoán mỗi Pod. Ví dụ kinh điển: một service xử lý request (CPU co giãn theo traffic) đồng thời giữ cache/working set in-memory mà kích thước phụ thuộc dữ liệu (memory khó đặt cứng).
  • Bạn đã từng bị một trong hai vấn đề: HPA scale tốt nhưng Pod thỉnh thoảng OOM vì memory request đặt cứng quá thấp; hoặc bạn đặt memory request cao "cho chắc" và lãng phí.
  • Bạn muốn một đối tượng quản lý thay vì tự phối hợp HPA + VPA Off thủ công.

Không dùng MPA (chọn cách khác) khi

  • Workload chỉ cần một chiều: thuần CPU-bound với memory ổn định → HPA đủ. Thuần memory-bound, ít thay đổi tải → VPA đủ.
  • Cần scale theo metric không phải CPU (RPS, queue depth, custom/external metric): MPA trục ngang chỉ theo CPU utilization. Nếu cần scale ngang theo custom metric right-size memory, pattern thay thế là HPA (custom metric) + VPA Off (memory, áp thủ công) — tách trục thủ công.
  • Workload event-driven cần scale-to-zero: dùng KEDA (file 8) cho trục ngang, kết hợp VPA Off cho memory nếu cần.
  • Workload JVM: VPA (và do đó trục dọc của MPA) chưa phù hợp JVM (file 3).

Một cách kiểm tra nhanh "workload này có cần MPA không": hỏi hai câu. (1) Khi traffic tăng, số lượng request đồng thời có tăng không, và CPU có phản ánh điều đó không? Nếu có → cần trục ngang theo CPU (phần HPA của MPA hợp lý). (2) Memory mỗi Pod có khó đặt cứng vì phụ thuộc dữ liệu/working set không? Nếu có → cần trục dọc theo memory (phần VPA của MPA hợp lý). Chỉ khi cả hai câu trả lời là "có" thì MPA mới là lựa chọn đúng; nếu chỉ một, dùng HPA hoặc VPA đơn thuần.

Bảng quyết định nhanh

Đặc điểm workloadCông cụ phù hợp
CPU-bound, memory ổn địnhHPA (CPU)
Memory-bound, tải ít đổiVPA
CPU-bound + memory khó đoán mỗi PodMPA
Scale theo RPS/queue + memory khó đoánHPA (custom metric) + VPA Off
Event-driven, cần scale-to-zeroKEDA (+ VPA Off nếu cần)
JVM serviceHPA + đặt heap/request thủ công

Real-world scenario: API stateful với cache in-memory

Xét một recommendation API: mỗi Pod nạp một phần model/feature store vào memory (working set vài trăm MB tới vài GB tùy dữ liệu nóng), và xử lý request CPU-bound. Đây là ứng cử viên sách giáo khoa cho MPA.

Nếu chỉ dùng HPA (CPU): scale ngang tốt theo traffic, nhưng memory request phải đặt cứng. Đặt thấp → OOM khi working set phình theo dữ liệu nóng. Đặt cao → mỗi Pod chiếm chỗ thừa, bin-packing kém, đắt. Và vì không có ai điều chỉnh memory, bạn kẹt giữa OOM và lãng phí.

Nếu dùng HPA + VPA độc lập trên cùng CPU: dao động (đã phân tích).

Với MPA: trục ngang giữ CPU ~60% (scale theo traffic), trục dọc tự điều chỉnh requests.memory theo working set thật quan sát được, trong khoảng minAllowed/maxAllowed an toàn. Khi dữ liệu nóng phình, memory request được nâng (in-place nếu hỗ trợ, hoặc qua tạo lại Pod); khi traffic tăng, replica được thêm. Hai điều chỉnh không can thiệp lẫn nhau.

Điểm cần lưu ý vận hành: trục dọc của MPA kế thừa toàn bộ đặc tính (và giới hạn) của VPA — scale-down memory chậm vì nhớ peak (file 3), và việc áp recommendation memory có thể gây tạo lại Pod trừ khi dùng in-place. Đặt maxAllowed để chặn runaway, và theo dõi để memory request không "kẹt cao" sau một spike hiếm.

Di chuyển từ HPA/VPA độc lập sang MPA

Nhiều đội đến với MPA sau khi đã chạy HPA (và có thể cả VPA) độc lập rồi gặp dao động. Quy trình chuyển đổi an toàn:

  1. Gỡ bỏ hoàn toàn HPA và VPA cũ trên workload đích. Đây là bước bắt buộc và hay bị quên — nếu để một HPA cũ chạy song song MPA, bạn tái tạo đúng xung đột MPA sinh ra để tránh, cộng thêm việc hai controller cùng động vào replicas.
  2. Quan sát trước khi enforce: nếu workload chưa có dữ liệu usage memory đủ (24h+), trục dọc của MPA sẽ có recommendation độ tin cậy thấp. Cân nhắc chạy VPA Off riêng một thời gian để xác định dải memory hợp lý, dùng nó đặt minAllowed/maxAllowed cho MPA.
  3. Đặt maxAllowed bảo thủ ban đầu: bắt đầu với trần memory không quá xa request hiện tại, nới dần khi tin tưởng recommendation. Tránh để recommendation nhảy vọt ngay lần đầu.
  4. Giám sát cả hai trục: theo dõi số replica (trục ngang) và memory request thực tế (trục dọc) sau khi áp MPA. Nếu thấy replica dao động bất thường, kiểm tra xem có HPA cũ sót lại không.

Quan sát recommendation và quyết định của MPA

Vì trục dọc của MPA là VPA bên dưới, bạn đọc recommendation memory qua chính đối tượng MPA (kubectl describe mpa my-app-mpa) — nó hiển thị target, lowerBound, upperBound cho memory tương tự VPA. Trục ngang (CPU) phát log quyết định qua HPA controller (hpa-controller, file 1) vì MPA tạo cơ chế HPA bên dưới cho phần horizontal. Nghĩa là toàn bộ kỹ năng đọc log atomic/final recommendation ở file 1 áp dụng cho phần CPU của MPA, và kỹ năng đọc recommendation VPA ở file 3 áp dụng cho phần memory.

So sánh MPA với pattern "HPA custom metric + VPA Off"

Đây là quyết định kiến trúc tinh tế mà nhiều đội bỏ qua. Cả hai đều cho "scale ngang + right-size", nhưng khác nhau ở điểm cốt lõi:

Tiêu chíMPAHPA (custom metric) + VPA Off
Trục ngang theoChỉ CPU utilizationBất kỳ metric nào (RPS, queue, custom)
Trục dọcTự động áp memoryRecommendation thủ công, áp qua GitOps
Số đối tượng quản lý1 (MPA)2 (HPA + VPA) + quy trình áp thủ công
Tự động hóaCaoThấp hơn (memory áp thủ công)
Kiểm soát/reviewThấp hơnCao (mỗi thay đổi memory qua PR)
Phù hợp khiCPU là tín hiệu scale đúngBottleneck không phải CPU

Quy tắc chọn: nếu CPU thực sự là tín hiệu scale-out đúng cho workload (CPU-bound thật) và bạn muốn tự động hóa cao, chọn MPA. Nếu bottleneck là I/O/queue/latency (CPU không phản ánh tải) hoặc bạn cần kiểm soát chặt việc thay đổi memory qua review, chọn HPA custom metric + VPA Off. Đây không phải "cái nào tốt hơn" mà là "tín hiệu scale của bạn là gì".

Trade-offs và giới hạn

  • Phức tạp hơn: thêm một CRD, một controller, hai trục để suy luận. Chỉ chấp nhận phức tạp này khi workload thật sự cần cả hai chiều.
  • Trục ngang chỉ theo CPU: không scale ngang theo custom/external metric trong MPA. Đây là giới hạn lớn nhất; nhiều workload thực tế muốn scale theo RPS/queue chứ không phải CPU.
  • Trục dọc chỉ memory: không right-size CPU qua MPA (CPU do trục ngang xử lý). Hợp lý về mặt thiết kế nhưng nghĩa là CPU request vẫn cần đặt đúng thủ công hoặc qua VPA Off riêng để lấy gợi ý CPU.
  • Beta: v1beta1, cân nhắc cho production critical; kiểm thử kỹ và theo dõi roadmap GA.
  • Kế thừa giới hạn VPA: standalone Pod không dùng được, JVM không phù hợp, scale-down memory chậm.

Failure modes khi MPA cấu hình sai

MPA giải quyết xung đột HPA+VPA, nhưng nó có các failure mode riêng cần lường trước:

  • maxAllowed memory quá thấp: trục dọc muốn nâng memory nhưng bị chặn ở trần → Pod vẫn OOM, và recommendation uncappedTarget (vượt target) là dấu hiệu trần đang chặn. Triệu chứng giống "VPA không làm gì" nhưng thực ra dây cương đang siết.
  • maxReplicas quá thấp: trục ngang muốn thêm replica nhưng đụng trần → CPU utilization vượt target kéo dài, biểu hiện như ScalingLimited của HPA bên dưới. Đây là vấn đề capacity/trần, không phải MPA.
  • CPU không phải tín hiệu đúng: nếu workload thực ra nghẽn ở I/O/queue chứ không phải CPU, trục ngang theo CPU của MPA scale sai thời điểm — đúng như HPA theo CPU sai. MPA không sửa được việc chọn sai tín hiệu; nó chỉ giải quyết xung đột hai chiều.
  • Memory request "kẹt cao" sau spike hiếm: trục dọc kế thừa đặc tính nhớ peak của VPA. Một batch job bất thường có thể kéo memory recommendation lên và giữ nhiều ngày, làm bin-packing kém. Theo dõi và đặt maxAllowed hợp lý.
  • Quên rằng đây là Beta: v1beta1 có thể thay đổi. Với production critical, kiểm thử kỹ hành vi trên phiên bản GKE cụ thể và theo dõi thông báo GA/deprecation.

Quy trình debug MPA về bản chất là debug hai loop riêng: với vấn đề số replica → debug như HPA (file 1, 2), đọc log hpa-controller; với vấn đề memory request → debug như VPA (file 3), đọc recommendation qua kubectl describe mpa. Việc tách đúng "vấn đề thuộc trục nào" là bước đầu tiên của mọi chẩn đoán MPA.

Anti-patterns thường gặp

  • Dùng MPA như "bật cho chắc" trên workload chỉ cần một chiều. Thêm phức tạp không cần thiết; HPA hoặc VPA đơn thuần đủ.
  • Quên rằng trục ngang MPA chỉ theo CPU. Nếu bottleneck là I/O/queue, MPA scale ngang sai như HPA theo CPU sai. Khi đó tách trục thủ công (HPA custom metric + VPA Off).
  • Không đặt maxAllowed cho memory. Một spike hiếm có thể kéo recommendation memory lên cao và giữ lâu; không có trần thì lãng phí kéo dài.
  • Vẫn để một HPA hoặc VPA cũ chạy song song với MPA trên cùng workload. Phải gỡ bỏ HPA/VPA độc lập trước khi áp MPA, nếu không lại tái tạo đúng xung đột mà MPA sinh ra để tránh.
  • Dùng MPA cho JVM service rồi ngạc nhiên vì recommendation memory kỳ lạ — trục dọc kế thừa hạn chế JVM của VPA.

References