FinOps 7 — Tối ưu chi phí Kubernetes

13 thg 7, 2026 2 lượt xem
#kubernetes
#data-engineering
#finops
#autoscaling
#kubecost

Vì sao chi phí Kubernetes khó nhìn

Sáu bài trước của series tối ưu chi phí ở tầng dịch vụ được quản lý (managed): warehouse, storage, compute pipeline, streaming, rồi dựng cơ chế giám sát (FinOps 6 — Giám sát chi phí & phát hiện bất thường). Nhưng ngày càng nhiều nền tảng dữ liệu — kể cả ở NCB — chạy trên Kubernetes (K8s): Spark on K8s, Flink, Airflow (KubernetesExecutor), Trino, các API phục vụ dữ liệu, job ML. K8s cho tính linh hoạt và tính di động (portability) tuyệt vời, nhưng lại tạo ra một điểm mù chi phí rất đặc thù mà bài này đi giải quyết. Vận hành cụm ở tầng production được bàn kỹ ở Kubernetes — Vận hành production; ở đây ta chỉ tập trung vào tiền.

Vấn đề gốc: hóa đơn cloud tính theo node (máy ảo), nhưng giá trị lại tiêu theo pod. Nhà cung cấp cloud gửi cho bạn hóa đơn "50 máy ảo m5.4xlarge chạy 720 giờ" — họ không biết trên mỗi node có bao nhiêu pod, thuộc team nào, chạy job gì. Cả cụm với họ chỉ là mấy chục máy ảo giống hệt nhau. Trong khi đó, thứ thực sự tiêu tiền là hàng trăm pod của nhiều team chia sẻ chung các node đó.

Ba đặc điểm khiến chi phí K8s khó quy trách nhiệm:

  • Nhiều workload chia sẻ một node: một node m5.4xlarge (16 vCPU, 64 GB) có thể chứa vài pod Airflow, một executor Spark của team Risk, một API của team Marketing. Hóa đơn node là một cục — làm sao xé nó ra cho từng team?
  • Khoảng cách giữa cấp phát và sử dụng thật: pod đặt trước (request) 4 vCPU nhưng chỉ dùng 0,5 vCPU. Bạn trả tiền cho 4 vCPU giữ chỗ, không phải 0,5 vCPU dùng thật. Khoảng cách này (slack) là nơi tiền bốc hơi.
  • Tính động cao: pod sinh/diệt liên tục, node scale lên xuống, job batch chạy vài phút rồi biến mất. Ảnh chụp tĩnh không phản ánh được chi phí thực theo thời gian.

Kết quả: cloud cost tool thấy được node nhưng về pod/team; còn dashboard K8s thấy pod nhưng không biết giá tiền. Cần một lớp riêng ghép hai thứ lại — đó là OpenCost/Kubecost, phần cuối bài.

REQUEST/LIMIT & right-sizing — nguồn lãng phí lớn nhất

Mỗi container khai báo hai con số tài nguyên cho CPU và memory:

  • Request: mức tài nguyên K8s đảm bảo dành riêng cho pod. Scheduler dùng request để quyết định đặt pod lên node nào và đây chính là thứ quyết định chi phí — vì node phải đủ chỗ cho tổng request của mọi pod trên nó.
  • Limit: mức trần pod được phép dùng. Vượt limit CPU → bị throttle (bóp ga, chậm lại); vượt limit memory → bị OOMKilled (kernel giết pod vì hết bộ nhớ).

Đây là điểm mấu chốt hay bị hiểu sai: bạn trả tiền theo request, không phải theo dùng thật. Nếu tổng request của các pod lấp đầy node, node được coi là "đầy" và Cluster Autoscaler sẽ bật thêm node mới — dù CPU thực tế chỉ dùng 15%. Request quá cao = trả tiền cho không khí.

Đặt request sao cho đúng

Hai hướng lệch đều tốn kém:

  • Request quá cao → lãng phí giữ chỗ (over-provisioning): node đầy request nhưng rỗng việc, cụm phình ra vô ích. Đây là lỗi phổ biến nhất và là mỏ tiết kiệm số một.
  • Request quá thấp → pod bị OOMKilled hoặc throttle khi tải lên, gây khởi động lại (restart) và mất ổn định. Với một API tra cứu hạn mức tín dụng, throttle nghĩa là độ trễ tăng vọt đúng giờ cao điểm.

