Skip to content

KEDA — Kubernetes Event-Driven Autoscaling

Khoảng trống mà KEDA lấp

HPA (file 1, 2) là công cụ mạnh nhưng có hai giới hạn cố hữu với kiến trúc event-driven:

  1. HPA không scale-to-zero. minReplicas của HPA tối thiểu là 1 — nó không bao giờ đưa workload về 0 replica. Với một worker chỉ chạy khi có message trong hàng đợi, giữ tối thiểu 1 Pod chạy 24/7 dù hàng đợi trống là lãng phí thuần túy.
  2. External metric của HPA cồng kềnh. Để HPA scale theo độ sâu hàng đợi Pub/Sub, kích thước Cloud Tasks queue, hay lag Kafka, bạn phải tự dựng và vận hành một external metrics adapter cho từng nguồn — phức tạp và dễ vỡ.

KEDA (Kubernetes Event-Driven Autoscaling) lấp đúng hai khoảng trống này. Nó cho phép scale-to-zero và cung cấp sẵn hàng chục scaler cho các nguồn sự kiện phổ biến (Pub/Sub, Cloud Tasks, Kafka, Prometheus, BigQuery...). KEDA không thay thế HPA — như sẽ thấy, nó dùng HPA bên dưới — mà mở rộng nó để xử lý đúng lớp workload event-driven.

Điểm khác biệt vận hành quan trọng so với mọi autoscaler khác trong chương: KEDA là add-on bạn tự cài và tự nâng cấp, không phải component do Google quản lý trên control plane. GKE hỗ trợ KEDA (có thể cài qua add-on hoặc Helm), nhưng vòng đời của nó nằm ở phía bạn.

Kiến trúc KEDA: ba thành phần và mối quan hệ với HPA

KEDA gồm các thành phần chính:

  • keda-operator (controller): theo dõi các ScaledObject/ScaledJob, và tạo/quản lý một đối tượng HPA tương ứng cho mỗi ScaledObject. Đây là phần "agent" của KEDA.
  • keda-operator-metrics-apiserver (metrics adapter): expose metric từ các external source dưới dạng API external.metrics.k8s.io để HPA (do KEDA tạo) đọc. Đây là cầu nối giữa nguồn sự kiện và HPA.
  • admission webhooks: validate ScaledObject/ScaledJob lúc tạo (ví dụ ngăn nhiều ScaledObject cùng trỏ một workload).

Cơ chế cốt lõi cần khắc sâu: KEDA tạo HPA bên dưới. Theo tài liệu KEDA (Scaling Deployments), KEDA giám sát nguồn sự kiện và đưa dữ liệu cho Kubernetes và HPA để thực hiện scale. Cụ thể: với mỗi ScaledObject, KEDA tạo một HPA tên keda-hpa-{scaled-object-name}, và HPA đó scale dựa trên metric mà metrics-apiserver của KEDA cung cấp.

Hệ quả của thiết kế này:

  • Khi replica từ 1 trở lên, chính HPA điều khiển scale — nghĩa là toàn bộ kiến thức ở file 1, 2 (thuật toán, behavior, stabilization) vẫn áp dụng. Bạn vẫn cấu hình behavior qua KEDA.
  • KEDA chỉ đặc biệt ở ranh giới 0↔1 — phần mà HPA không làm được. Đây là "pha activation" (bên dưới).

ScaledObject vs ScaledJob

KEDA có hai đối tượng cho hai mô hình workload khác nhau:

  • ScaledObject: scale một workload chạy liên tục (Deployment, StatefulSet, hoặc custom resource có subresource /scale). Phù hợp worker tiêu thụ message liên tục — số replica tăng/giảm theo backlog, kể cả về 0. Đây là loại phổ biến nhất.
  • ScaledJob: tạo Kubernetes Job cho mỗi đơn vị công việc rời rạc. Phù hợp khi mỗi message cần một lần xử lý độc lập, dài, không idempotent với việc chia sẻ process — mỗi event sinh một Job riêng, chạy tới hoàn thành rồi biến mất. Khác biệt bản chất: ScaledObject điều chỉnh replica của một workload đang chạy; ScaledJob sinh ra các Job mới.

