Bỏ qua đến nội dung
DevOps Lab

Karpenter — Bin-pack 1 lần/ngày bằng Scheduled Disruption Budget

Gom node loãng có kiểm soát đúng một cửa sổ off-peak mỗi ngày — tiết kiệm spot mà không churn liên tục.

AWS · EKS · Karpenter

Bin-pack 1 lần/ngày bằng Scheduled Disruption Budget

Cho phép Karpenter gom (bin-pack) node "loãng" đúng một cửa sổ ngắn mỗi ngày vào giờ thấp điểm — tiết kiệm tiền spot mà không gây churn liên tục. Dùng kỹ thuật "đảo ngược cửa sổ" với disruption budget theo lịch (cron).

Karpenter v1.12 · consolidationPolicy + scheduled budgets

Bối cảnh & mục tiêu

Sau khi migrate cluster sang Karpenter + spot, các app NodePool thường để consolidationPolicy: WhenEmpty — nghĩa là Karpenter chỉ xoá node rỗng hoàn toàn, không bao giờ gom các node "loãng" lại với nhau. Lý do chọn WhenEmpty: tránh churn liên tục, ưu tiên ổn định cho app quan trọng.

Nhưng quan sát thực tế cho thấy nhiều node chạy rất loãng: CPU thực tế chỉ 1–8%, RAM 15–60%, trong khi requests lại 66–99% (workload over-request). WhenEmpty không bao giờ thu hồi được mấy node mỏng này → lãng phí tiền.

i
Mục tiêu: tiết kiệm thêm mà không gom gấp gáp → cho phép bin-pack đúng 1 cửa sổ ngắn mỗi ngày, vào giờ thấp điểm, cap số node bị đụng mỗi lần.

Vì sao "scheduled disruption budget" làm được điều này

Bốn sự thật khoá chốt từ tài liệu Karpenter:

1. consolidationPolicy

  • WhenEmpty — chỉ xoá node rỗng. Không bao giờ bin-pack.
  • WhenEmptyOrUnderutilized — gom cả node underutilized → cái này mới tiết kiệm thêm, nhưng nếu để mặc định (consolidateAfter: 0s) thì gom liên tục → churn.

2. Disruption Budgets tách theo reasons

Budget tách được theo reasons: Empty, Drifted, Underutilized.

  • Karpenter lấy MIN của tất cả budget khớp một reason (hoặc budget không khai reasons).
  • Budget nodes: "0" = chặn hoàn toàn reason đó.

3. Scheduled budget: schedule + duration đi cùng nhau

  • Khi tới schedule (cron), budget bắt đầu được enforce trong đúng duration. Ngoài khoảng đó budget không áp dụng.
  • Cron CHỈ chạy theo UTC — không hỗ trợ timezone. (doc ghi rõ "Schedules are always in UTC")
  • duration chỉ nhận phút/giờ: 23h, 30m, 10h5m.

4. Forceful methods KHÔNG bị budget giới hạn

Spot interruption, expiration, node repair, xoá tay không bị budget chặn → cửa sổ này không ảnh hưởng tới việc Karpenter xử lý spot bị thu hồi.

Kỹ thuật "đảo ngược cửa sổ"

Doc cho ví dụ: budget nodes:"0" + @daily + 10m để chặn Underutilized trong 10 phút đầu ngày. Ta làm ngược lại: chặn Underutilized gần như cả ngày (23h), chỉ chừa 1 cửa sổ 1 tiếng để Karpenter được phép gom.

Vì budget scheduled bắt đầu enforce tại schedule và kéo dài duration:

  • schedule: "0 6 * * *" + duration: "23h" → chặn từ 06:00 UTC tới 05:00 UTC hôm sau.
  • ⇒ Khoảng 05:00–06:00 UTC mỗi ngày budget chặn KHÔNG active ⇒ cửa sổ gom mở.

Trong cửa sổ đó vẫn còn budget thường nodes: "10%" reasons:[Underutilized] → MIN ⇒ cap ~10% số node mỗi chu kỳ. Không gấp gáp.

nodes: phần trăm vs số tuyệt đối

Field nodes trong mỗi budget khai được 2 kiểu, ý nghĩa khác hẳn nhau:

KiểuVí dụSố node được phép gom / chu kỳHành vi
Phần trămnodes: "10%"roundup(tổng_node × 10%) − đang_xoá − notreadyco giãn theo quy mô pool
Số tuyệt đốinodes: "1"1 − đang_xoá − notreadytrần cứng, không đổi theo quy mô