Nguyên tắc: đo thực tế rồi mới chỉnh, đừng đoán. Lấy dữ liệu sử dụng CPU/memory theo percentile trong vài tuần (Prometheus, kubectl top, Kubecost), rồi:

  • Request đặt gần P50–P75 mức dùng thật (mức nền hằng ngày).
  • Limit memory đặt sát đỉnh (P99) để tránh OOM — với memory, request ≈ limit là an toàn vì memory không nén được (không throttle được như CPU).
  • Limit CPU: nhiều nơi để rộng hoặc không đặt limit CPU cho batch để job tận dụng CPU rảnh của node (burst), miễn là request đủ để không đói tài nguyên hàng xóm.

VPA (Vertical Pod Autoscaler) giúp việc này: chạy ở chế độ recommendation (chỉ gợi ý, không tự sửa), VPA quan sát mức dùng thật và đề xuất request phù hợp. Với workload ổn định như Trino coordinator hay API, có thể để VPA chế độ Auto tự chỉnh. Right-sizing thủ công + VPA gợi ý thường cắt 30–50% chi phí compute mà không đụng gì tới tính ổn định.

Bin-packing & node utilization

Nếu request là "mỗi pod đòi bao nhiêu chỗ", thì bin-packing là "nhét các pod vào ít node nhất có thể". Hình dung mỗi node là một chiếc thùng (bin) dung tích cố định, mỗi pod là một kiện hàng; mục tiêu là dùng ít thùng nhất. Node rỗng vẫn tính tiền đủ — nên giảm số node bằng cách gom pod chặt lại là đòn tiết kiệm trực tiếp.

Chỉ số cần theo dõi là utilization (tỷ lệ dùng): so tài nguyên đã request với dung lượng node, và so dùng thật với đã request. Cụm khỏe mạnh về chi phí thường nhắm request-vs-capacity ~70–80% (còn ~20% cho đỉnh và cho việc dời pod), và dùng-thật-vs-request cao (nghĩa là request sát thực tế). Utilization thấp trên cả hai chiều = tiền chảy ra ngoài.

Vài đòn bẩy bin-packing:

  • Chọn scheduler pack chặt: mặc định K8s cân bằng pod đều các node; đổi sang chiến lược MostAllocated (bin-packing) gom pod vào node đã dùng nhiều, để trống node khác cho autoscaler tắt.
  • Right-size trước, bin-pack sau: nếu request phồng, bin-packing cũng vô nghĩa vì mỗi pod chiếm chỗ ảo. Sửa request luôn là bước một.
  • Descheduler: công cụ dời pod khỏi node dùng ít để dồn lại, cho phép Cluster Autoscaler xóa node rỗng.

Autoscaling ba tầng

K8s có ba cơ chế autoscaling bổ sung cho nhau, giải quyết ba câu hỏi khác nhau. Hiểu rõ vai trò từng tầng là chìa khóa vừa tiết kiệm vừa ổn định.

Tầng 1 — HPA (Horizontal Pod Autoscaler): scale theo tải

HPA tăng/giảm số lượng pod (replicas) theo một metric — CPU, memory, hoặc metric tùy biến như số request/giây (QPS), độ dài hàng đợi. Tải lên → thêm pod; tải xuống → bớt pod. Đây là tầng phù hợp cho workload co giãn ngang được: API phục vụ dữ liệu, Trino worker, consumer Kafka. Ví dụ: API tra cứu giao dịch giờ hành chính cần 20 pod, ban đêm 3 pod — HPA tự điều chỉnh, không trả tiền cho 20 pod suốt đêm.

Ví dụ minh họa (YAML Kubernetes — không phải SQL, không đánh dấu chạy được):

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: data-api-hpa
  namespace: risk-analytics
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: data-api
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 65   # giữ CPU trung bình ~65% mỗi pod

Tầng 2 — VPA (Vertical Pod Autoscaler): chỉnh kích cỡ pod

VPA giải bài toán right-sizing ở trên: điều chỉnh request/limit của từng pod cho khớp mức dùng thật. Dùng cho workload không co giãn ngang tốt (một tiến trình đơn, ví dụ Airflow scheduler, Trino coordinator) hoặc để lấy gợi ý request cho mọi workload. Lưu ý xung khắc: đừng để VPA và HPA cùng điều khiển cùng một metric CPU/memory trên cùng deployment — chúng sẽ đánh nhau. Thực hành phổ biến: HPA theo metric tùy biến (QPS) còn VPA lo memory, hoặc dùng VPA chỉ ở chế độ gợi ý.

Tầng 3 — Cluster Autoscaler / Karpenter: thêm/bớt node