Chọn sai gây vấn đề: dùng ScaledObject cho công việc batch dài (Pod bị scale-down giết giữa chừng) hay dùng ScaledJob cho stream message liên tục (overhead tạo Job cho mỗi message) đều sai mô hình.

Scale-to-zero: hai pha activation và scaling

Đây là tính năng đặc trưng và là lý do chính người ta dùng KEDA. Theo tài liệu KEDA (Scaling Deployments), có hai pha tách biệt:

Pha activation (deactivation/activation)

Là thời điểm KEDA quyết định scale từ 0 lên hay về 0. Đây là phần KEDA tự xử lý, không phải HPA (HPA không làm được 0↔1). KEDA poll nguồn sự kiện theo pollingInterval; khi phát hiện có việc (metric vượt activationThreshold), nó kích hoạt workload từ 0 lên 1. Khi không còn việc và qua cooldownPeriod, nó đưa về 0.

Pha scaling

Sau khi đã ở 1 replica trở lên, HPA tiếp quản việc quyết định scale (1→N→1). KEDA chỉ cung cấp metric; HPA tính desiredReplicas theo đúng thuật toán file 1.

activationThreshold vs threshold — phân biệt then chốt

Đây là điểm gây nhầm lẫn nhất:

  • threshold (giá trị mục tiêu của scaler): dùng trong pha scaling để HPA tính số replica — tương tự target của HPA.
  • activationThreshold (tùy chọn, mặc định 0): dùng trong pha activation để quyết định có rời khỏi 0 hay không.

Ví dụ cụ thể: một Pub/Sub scaler với value: 5 (mỗi replica xử lý ~5 message tồn) và activationThreshold: 10. Nghĩa là: KEDA chỉ kích hoạt từ 0 lên 1 khi backlog vượt 10 message; sau khi đã chạy, HPA scale để giữ ~5 message/replica. Đặt activationThreshold cao hơn 0 tránh "flapping quanh 0" — không bật dậy chỉ vì 1 message lẻ.

Các default quan trọng của ScaledObject

Theo spec KEDA (ScaledObject spec):

TrườngMặc địnhÝ nghĩa
pollingInterval30 giâyKEDA poll nguồn sự kiện mỗi 30s (quyết định độ nhạy activation)
cooldownPeriod300 giâyChờ sau lần cuối có hoạt động trước khi về 0
initialCooldownPeriod0 giâyCooldown ngay sau khi tạo ScaledObject
minReplicaCount0Số replica tối thiểu — 0 nghĩa là scale-to-zero
maxReplicaCount100Trần replica
idleReplicaCount(bỏ qua)Phải nhỏ hơn minReplicaCount nếu đặt
fallback.failureThreshold / replicas(bắt buộc nếu có fallback)Số replica dùng khi scaler lỗi
advanced.restoreToOriginalReplicaCountfalseKhi xóa ScaledObject, có khôi phục replica gốc không

Hai điểm vận hành quan trọng:

  • pollingInterval quyết định độ trễ activation từ 0. Mặc định 30s nghĩa là trong trường hợp xấu nhất, một message tới hàng đợi trống có thể đợi tới ~30s trước khi KEDA kích hoạt Pod đầu tiên — cộng thêm cold start của Pod. Đây là cái giá của scale-to-zero (xem trade-off bên dưới).
  • fallback cực kỳ quan trọng cho production: nếu nguồn metric (Pub/Sub API, Prometheus) tạm thời lỗi, không có fallback nghĩa là KEDA không lấy được metric → workload có thể bị kẹt sai số replica. fallback cho phép "giữ N replica an toàn" khi scaler lỗi.

Cấu hình behavior của HPA bên dưới vẫn được, qua advanced.horizontalPodAutoscalerConfig.behavior — nghĩa là toàn bộ behavior policy ở file 2 áp dụng cho phần scaling của KEDA.

Pub/Sub scaler: scale theo backlog

