FinOps 4 — Chi phí compute & pipeline
Compute: khoản đắt nhất và hay lãng phí nhất
Trong ba trụ chi phí dữ liệu — storage, compute, network — thì compute thường là khoản lớn nhất và cũng dễ lãng phí nhất. Lý do: storage tăng đều và dễ nhìn (xem FinOps 3 — Chi phí lưu trữ & phân tầng), còn compute thì bùng nổ theo từng job, từng cluster, từng lần retry — và phần lớn tiền trôi đi khi máy đang bật mà không làm gì có ích. Một cụm Spark 20 node bật 24/7 để chạy vài giờ ETL đêm là ví dụ kinh điển: bạn trả tiền cho 24 giờ nhưng dùng thật 4 giờ.
Tư duy FinOps về compute (xem FinOps 1 — Tổng quan) gói gọn trong một câu: chỉ trả cho đúng lượng máy, đúng loại máy, trong đúng khoảng thời gian thực sự cần. Bài này đi qua bốn nhóm đòn bẩy: (1) chọn và mua hạ tầng đúng cách (right-sizing, spot, autoscaling, reserved), (2) tối ưu chính công việc xử lý (Spark/xử lý phân tán), (3) tối ưu lịch và điều phối (chạy khi cần, incremental, tắt lúc rảnh), (4) dọn tài nguyên bỏ quên. Bối cảnh xuyên suốt: cụm Spark/ETL chạy đêm của một ngân hàng.
Chọn và mua hạ tầng đúng cách
Right-sizing: đúng loại và đúng kích cỡ máy
Right-sizing là chọn instance đủ dùng, không thừa. Sai lầm phổ biến là over-provision — cấp máy to "cho chắc", rồi CPU chạy 15%, RAM dùng 30%. Bạn trả 100% cho phần lớn công suất bỏ không.
Hai chiều của right-sizing:
- Đúng kích cỡ (size): nhìn vào metric sử dụng thực (CPU, RAM, I/O) trong vài tuần, chọn cỡ máy sao cho tải đỉnh dùng ~60–80% tài nguyên. Cao hơn thì rủi ro nghẽn, thấp hơn thì lãng phí.
- Đúng họ máy (family/type): mỗi workload có "hình dạng" tài nguyên riêng. Job Spark shuffle nặng cần memory-optimized (nhiều RAM); job nén/giải mã, ML feature engineering cần compute-optimized (nhiều CPU); job đọc/ghi lake lớn cần băng thông mạng và đĩa tốt. Chạy job memory-bound trên máy compute-optimized nghĩa là trả tiền CPU thừa mà vẫn thiếu RAM.
Với Spark, right-sizing còn là chuyện số executor × cỡ mỗi executor: nhiều executor nhỏ tăng song song nhưng tốn overhead; ít executor to giảm overhead nhưng dễ nghẽn GC. Chi tiết cân chỉnh xem Spark — Tuning & tối ưu.
Spot / preemptible: giảm 60–90% cho tải chịu gián đoạn
Cloud bán công suất dư dưới dạng spot instance (AWS) / preemptible VM (GCP) / spot VM (Azure) với giá rẻ hơn 60–90% so với on-demand. Đánh đổi: nhà cung cấp có thể thu hồi máy bất cứ lúc nào (thường báo trước 30 giây–2 phút) khi cần công suất cho khách trả giá on-demand.
Vì thế nguyên tắc vàng: spot chỉ dùng cho workload chịu được gián đoạn.
- Hợp với spot: batch ETL, Spark job có thể chạy lại, training ML có checkpoint, xử lý hàng đợi. Spark vốn thiết kế chịu lỗi — mất một executor thì task được lập lịch lại trên node khác, chỉ chậm chứ không hỏng kết quả.
- KHÔNG dùng spot: job trọng yếu, nhạy độ trễ, không được phép trễ hạn — ví dụ pipeline sinh báo cáo bắt buộc nộp NHNN trước giờ chốt, hệ thống chấm điểm giao dịch thời gian thực, driver của một job dài không có checkpoint.
Chiến lược thực dụng cho cụm Spark đêm: chạy driver + một phần worker tối thiểu trên on-demand (đảm bảo job không chết hẳn), phần worker mở rộng dùng spot để rẻ. Cấu hình fallback: nếu không xin được spot thì tự động dùng on-demand để job vẫn xong. Nhiều nền tảng (EMR, Dataproc, Databricks) hỗ trợ mô hình "mixed instance" này sẵn.
Autoscaling và scale-to-zero
Autoscaling là để hệ thống tự thêm/bớt node theo tải thay vì cố định số máy. Hai kiểu:
- Horizontal: thêm/bớt số node (phổ biến cho Spark, Kubernetes).
- Vertical: đổi cỡ máy (ít dùng cho compute phân tán).
Với cluster xử lý, autoscaling giúp cụm phình ra khi có job nặng và co lại khi tải nhẹ. Quan trọng nhất là scale-to-zero: khi không có job nào, cụm co về 0 node (hoặc tự tắt hẳn), bạn không trả gì cho compute. Đây là điểm serverless và các "job cluster" (cụm tạo riêng cho một job rồi tự huỷ) làm rất tốt: cụm chỉ tồn tại đúng lúc chạy.
Cạm bẫy autoscaling: thời gian khởi động (cold start — vài phút để cấp và bootstrap node) khiến job ngắn không kịp hưởng lợi; và cấu hình scale-down chậm khiến node rảnh vẫn nằm chờ tính tiền. Đặt ngưỡng và độ trễ scale-down hợp lý (đủ tránh giật cục nhưng không giữ node rảnh quá lâu).
Reserved / Savings plan cho tải ổn định
Spot rẻ nhưng bấp bênh; on-demand linh hoạt nhưng đắt. Với phần tải luôn chạy, đoán trước được (ví dụ cụm điều phối Airflow, database vận hành, một baseline worker luôn bật), bạn nên cam kết trước để lấy giá rẻ:
- Reserved Instance / Committed Use: cam kết dùng một lượng cụ thể trong 1–3 năm, đổi lấy chiết khấu ~30–60%.
- Savings Plan: cam kết một mức chi tiêu $/giờ (linh hoạt hơn về loại máy), đổi lấy chiết khấu tương tự.
Nguyên tắc phối hợp ba lớp: reserved/savings cho baseline ổn định + on-demand cho phần co giãn dự đoán được + spot cho phần bùng nổ chịu gián đoạn. Đừng reserved cho tải hay thay đổi (khoá vốn vào máy không dùng), cũng đừng để 100% on-demand cho baseline chạy quanh năm (trả giá đắt oan).
| Mô hình mua | Giá tương đối | Rủi ro bị thu hồi | Dùng cho |
|---|---|---|---|
| On-demand | Chuẩn (100%) | Không | Tải thất thường, không đoán trước |
| Reserved / Savings | ~40–70% | Không | Baseline ổn định, chạy quanh năm |
| Spot / preemptible | ~10–40% | Có (thu hồi bất kỳ lúc nào) | Batch/Spark chịu gián đoạn |
Tối ưu chính công việc xử lý (Spark)
Chọn máy rẻ mới là một nửa; nửa còn lại là job làm ít việc thừa hơn. Một job Spark quét thừa 10× dữ liệu thì máy rẻ 90% vẫn tốn. Các đòn bẩy chính (chi tiết ở Spark — Tuning & tối ưu):
- Giảm shuffle: shuffle (xáo trộn dữ liệu giữa các node cho
JOIN/GROUP BY) là thao tác đắt nhất — ghi đĩa, truyền mạng cross-node. Giảm bằng cách broadcast bảng nhỏ (broadcast join), lọc/aggregate trước khi join, chọn khoá phân vùng hợp lý. - Xử lý data skew: khi một khoá chiếm phần lớn dữ liệu (ví dụ một tài khoản tổng hợp có hàng triệu giao dịch), một task ôm toàn bộ trong khi các task khác rảnh — cả cụm chờ một task. Xử lý bằng salting hoặc adaptive query execution.
- Partition hợp lý: quá ít partition thì không tận dụng song song; quá nhiều thì overhead lập lịch. Cân chỉnh
spark.sql.shuffle.partitionstheo khối lượng thật. - Tránh đọc thừa: dùng predicate pushdown (chỉ đọc partition/block khớp filter) và column pruning (chỉ đọc cột cần) — điều này đòi định dạng Parquet/ORC thay vì CSV. Đọc đúng cột và đúng partition có thể cắt 90%+ byte quét.
- Cache đúng chỗ:
cache()/persist()chỉ giá trị khi một DataFrame được dùng lại nhiều lần; cache bừa thì tốn RAM/đĩa mà chẳng lợi. Cache dữ liệu đã lọc nhỏ, không cache bảng thô khổng lồ. - Tránh job chạy vô ích: xoá bước tính toán không ai dùng kết quả, gộp nhiều action đọc lại cùng dữ liệu, tránh
count()/collect()debug còn sót trong production (mỗi cái kích hoạt một lần quét).
Tối ưu lịch và điều phối
Đây là nhóm đòn bẩy ROI cao mà hay bị bỏ qua, vì nó không đụng đến code xử lý — chỉ là chạy ít hơn và đúng lúc hơn.
Incremental thay full
Nguyên tắc quan trọng nhất: chỉ xử lý dữ liệu mới/đổi, không xử lý lại toàn bộ mỗi lần. Một bảng transactions 5 năm mà đêm nào cũng quét full để tính tổng hợp là lãng phí kinh điển — 99,9% dữ liệu không đổi từ hôm qua. Xử lý incremental chỉ đọc phần dữ liệu trong cửa sổ thời gian gần (ví dụ giao dịch phát sinh hôm nay) rồi cập nhật/gộp vào kết quả.
Minh hoạ nguyên tắc incremental trên sandbox: thay vì quét toàn bảng, chỉ lọc giao dịch trong khoảng gần đây rồi tổng hợp — đúng thứ một job incremental làm mỗi lần chạy:
-- ▶ Chạy được
SELECT
DATE(created_at) AS ngay,
kind AS loai_gd,
COUNT(*) AS so_giao_dich,
SUM(amount) AS tong_tien
FROM transactions
WHERE created_at >= NOW() - INTERVAL '1 day'
GROUP BY DATE(created_at), kind
ORDER BY ngay, loai_gd;
Điểm mấu chốt là mệnh đề WHERE created_at >= ... cắt phạm vi xử lý về đúng phần dữ liệu mới. Trên data lake, cùng logic này chạy với partition theo ngày để engine chỉ đọc đúng thư mục hôm nay (partition pruning) — byte quét, thời gian chạy và tiền compute đều giảm theo tỷ lệ dữ liệu mới trên tổng. Đây cũng là triết lý của mô hình incremental trong dbt (xem dbt — Incremental & hiệu năng).
Chỉ chạy khi cần, gộp job, tránh reprocessing
- Chạy đúng tần suất: nhiều dashboard làm mới mỗi giờ dù nghiệp vụ chỉ cần dữ liệu ngày. Hạ tần suất về đúng nhu cầu cắt tiền tuyến tính.
- Gộp job nhỏ: hàng chục job Spark tí hon, mỗi cái tự dựng cụm rồi tắt, tốn thời gian cold-start hơn cả thời gian xử lý. Gộp thành một pipeline dùng chung cụm.
- Tránh reprocessing không cần: mỗi lần chạy lại toàn bộ lịch sử (backfill) là một cú compute đắt. Chỉ backfill khi thật sự cần (đổi logic, sửa lỗi dữ liệu), và giới hạn phạm vi thời gian.
Tắt dev ngoài giờ, dừng cluster nhàn rỗi
- Tắt môi trường dev/test ngoài giờ làm việc: cụm dev bật 24/7 nhưng chỉ dùng 8×5 giờ → tắt buổi tối và cuối tuần cắt ~70% thời gian bật. Dùng scheduler tự bật/tắt (ví dụ tắt 20h, bật 7h ngày làm việc).
- Idle timeout: đặt cụm/notebook tự tắt sau N phút không hoạt động. Một notebook Databricks/EMR quên tắt cuối ngày có thể chạy cả đêm vô ích; idle timeout 30–60 phút chặn khoản này.
- Job cluster thay vì cụm chung luôn bật: mỗi job production tạo cụm riêng, chạy xong tự huỷ — không có cụm nào "nằm chờ".
Serverless vs cluster
Chọn mô hình theo hình dạng tải:
- Serverless (BigQuery, Athena, serverless Spark/warehouse): trả theo lượng dùng (byte quét hoặc thời gian thực thi), scale-to-zero, không quản hạ tầng. Hợp với tải thưa, thất thường, khó đoán — job chạy vài lần/ngày, ad-hoc query. Rẻ vì không trả cho lúc rảnh.
- Cluster (cụm được cấp sẵn): trả theo thời gian máy bật. Hợp với tải dày, liên tục — pipeline chạy suốt, nhiều job nối nhau, nơi cụm luôn có việc. Ở tải cao, cluster (nhất là có reserved + spot) thường rẻ hơn serverless tính theo đơn giá.
Ranh giới: tải càng thưa và bùng nổ → nghiêng serverless; tải càng dày và đều → nghiêng cluster có cam kết. Nhiều tổ chức trộn cả hai: serverless cho ad-hoc/BI, cluster cho ETL đêm nặng.
Dọn tài nguyên bỏ quên (zombie)
Giống small files trong storage, compute cũng có "mỡ thừa" là tài nguyên đã cấp mà không ai dùng:
- Zombie cluster/VM: cụm bật cho một thử nghiệm rồi quên tắt; VM của một dự án đã đóng vẫn chạy hằng tháng.
- Đĩa mồ côi (orphan disk): volume (EBS/Persistent Disk) còn tồn sau khi VM đã xoá — không gắn vào đâu nhưng vẫn tính tiền theo dung lượng cấp phát.
- IP tĩnh, load balancer, endpoint cấp rồi bỏ.
- Snapshot/AMI cũ tích tụ (giao thoa với storage).
Cách kiểm soát: quét định kỳ (hằng tuần/tháng) tìm tài nguyên có mức sử dụng ~0 hoặc không gắn với dịch vụ nào; gắn tag bắt buộc (owner, project, môi trường) để truy ra chủ và tự xoá thứ không tag; đặt budget alert cảnh báo khi chi phí một dịch vụ tăng bất thường (xem FinOps 6 — Giám sát & bất thường). Một đợt dọn zombie định kỳ thường thu hồi 5–15% hoá đơn compute mà không ảnh hưởng gì tới production.
Use case thực tế
Bối cảnh: NCB chạy ETL đêm trên một cụm Spark cố định 20 node on-demand, bật 24/7 để phục vụ khoảng 3 giờ xử lý mỗi đêm (tổng hợp giao dịch ngày, cập nhật data mart, tính chỉ số rủi ro). Job đọc bảng transactions dạng CSV và quét full lịch sử mỗi đêm để tính lại tổng hợp. Hoá đơn compute cụm này ~14.000 USD/tháng.
Chẩn đoán:
- Cụm bật 24 giờ nhưng dùng thật ~3 giờ → ~87% thời gian trả tiền cho máy rảnh.
- 20 node cố định, không autoscale → giờ thấp điểm vẫn full.
- Quét full lịch sử mỗi đêm dù chỉ dữ liệu hôm qua là mới.
- CSV → không column/predicate pushdown, quét thừa cột.
- Toàn on-demand, không spot, không reserved.
Hành động:
- Chuyển sang job cluster tự dựng–tự huỷ: cụm chỉ tồn tại ~3 giờ chạy, xong tự tắt (scale-to-zero) → cắt ~87% thời gian bật.
- Autoscale 4–20 node + phần worker mở rộng dùng spot (driver + 4 node baseline on-demand để đảm bảo job không chết).
- Đổi bảng sang Parquet, partition theo ngày; job chuyển sang incremental — chỉ đọc partition hôm nay rồi merge, thay vì quét 5 năm.
- Đặt idle timeout cho cụm dev và lịch tắt dev ngoài giờ; quét zombie hằng tuần.
- Mua savings plan cho phần baseline (điều phối Airflow + 4 node on-demand luôn cần).
Kết quả (minh hoạ, đơn giá gần đúng): compute cụm ETL từ ~14.000 còn ~2.500 USD/tháng — giảm ~82%. Trong đó: chỉ chạy 3h thay 24h (giảm ~87% thời gian), spot cho worker (giảm thêm ~70% phần đó), incremental + Parquet cắt thời gian mỗi lần chạy còn ~1/3. Đổi lại rủi ro spot bị thu hồi được che bằng fallback on-demand nên job vẫn xong trước giờ chốt; các báo cáo trọng yếu nộp NHNN vẫn giữ trên on-demand để đảm bảo đúng hạn.
Ghi nhớ
- Compute thường là khoản đắt nhất và lãng phí nhất; phần lớn tiền trôi đi khi máy bật mà không làm việc có ích.
- Right-sizing: chọn đúng cỡ (tải đỉnh ~60–80% tài nguyên) và đúng họ máy (memory- vs compute-optimized) theo hình dạng workload; tránh over-provision.
- Spot/preemptible rẻ 60–90% nhưng bị thu hồi bất kỳ lúc nào → chỉ dùng cho batch/Spark chịu gián đoạn, KHÔNG cho job trọng yếu/nhạy độ trễ; luôn có fallback on-demand.
- Autoscaling + scale-to-zero: co giãn theo tải, về 0 khi rảnh; chú ý cold start và độ trễ scale-down.
- Reserved/Savings cho baseline ổn định; phối ba lớp: reserved (baseline) + on-demand (co giãn) + spot (bùng nổ).
- Tối ưu Spark: giảm shuffle & data skew, partition hợp lý, predicate/column pruning (Parquet), cache đúng chỗ, bỏ job/action vô ích.
- Incremental thay full là đòn bẩy lịch mạnh nhất: chỉ xử lý dữ liệu mới (
WHERE created_at >= ...+ partition pruning). - Chỉ chạy đúng tần suất, gộp job nhỏ, tránh reprocessing/backfill thừa; tắt dev ngoài giờ và đặt idle timeout cho cụm nhàn rỗi.
- Serverless cho tải thưa/thất thường (trả theo dùng), cluster cho tải dày/liên tục (trả theo giờ bật, rẻ hơn khi có cam kết).
- Dọn định kỳ zombie: cluster/VM/disk mồ côi, IP/LB/snapshot bỏ quên — gắn tag bắt buộc và budget alert.
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.
Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.
Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.
Đả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.