Hai tầng trên chỉ xếp lại pod; nhưng nếu không đủ node để chứa pod mới, pod sẽ ở trạng thái Pending. Cluster Autoscaler (CA) phát hiện pod Pending vì thiếu chỗ → thêm node; và khi một node rỗng (pod đã dồn đi nơi khác) → xóa node để ngừng trả tiền. Đây là tầng thực sự khép vòng chi phí: HPA/VPA tối ưu cách dùng, CA tối ưu số máy phải trả tiền.

Karpenter (của AWS, mã nguồn mở) là thế hệ mới thay CA: thay vì bị buộc vào các node group cố định, Karpenter chọn đúng loại và kích cỡ instance theo nhu cầu pod đang chờ, provision trong vài chục giây, và consolidate (gom) node để giảm lãng phí liên tục. Với cụm dữ liệu có workload đa dạng (job Spark cần nhiều RAM, API cần nhiều CPU), Karpenter thường pack tốt và rẻ hơn CA truyền thống.

Điểm vàng cho chi phí batch: scale-to-zero. Cụm chạy job batch (Spark ETL ban đêm, retrain model cuối tuần) có thể tụt về 0 node khi không có job — trả 0 đồng compute lúc rảnh, tự bật node khi job tới. Đây là khác biệt lớn nhất so với warehouse luôn-bật.

Spot / preemptible node cho workload chịu gián đoạn

Cloud bán năng lực dư thừa với giá spot/preemptible rẻ hơn on-demand 60–90%, đổi lại có thể bị thu hồi (reclaim) bất cứ lúc nào với thông báo trước rất ngắn (thường ~30 giây với AWS Spot, ~30 giây với GCP preemptible). Đây là đòn tiết kiệm lớn nhất trên K8s cho đúng loại workload.

Nguyên tắc: chỉ đặt lên spot những gì chịu được gián đoạn (fault-tolerant, interruptible):

Đặt lên SPOT (an toàn)Giữ ON-DEMAND (nhạy cảm)
Spark executor (mất thì driver tính lại task)Spark driver (mất là chết cả job)
Job batch/ETL có thể chạy lạiTrino coordinator, database stateful
Retrain model, backfillAPI phục vụ khách hàng cần SLA cao
Consumer stream có checkpointAirflow scheduler / metadata DB

Cách gắn workload vào đúng loại node là dùng taint & tolerationnode affinity: gắn taint (vết đánh dấu) lên spot node để mặc định pod không được lên đó; chỉ pod có toleration tương ứng mới được xếp lên. Kết hợp nodeAffinity/nodeSelector để đẩy executor lên nhóm spot, đẩy driver lên nhóm on-demand.

# Minh họa — pod chịu được gián đoạn, ưu tiên chạy trên spot node
spec:
  tolerations:
    - key: "workload-type"
      operator: "Equal"
      value: "spot"
      effect: "NoSchedule"
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          preference:
            matchExpressions:
              - key: "capacity-type"
                operator: "In"
                values: ["spot"]

Thực hành an toàn: trộn spot + on-demand (ví dụ 70% spot, 30% on-demand làm nền), phân tán qua nhiều loại instance để giảm rủi ro thu hồi đồng loạt, và với Spark bật cấu hình chịu mất executor (executor decommissioning) để dời shuffle data trước khi node bị lấy. Với NCB, workload phục vụ khách trực tiếp (core banking, API) luôn on-demand; còn ETL ban đêm và retrain model là ứng viên hoàn hảo cho spot.

Chọn loại node đúng workload

Cloud có nhiều họ instance với tỷ lệ CPU:RAM khác nhau. Chọn sai họ = trả tiền cho tài nguyên không dùng:

  • Compute-optimized (nhiều CPU, ít RAM — họ c của AWS): API, tính toán nặng CPU, một số Trino worker.
  • Memory-optimized (nhiều RAM — họ r/x): Spark với shuffle/cache lớn, in-memory cache, Trino xử lý join lớn.
  • General purpose (họ m): workload cân bằng, khi chưa rõ đặc tính.

Nếu Spark executor liên tục OOM trên node compute-optimized, đó là tín hiệu cần chuyển sang memory-optimized thay vì cứ tăng số node. Karpenter tự chọn họ phù hợp theo request pod, nhưng vẫn nên định hình bằng khai báo request đúng và nhóm node có chủ đích. Cân nhắc thêm ARM (Graviton): kiến trúc ARM thường rẻ hơn ~20% với hiệu năng/giá tốt cho nhiều workload dữ liệu, miễn image container build đa kiến trúc.

Namespace quota & limit ranges — rào chắn tránh tràn