Scaler GCP-native phổ biến nhất. Nó scale dựa trên độ trễ/backlog của subscription — tín hiệu phản ánh đúng "lượng việc đang chờ", chứ không phải CPU. Theo tài liệu KEDA (GCP Pub/Sub scaler):

  • mode: metric để scale, là PascalCase của tên metric GCP. Phổ biến nhất:
    • SubscriptionSize (mặc định) — số message chưa giao (num_undelivered_messages), tức backlog.
    • OldestUnackedMessageAge — tuổi của message cũ nhất chưa ack, phản ánh độ trễ xử lý.
  • value: mục tiêu mỗi replica (mặc định 10). Ví dụ value: 5 nghĩa là KEDA/HPA cố giữ ~5 message tồn cho mỗi replica.
  • subscriptionName (hoặc topicName): nguồn cần theo dõi.
  • aggregation: hàm tổng hợp (mean, sum, percentileX...) — bắt buộc cho metric kiểu distribution.

Ví dụ:

yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: pubsub-worker
spec:
  scaleTargetRef:
    name: pubsub-worker
  minReplicaCount: 0
  maxReplicaCount: 50
  cooldownPeriod: 120
  triggers:
  - type: gcp-pubsub
    metadata:
      mode: "SubscriptionSize"
      value: "5"
      subscriptionName: "orders-subscription"
    authenticationRef:
      name: keda-gcp-auth

Xác thực: nên dùng GCP Workload Identity (podIdentity: provider: gcp) thay vì credential JSON trong env — KEDA gọi Cloud Monitoring API để đọc metric subscription, và Workload Identity (Chương 13) là cách an toàn không cần key dài hạn.

Vì sao scale theo backlog đúng hơn CPU: một worker tiêu thụ message có thể CPU thấp nhưng backlog đang phình (ví dụ nghẽn ở downstream/DB). Scale theo CPU sẽ không phản ứng; scale theo SubscriptionSize hoặc OldestUnackedMessageAge phản ánh đúng áp lực thật và scale đúng lúc. OldestUnackedMessageAge đặc biệt tốt khi SLO của bạn là về độ trễ xử lý ("không message nào chờ quá X giây").

TriggerAuthentication và Workload Identity

Để tách cấu hình xác thực khỏi ScaledObject, KEDA dùng đối tượng TriggerAuthentication (hoặc ClusterTriggerAuthentication). Với GCP, pattern an toàn nhất là Workload Identity:

yaml
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: keda-gcp-auth
spec:
  podIdentity:
    provider: gcp

ScaledObject tham chiếu qua authenticationRef. KEDA operator (hoặc Pod được scale) dùng Google Service Account gắn qua Workload Identity (Chương 13) để gọi Cloud Monitoring API đọc metric Pub/Sub/Cloud Tasks. Không có key JSON dài hạn nào nằm trong cluster — đây là điểm hardening quan trọng so với credentialsFromEnv.

ScaledJob: một Job cho mỗi đơn vị công việc

Khi mỗi message là một công việc dài, độc lập, nên chạy tới hoàn thành (xử lý video, training ngắn, ETL một file lớn), ScaledJob đúng hơn ScaledObject. Nó tạo một Kubernetes Job cho mỗi đơn vị công việc thay vì điều chỉnh replica của một workload chạy liên tục:

yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
  name: video-encoder
spec:
  jobTargetRef:
    template:
      spec:
        containers:
        - name: encoder
          image: my-encoder:1.0
        restartPolicy: Never
  pollingInterval: 30
  maxReplicaCount: 100        # tối đa số Job song song
  scalingStrategy:
    strategy: "default"
  triggers:
  - type: gcp-pubsub
    metadata:
      mode: "SubscriptionSize"
      value: "1"             # mỗi Job xử lý 1 message
      subscriptionName: "encode-jobs"
    authenticationRef:
      name: keda-gcp-auth

Khác biệt bản chất với ScaledObject:

  • Không bị scale-down giết giữa chừng: mỗi Job chạy tới hoàn thành rồi tự kết thúc. ScaledObject điều chỉnh replica của workload đang chạy, nên scale-down có thể giết Pod đang xử lý dở — sai cho công việc dài không idempotent với việc bị ngắt.
  • scalingStrategy kiểm soát cách KEDA tính số Job cần tạo dựa trên số message tồn và số Job đang chạy.
  • Phù hợp workload "mỗi message = một lần chạy rời rạc"; không phù hợp stream message liên tục (overhead tạo Job mỗi message quá lớn — dùng ScaledObject).

Prometheus scaler và external scalers khác

Prometheus scaler