Minh hoạ:

  • "10%" với pool 9 node → roundup(0.9) = 1 node/chu kỳ. Pool lớn lên 30 node → roundup(3.0) = 3 node/chu kỳ (tự tăng theo quy mô).
  • "1" (tuyệt đối) → luôn tối đa 1 node/chu kỳ, dù pool 5 hay 50 node.
  • "0" (tuyệt đối) → chặn hoàn toàn reason đó (chính là budget scheduled khoá Underutilized 23h).
i
Khi nhiều budget cùng khớp một reason, Karpenter lấy MIN. Trong cửa sổ 05:00–06:00 UTC: budget "10%" (hoặc "1" ở pool Java) là cái duyệt; ngoài cửa sổ budget scheduled "0" thắng (vì MIN(10%, 0) = 0) ⇒ không gom.

Vì sao pool Java (on-demand) cố tình dùng "1" tuyệt đối thay vì %

App Java khởi động chậm, là pool quan trọng nhất. Nếu để "10%" thì khi pool to ra (vd 30 node) một chu kỳ có thể gom 3 node Java cùng lúc ⇒ 3 service restart chậm đồng thời. Dùng "1" thì trần cố định = 1 node/chu kỳ bất kể quy mô ⇒ an toàn nhất. Pool spot stateless thì dùng % vì gom song song nhiều hơn không sao.

Cách chọn giờ cửa sổ (phần dễ chọn sai)

Ràng buộc: cron Karpenter chỉ UTC. Với một team đa múi giờ, khung rảnh chung rất hẹp — phải tìm bằng dữ liệu thật chứ không đoán. Ví dụ một team trải VN + US:

VùngGiờ làm (local)Quy đổi UTC
VN (UTC+7)08:00–17:0001:00–10:00 UTC
US West (PDT, UTC−7)09:00–17:0016:00–00:00 UTC
US East (EDT, UTC−4)09:00–17:0013:00–21:00 UTC

Ghép lại → gần như bận cả ngày. Khung rảnh chung rất hẹp.

Đo từ Prometheus / VictoriaMetrics (ví dụ, 48h, gom theo giờ UTC)

# port-forward read-only tới metrics query endpoint (VictoriaMetrics / Prometheus)
kubectl -n monitoring port-forward svc/<metrics-query-svc> 8481:8481 &
# endpoint: http://localhost:8481/select/0/prometheus/api/v1/query_range

Metric đáng tin để biết "tải thật":

MetricÝ nghĩa
sum(karpenter_pods_state{nodepool="spot-apps"})tổng pod đang chạy trên pool
sum(karpenter_nodes_allocatable{nodepool="spot-apps",resource_type="cpu"})tổng vCPU pool cấp phát
sum(rate(container_cpu_usage_seconds_total{namespace=~"app-a|app-b"}[5m]))CPU thực tế app dùng

Kết quả ví dụ (đáy trong ngày):

Chỉ sốĐáyGiờ UTC
pods_state trên spotthấp nhất (đáy rõ rệt)05:00–06:00 UTC
node allocatable (vCPU)pool nhỏ nhất05:00 UTC
CPU apps (rate)thấp17:00–23:00 UTC

Phân tích & quyết định

  • pods_state và số node đồng thuận: workload thật chạm đáy 05:00–06:00 UTC. Quy đổi: VN = trưa (nghỉ trưa), US West = đêm, US East = đêm → điểm trũng toàn cầu không đụng giờ làm ai.
  • CPU rate đáy ở 22–23 UTC, NHƯNG lúc đó US West vẫn đang làm → loại. Gom node khi nhân viên đang dùng = rủi ro. pods_state phản ánh "có bao nhiêu thứ đang chạy" sát hơn cho quyết định gom node.
  • Bonus: tại 05:00 UTC pool đã tự co nhỏ nhất ⇒ gom thêm ít phải pre-spin node thay thế ⇒ churn thấp nhất.
i
Chọn cửa sổ 05:00–06:00 UTC.

Vì sao độ rộng 1 tiếng (không phải 30 phút)

  • Karpenter quét consolidation theo nhịp; pool này dùng consolidateAfter: 1h.
  • 30 phút quá hẹp → dễ lỡ một chu kỳ quét, có ngày không gom được gì.
  • 1 tiếng đủ cho 1–2 chu kỳ gom mà vẫn đóng cửa sổ trước khi vùng kế tiếp vào ca làm.

Fix — áp cho 3 NodePool