Trong cụm chia sẻ nhiều team, cần rào chắn để một team không nuốt hết tài nguyên (và ngân sách) của cả cụm:

  • ResourceQuota: đặt trần tổng request/limit CPU và memory (và số pod/PVC) cho mỗi namespace. Ví dụ namespace marketing-bi không được request quá 200 vCPU tổng. Đây vừa là kỷ luật kỹ thuật vừa là ranh giới chi phí ngầm — quota gián tiếp giới hạn tiền team có thể tiêu.
  • LimitRange: đặt mặc địnhtrần/sàn request-limit cho từng pod/container trong namespace. Nó chặn hai lỗi kinh điển: pod quên khai request (mặc định về 0, phá vỡ scheduling và cost allocation) và pod đòi request khổng lồ vô lý.

Cặp ResourceQuota + LimitRange biến "cụm chia sẻ hỗn loạn" thành "cụm chia sẻ có kỷ luật", và là điều kiện tiên quyết để quy chi phí về team cho công bằng.

Dọn tài nguyên bỏ quên

Y như storage tiering có object mồ côi, K8s có tài nguyên mồ côi (orphaned) âm thầm đốt tiền:

  • Pod/Deployment zombie: job test bật rồi quên, deployment cũ không ai dùng nhưng vẫn giữ request → giữ node.
  • PVC (PersistentVolumeClaim) mồ côi: pod đã chết nhưng volume (đĩa) còn đó, vẫn tính tiền lưu trữ. Đây là nguồn rò rỉ chi phí rất hay bị bỏ sót.
  • LoadBalancer mồ côi: mỗi Service loại LoadBalancer tạo một load balancer thật ở cloud (tốn phí cố định/tháng); service xóa dở khiến LB treo lơ lửng tính tiền.
  • Namespace/môi trường ephemeral dựng cho một lần test rồi bỏ quên nguyên cụm.

Giải pháp: quét định kỳ (Kubecost, kubectl + script, hoặc công cụ như Popeye) tìm PVC unbound, LB không gắn service, deployment 0 traffic; đặt TTL cho môi trường ephemeral; và tắt môi trường dev/UAT ngoài giờ — scale namespace dev về 0 replica lúc 20h, bật lại 7h, cắt ~65% chi phí dev (nghỉ ~16h/ngày + cuối tuần).

Quy chi phí về team — OpenCost / Kubecost

Đây là mảnh ghép trả lời câu hỏi mở đầu: làm sao xé hóa đơn node cho từng team? OpenCost (dự án CNCF, mã nguồn mở — chuẩn tham chiếu cho cost allocation trên K8s) và Kubecost (bản thương mại xây trên OpenCost) làm đúng việc đó.

Cách chúng hoạt động, khái quát: lấy giá node thật (từ billing/pricing API của cloud, phân biệt on-demand vs spot), rồi phân bổ xuống từng pod theo tài nguyên pod đó chiếm — thường tính theo max(request, dùng thật) cho CPU/memory, cộng chi phí storage (PVC) và network. Nhờ đó mỗi pod có một "giá tiền", và cộng dồn theo nhãn (label) team/app/env/namespace để ra chi phí từng chiều.

Điều kiện để cost allocation đáng tin: gắn nhãn nhất quán (label team, cost-center, env trên mọi pod) và xử lý chi phí dùng chung (idle capacity của node, control plane, hệ thống monitoring) — thường phân bổ tỷ lệ (proportional) cho các team hoặc dồn vào một "namespace nền tảng" rồi showback minh bạch. Đầu ra nối thẳng vào mô hình showback/chargeback đã dựng ở FinOps 6 — Giám sát chi phí & phát hiện bất thường: showback cho team thấy mình tiêu bao nhiêu, chargeback tính tiền thật về trung tâm chi phí. Không có lớp này, K8s là một cục chi phí mù không ai chịu trách nhiệm — có nó, mỗi team tự nhìn thấy và tự tối ưu phần của mình.

Một chỉ số Kubecost hay dùng là efficiency: tỷ lệ dùng-thật / đã-request. Efficiency thấp (ví dụ 20%) trên một namespace là cờ đỏ right-sizing — đúng đòn bẩy ở mục đầu bài.

Use case thực tế

Bối cảnh: NCB chuyển pipeline dữ liệu và một số API phục vụ sang một cụm EKS (Kubernetes trên AWS) dùng chung: Airflow (KubernetesExecutor), Spark on K8s cho ETL đêm, Trino cho truy vấn ad-hoc, và ~10 API phục vụ dữ liệu. Sau 3 tháng, hóa đơn cụm ~900 triệu VND/tháng nhưng utilization trung bình chỉ ~22% — cờ đỏ over-provisioning. Không ai biết team nào tiêu bao nhiêu.