Cho phép scale theo bất kỳ truy vấn PromQL nào. Đây là scaler linh hoạt nhất: nếu metric của bạn đã ở Prometheus (kể cả Google Cloud Managed Service for Prometheus), bạn scale theo một biểu thức PromQL tùy ý — RPS, p99 latency, queue depth nội bộ, business metric. Cấu hình gồm serverAddress, query (PromQL), và threshold.

Trên GKE, kết hợp KEDA Prometheus scaler với Managed Service for Prometheus (Chương 14) cho một pipeline scale rất mạnh: workload expose metric → Managed Prometheus thu thập → KEDA query PromQL → scale. Đây là cách scale theo "đơn vị công việc nghiệp vụ" mà không tự dựng custom metrics adapter.

Ví dụ Prometheus scaler scale theo p99 latency từ Managed Prometheus:

yaml
triggers:
- type: prometheus
  metadata:
    serverAddress: "https://monitoring.googleapis.com/v1/projects/PROJECT/location/global/prometheus"
    query: "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[2m])) by (le))"
    threshold: "0.5"        # giữ p99 latency ~500ms
    activationThreshold: "0.1"
  authenticationRef:
    name: keda-gcp-auth

Đây là dạng scale theo SLO trực tiếp: thay vì scale theo CPU/RPS và hy vọng latency ổn, bạn scale theo chính metric latency mà SLO cam kết. Khi p99 vượt ngưỡng, KEDA/HPA thêm Pod để kéo nó xuống. Cần thận trọng: scale theo latency là vòng phản hồi gián tiếp (thêm Pod không phải lúc nào cũng giảm latency nếu bottleneck ở downstream), nên thường kết hợp với một trigger thứ hai (RPS/queue) làm tín hiệu chính.

Các external scaler GCP khác

  • Cloud Tasks queue: scale theo số task tồn trong một Cloud Tasks queue — tương tự Pub/Sub nhưng cho mô hình task/job. Phù hợp khi kiến trúc dùng Cloud Tasks làm hàng đợi công việc.
  • BigQuery: scale theo kết quả một truy vấn BigQuery (ví dụ số dòng cần xử lý, độ trễ pipeline tính từ một bảng trạng thái). Hữu ích cho data pipeline mà tín hiệu tải nằm trong chính dữ liệu.

Mẫu hình chung của external scaler: chúng dịch một tín hiệu bên ngoài cluster hoàn toàn thành quyết định scale, kể cả scale-to-zero — điều HPA thuần rất khó làm gọn.

Trade-off cốt lõi: cold start của scale-to-zero

Scale-to-zero không miễn phí. Cái giá là độ trễ của request/message đầu tiên khi workload đang ở 0:

message tới (hàng đợi trống)
  → đợi tới chu kỳ poll tiếp theo (tối đa ~pollingInterval, mặc định 30s)
  → KEDA kích hoạt 1 replica
  → Pod Pending → có thể cần CA tạo node (phút, file 5)
  → Pod khởi động + kéo image + warm-up
  → bắt đầu xử lý

Tổng cold start có thể từ vài giây tới vài phút. Hệ quả thiết kế:

  • Phù hợp: workload async/batch nơi độ trễ vài giây–phút cho item đầu chấp nhận được (xử lý hàng đợi, job định kỳ, pipeline). Tiết kiệm lớn khi idle nhiều.
  • Không phù hợp: API đồng bộ phục vụ người dùng cần latency thấp ngay cả request đầu. Với loại này, đặt minReplicaCount >= 1 (KEDA vẫn dùng được cho phần scaling, chỉ không về 0), hoặc dùng idleReplicaCount giữ một mức nền tối thiểu.
  • Giảm cold start: hạ pollingInterval (đánh đổi: poll nguồn metric thường xuyên hơn, tốn API call/quota); kết hợp capacity buffer (file 7) để Pod không phải đợi tạo node; giữ image nhỏ và startup nhanh.

Real-world: hệ thống xử lý đơn hàng event-driven

Xét một hệ thống thương mại điện tử: đơn hàng đẩy vào Pub/Sub, một fleet worker xử lý (validate, tính thuế, gọi payment, ghi DB). Tải dao động cực mạnh — gần như 0 lúc 3 giờ sáng, spike khủng khiếp giờ flash sale.

