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.
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 budgetsBố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.
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 đúngduration. 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")
durationchỉ 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ểu | Ví dụ | Số node được phép gom / chu kỳ | Hành vi |
|---|---|---|---|
| Phần trăm | nodes: "10%" | roundup(tổng_node × 10%) − đang_xoá − notready | co giãn theo quy mô pool |
| Số tuyệt đối | nodes: "1" | 1 − đang_xoá − notready | trầ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).
"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ùng | Giờ làm (local) | Quy đổi UTC |
|---|---|---|
| VN (UTC+7) | 08:00–17:00 | 01:00–10:00 UTC |
| US West (PDT, UTC−7) | 09:00–17:00 | 16:00–00:00 UTC |
| US East (EDT, UTC−4) | 09:00–17:00 | 13: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ố | Đáy | Giờ UTC |
|---|---|---|
| pods_state trên spot | thấp nhất (đáy rõ rệt) | 05:00–06:00 UTC |
| node allocatable (vCPU) | pool nhỏ nhất | 05:00 UTC |
| CPU apps (rate) | thấp | 17:00–23:00 UTC |
Phân tích & quyết định
pods_statevà 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_statephả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.
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"]
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)
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
SpotToSpotConsolidationfeature 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
Underutilizednhưng policy làWhenEmpty⇒ là dead config (underutilized không bao giờ chạy) — dọn luôn cho đúng nghĩa.
References
- Karpenter Disruption (v1.12): karpenter.sh/docs/concepts/disruption
- Image tham chiếu:
public.ecr.aws/karpenter/controller:1.12.1