1. spot-apps (app — quan trọng nhất)

Bật bin-pack có kiểm soát theo cửa sổ ngày:

disruption:
  consolidationPolicy: WhenEmptyOrUnderutilized   # đổi từ WhenEmpty
  consolidateAfter: 1h
  budgets:
    - nodes: "20%"
      reasons: ["Drifted"]
    - nodes: "1"
      reasons: ["Empty"]
    - nodes: "10%"
      reasons: ["Underutilized"]
    - nodes: "0"
      schedule: "0 6 * * *"     # chặn Underutilized từ 06:00 UTC
      duration: "23h"           # suốt 23h -> chừa cửa sổ 05:00-06:00 UTC
      reasons: ["Underutilized"]

Hành vi: ngoài 05:00–06:00 UTC ⇒ 0 gom underutilized (ổn định như WhenEmpty cũ). Trong cửa sổ ⇒ gom tối đa ~1 node/chu kỳ. Empty vẫn xoá đều; Drifted 20% cho AMI update.

2. spot-observability (loki + VM stack — stateful)

Áp cùng cơ chế và cùng cap 10%. Tier này có stateful (vmstorage, loki-ingester) nhưng các pod đó có PDB chặn nên Karpenter tự bỏ qua khi gom.

disruption:
  consolidationPolicy: WhenEmptyOrUnderutilized
  consolidateAfter: 1h
  budgets:
    - nodes: "20%"
      reasons: ["Drifted"]
    - nodes: "1"
      reasons: ["Empty"]
    - nodes: "10%"
      reasons: ["Underutilized"]
    - nodes: "0"
      schedule: "0 6 * * *"
      duration: "23h"
      reasons: ["Underutilized"]
!
vmstorage / loki-ingester nên có PDB để Karpenter không gom nhầm khi đang ghi. Nếu thấy churn stateful, hạ về WhenEmpty cho riêng pool này.

3. ondemand-java (khởi động chậm — bảo thủ nhất)

App Java là quan trọng nhất, khởi động chậm. Vẫn cho 1 cửa sổ/ngày nhưng cap tuyệt đối 1 node/chu kỳ (absolute, không phải %) — chặt hơn cả 2 pool spot.

disruption:
  consolidationPolicy: WhenEmptyOrUnderutilized
  consolidateAfter: 1h
  budgets:
    - nodes: "10%"
      reasons: ["Drifted"]
    - nodes: "1"
      reasons: ["Empty"]
    - nodes: "1"
      reasons: ["Underutilized"]
    - nodes: "0"
      schedule: "0 6 * * *"
      duration: "23h"
      reasons: ["Underutilized"]

Tổng kết cơ chế (1 dòng)

Budget nodes:"0" + schedule/duration chặn Underutilized 23h/ngày; chỉ chừa 05:00–06:00 UTC cho phép bin-pack, cap theo % (MIN của các budget cùng reason). Cron Karpenter chỉ UTC. Empty + Drifted không bị cửa sổ ảnh hưởng.

Kiểm chứng sau khi áp dụng

# Xem budget hiện tại
kubectl get nodepool spot-apps -o yaml | yq '.spec.disruption'

# Theo dõi event consolidation (chỉ nên thấy gom trong 05-06 UTC)
kubectl get events -A --field-selector reason=DisruptionBlocked,reason=Unconsolidatable -w

# Đếm node theo thời gian (so trước/sau) — PromQL:
# count(count by (node) (karpenter_nodes_allocatable{nodepool="spot-apps"}))

Dấu hiệu đúng: ngoài cửa sổ, Karpenter log disruption budget ... allows 0 ... Underutilized.

Prevention / lưu ý

  • Cron luôn UTC — khi đổi mùa (DST tháng 3 & tháng 11) giờ local của Mỹ dịch 1h, nhưng UTC giữ nguyên. 05:00–06:00 UTC vẫn là đêm Mỹ + trưa VN quanh năm → an toàn, không cần chỉnh theo DST.
  • Nếu tăng số node nhiều, xem lại 10% (có thể thành 2–3 node/lần).
  • KHÔNG bật SpotToSpotConsolidation feature gate trừ khi có chủ đích (cần ≥15 instance-type flexibility, dễ "race to the bottom" về instance rẻ nhất → interruption cao).
  • Config cũ có budget Underutilized nhưng policy là WhenEmpty ⇒ là dead config (underutilized không bao giờ chạy) — dọn luôn cho đúng nghĩa.

References