Với HPA theo CPU: giữ tối thiểu vài Pod 24/7 (lãng phí ban đêm), và scale theo CPU không phản ánh backlog (worker chờ payment downstream thì CPU thấp dù đơn dồn).

Với KEDA Pub/Sub scaler:

  • minReplicaCount: 0 → ban đêm hàng đợi trống, fleet về 0, không tốn gì.
  • mode: SubscriptionSize, value: 30 → mỗi replica giữ ~30 đơn tồn; backlog tăng thì scale ngang theo đúng lượng việc thật.
  • OldestUnackedMessageAge làm trigger thứ hai → đảm bảo không đơn nào chờ quá SLO (ví dụ 60s), kể cả khi tổng backlog chưa lớn.
  • cooldownPeriod đủ dài để không flap khi traffic lác đác.
  • Kết hợp capacity buffer (file 7) để khi flash sale spike, Pod mới có node sẵn, cắt cold start.

Kết quả: chi phí gần 0 lúc nhàn rỗi, scale đúng theo backlog thật lúc cao điểm, và SLO độ trễ xử lý được bảo vệ bằng trigger tuổi message.

Hiểu đúng cooldownPeriod và quá trình về 0

cooldownPeriod (mặc định 300s) là một trong những trường hay bị hiểu sai nhất. Nó không phải khoảng giữa các lần scale thông thường (đó là việc của HPA stabilization, file 1). Nó là khoảng KEDA chờ sau lần cuối cùng có hoạt động (metric trên activationThreshold) trước khi đưa workload về 0.

Diễn giải chuỗi về 0:

  1. Backlog cạn, metric tụt dưới activationThreshold.
  2. HPA (do KEDA tạo) scale dần xuống minReplicaCount — nhưng vì minReplicaCount: 0 không hợp lệ cho HPA thuần (HPA tối thiểu 1), KEDA giữ ở 1 cho tới khi quyết định deactivate.
  3. Sau cooldownPeriod không có hoạt động, KEDA deactivate: xóa HPA tạm thời và đưa workload về 0.

Hệ quả: cooldownPeriod quá ngắn → workload bị đưa về 0 rồi lại bật dậy liên tục khi traffic lác đác (flapping quanh 0, tốn cold start lặp lại). Quá dài → giữ 1 Pod chạy không cần thiết lâu hơn mức tối ưu. Đặt cooldownPeriod dựa trên pattern "khoảng lặng" thật của hàng đợi: nếu message thường tới theo cụm cách nhau vài phút, đặt cooldown đủ dài để không tắt giữa hai cụm.

idleReplicaCount là biến thể tinh tế: thay vì về hẳn 0, giữ một mức nền (ví dụ 1) khi idle nhưng vẫn cho phép scale cao khi bận — dung hòa giữa tiết kiệm và cold start cho workload không thể chịu cold start hoàn toàn nhưng vẫn muốn tiết kiệm lúc nhàn.

Anti-patterns thường gặp

  • Dùng scale-to-zero cho API đồng bộ latency-thấp. Cold start làm request đầu chậm/timeout. Đặt minReplicaCount >= 1 cho loại này.
  • Quên fallback. Khi Pub/Sub/Prometheus API lỗi tạm thời, KEDA mất metric → scale sai. fallback giữ mức an toàn.
  • Scale theo CPU trong khi backlog là tín hiệu thật. Lặp lại sai lầm của HPA. Dùng SubscriptionSize/OldestUnackedMessageAge/PromQL phản ánh việc thật.
  • Đặt activationThreshold: 0 (mặc định) rồi flap quanh 0. Một message lẻ bật cả workload dậy. Đặt ngưỡng activation hợp lý.
  • Dùng credential JSON dài hạn thay vì Workload Identity. Rủi ro bảo mật không cần thiết trên GKE.
  • Để nhiều ScaledObject/HPA cùng trỏ một workload. Xung đột điều khiển; admission webhook của KEDA chặn phần lớn nhưng đừng cố lách.
  • Quên rằng phần scaling vẫn là HPA. Mọi đặc tính stabilization/behavior (file 1, 2) áp dụng; cấu hình chúng qua advanced.horizontalPodAutoscalerConfig.

References