Triển khai (số minh họa):

  1. Visibility (tuần 1): Cài Kubecost. Phát hiện efficiency toàn cụm 22%: request CPU/memory phồng gấp ~4 lần dùng thật; namespace platform chiếm 40% chi phí phần lớn là idle capacity của node.
  2. Right-sizing (tuần 2–3): Bật VPA chế độ recommendation, chỉnh request về P75 mức dùng thật cho Airflow, Trino worker, các API. Request CPU tổng giảm ~55%.
  3. Bin-packing + Karpenter (tuần 3): Thay node group cố định bằng Karpenter với consolidation. Số node đỉnh từ 45 xuống 18; ban đêm ngoài cửa sổ ETL, cụm co về ~5 node.
  4. Spot cho batch (tuần 4): Đẩy Spark executor và job retrain lên nhóm spot (taint + toleration), giữ Spark driver và API khách hàng trên on-demand. Bật executor decommissioning để chịu thu hồi. Riêng phần Spark tiết kiệm ~72% so với on-demand.
  5. Dọn dẹp + tắt dev ngoài giờ: Xóa 14 PVC mồ côi và 2 LoadBalancer treo; đặt job scale namespace dev/UAT về 0 lúc 20h–7h và cuối tuần.
  6. Chargeback (tuần 5): Dùng label team/cost-center, Kubecost xuất báo cáo chi phí theo namespace; chuyển từ showback sang chargeback với 3 domain lớn.

Kết quả sau 2 tháng: hóa đơn cụm từ ~900tr xuống ~360tr/tháng (giảm ~60%), efficiency lên ~68%, không sự cố OOM tăng thêm (nhờ chỉnh request theo P75/P99 chứ không bóp bừa). Quan trọng nhất: mỗi domain giờ nhìn thấy chi phí K8s của mình và tự canh — đúng vòng phản hồi OPERATE của FinOps.

Ghi nhớ

  • Chi phí K8s khó nhìn vì hóa đơn tính theo node còn giá trị tiêu theo pod: nhiều team chia sẻ node, có khoảng cách lớn giữa request (trả tiền) và dùng thật, và cụm rất động. Cloud cost tool mù về pod/team.
  • Request quyết định chi phí (scheduler + autoscaler dựa vào nó), không phải dùng thật. Request quá cao → lãng phí giữ chỗ (lỗi phổ biến nhất); quá thấp → OOM/throttle. Đo thật rồi chỉnh: request ≈ P50–P75, limit memory ≈ P99; VPA gợi ý.
  • Bin-packing: gom pod vào ít node nhất, theo dõi utilization hai chiều (request-vs-capacity ~70–80%, dùng-vs-request cao). Right-size trước, pack sau.
  • Autoscaling ba tầng bổ sung nhau: HPA đổi số pod theo tải; VPA chỉnh request/limit mỗi pod; Cluster Autoscaler/Karpenter thêm/bớt node — khép vòng chi phí, scale-to-zero cho batch. Đừng để HPA & VPA tranh cùng metric.
  • Spot/preemptible rẻ 60–90% cho workload chịu gián đoạn (Spark executor, batch, retrain) qua taint/toleration + node affinity; giữ driver, DB stateful, API SLA cao trên on-demand; trộn spot+on-demand để an toàn.
  • Chọn node đúng: compute-optimized cho CPU, memory-optimized cho Spark/cache; cân nhắc ARM/Graviton rẻ hơn ~20%.
  • ResourceQuota + LimitRange: rào chắn tài nguyên (và ngân sách ngầm) cho từng namespace; chặn pod quên request / đòi vô lý.
  • Dọn mồ côi: PVC unbound, LoadBalancer treo, deployment zombie, môi trường ephemeral bỏ quên; tắt dev/UAT ngoài giờ cắt ~65% chi phí dev.
  • Quy chi phí bằng OpenCost/Kubecost: phân bổ giá node xuống pod theo max(request, dùng thật) rồi cộng theo label team/envshowback/chargeback (nối FinOps 6); xử lý idle/shared capacity minh bạch. Chỉ số efficiency thấp là cờ đỏ right-sizing.

Bài viết liên quan

CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.

13 thg 7, 2026 8

Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.

13 thg 7, 2026 5

Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.

13 thg 7, 2026 5

Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.

13 thg 7, 2026 5