Iceberg 6 — Bảo trì & Hiệu năng: compaction, small files

13 thg 7, 2026 3 lượt xem
#data-engineering
#performance
#compaction
#iceberg
#maintenance

Một bảng Iceberg vận hành đúng chưa đủ — phải bảo trì

Apache Iceberg cho bạn ACID, time travel, schema evolution — nhưng những năng lực đó không miễn phí về mặt vận hành. Mỗi lần ghi sinh thêm data file, metadata file, manifest; mỗi lần xóa/cập nhật ở chế độ merge-on-read sinh thêm delete file; mỗi commit tạo một snapshot mới giữ tham chiếu tới các file cũ. Nếu chỉ ghi mà không bao giờ dọn, một bảng "chạy tốt" hôm nay sẽ chậm dần và tốn kém dần theo tháng. Bài này nói về mặt ít hào nhoáng nhưng quyết định chi phí thật của Iceberg: làm sao vận hành bảng cho nhanh và rẻ.

Ba nhóm việc chính: (1) trị small files bằng compaction, (2) quản lý vòng đời snapshot và file rác, (3) chăm metadata để planning nhanh. Cuối bài là cách tự động hóa và một tình huống thực tế ở NCB.

Vấn đề small files: từ đâu ra và hại thế nào

Small files là hiện tượng bảng tích tụ rất nhiều data file kích thước nhỏ (vài chục KB đến vài MB) thay vì ít file lớn. Nguồn gốc phổ biến:

  • Streaming ingest / micro-batch: Flink hoặc Spark Structured Streaming commit mỗi vài giây tới vài phút. Mỗi commit ghi ít nhất một file cho mỗi partition nó chạm. Một job đọc Kafka commit mỗi phút, ghi vào bảng phân vùng theo ngày, sẽ tạo ~1.440 file/ngày/partition — chưa kể nhân với số task song song.
  • Nhiều writer / nhiều task: mỗi Spark executor task ghi file riêng. Job 200 task chạm một partition sẽ để lại 200 file dù tổng dữ liệu chỉ vài trăm MB.
  • Upsert/CDC ở chế độ merge-on-read: ngoài data file còn sinh delete file liên tục (xem ACID & MoR).
  • Over-partitioning: phân vùng quá mịn (theo giờ, theo account_id) khiến mỗi partition chỉ nhận vài dòng mỗi lần ghi → file tí hon.

Tác hại không chỉ là "nhiều file cho vui":

Hệ quảVì sao
Query planning chậmEngine phải đọc manifest liệt kê từng data file, đánh giá thống kê, quyết định file nào cần đọc. Hàng vạn file → planning tốn giây tới phút trước khi đọc byte dữ liệu nào.
Đọc chậm (I/O overhead)Object storage (S3/GCS) mỗi lần mở file mất latency cố định (open, HTTP request). 10.000 file 1MB đắt hơn nhiều so với 40 file 256MB dù cùng tổng dung lượng.
Metadata phìnhNhiều data file → nhiều entry manifest → manifest list dài → metadata.json lớn. Bản thân việc đọc/ghi metadata cũng chậm dần.
Chi phí API tăngNhiều request GET/LIST lên object storage = hóa đơn cao hơn, và dễ chạm rate limit.

Nói ngắn: small files bào mòn cả tốc độ lẫn chi phí. Đây là lý do compaction là việc bảo trì số một.

Compaction: rewrite_data_files

Compaction là gộp nhiều file nhỏ thành ít file lớn hơn, kích thước mục tiêu quanh 128–512 MB (mặc định thường 512 MB qua target-file-size-bytes). Iceberg thực hiện qua Spark procedure rewrite_data_files. Bản chất: đọc tập file cũ, viết lại thành file mới đạt target size, rồi commit một snapshot mới thay các file cũ bằng file mới — không đổi dữ liệu logic, chỉ đổi cách bố trí vật lý.

Có hai chiến lược sắp xếp khi viết lại:

  • Bin-pack (mặc định): chỉ gom file cho đủ target size, không sắp lại dữ liệu bên trong. Rẻ, nhanh, mục tiêu duy nhất là trị small files.
  • Sort / z-order (clustering): vừa gộp vừa sắp dữ liệu liên quan nằm gần nhau. SORT sắp theo một hoặc nhiều cột (ví dụ account_id), zorder xen kẽ bit của nhiều cột để gom theo nhiều chiều cùng lúc. Kết quả: min/max của mỗi file "chặt" hơn, engine cắt tỉa (pruning) file tốt hơn khi query lọc theo các cột đó → đọc ít file hơn nữa. Đắt hơn bin-pack vì phải sort, nhưng đáng nếu truy vấn thường lọc/gom theo những cột này.

Minh họa gọi procedure (Spark — MINH HOẠ, không chạy trong sandbox PostgreSQL):

-- MINH HOẠ (Spark) — bin-pack các partition có nhiều file nhỏ
CALL catalog.system.rewrite_data_files(
  table => 'db.txn',
  strategy => 'binpack',
  options => map(
    'target-file-size-bytes','536870912',   -- 512MB
    'min-input-files','5',                    -- chỉ nén partition có >=5 file
    'rewrite-all','false'
  )
);

-- MINH HOẠ (Spark) — sort theo account_id để gom dữ liệu cùng khách
CALL catalog.system.rewrite_data_files(
  table => 'db.txn',
  strategy => 'sort',
  sort_order => 'account_id ASC NULLS LAST',
  where => 'created_at >= date ''2026-07-01''',
  options => map('target-file-size-bytes','536870912')
);

Vài lựa chọn quan trọng: where giới hạn phạm vi nén (chỉ những partition mới) để khỏi viết lại cả bảng; min-input-files bỏ qua partition đã đủ tốt; partial-progress.enabled cho commit từng nhóm để job dài không mất trắng khi lỗi giữa chừng.

Copy-on-write vs merge-on-read ảnh hưởng compaction

Nếu bảng dùng merge-on-read (xem bài 3), compaction làm thêm một việc sống còn: gộp delete file vào data file. Mỗi UPDATE/DELETE ở MoR ghi thêm một delete file; lúc đọc engine phải hợp nhất data ⊕ delete, và delete file càng tích tụ đọc càng chậm. Compaction viết lại data file đã áp delete (dòng bị xóa biến mất khỏi file mới) rồi loại các delete file đã tiêu hóa. Vì thế bảng MoR/CDC cần compaction thường xuyên hơn bảng copy-on-write — không phải để trị small files thường mà để delete file không tồn đọng.

Với bảng copy-on-write, bản thân UPDATE/DELETE đã viết lại nguyên data file nên không có delete file; compaction ở đây thuần túy trị small files sinh từ nhiều lần append nhỏ.

Quản lý snapshot & file rác

Compaction viết file mới nhưng không tự xóa file cũ — vì các snapshot cũ vẫn tham chiếu chúng (để time travel). Nếu chỉ compaction mà không dọn snapshot, dung lượng lưu trữ còn phình to hơn: bạn có cả file cũ lẫn file mới. Ba procedure dọn dẹp bổ trợ nhau:

expire_snapshots — xóa snapshot cũ

expire_snapshots gỡ bỏ các snapshot quá hạn và xóa vật lý những data/manifest file chỉ còn bị các snapshot đó tham chiếu. Đây mới là bước thật sự giải phóng dung lượng sau compaction.

-- MINH HOẠ (Spark) — giữ snapshot 7 ngày gần nhất
CALL catalog.system.expire_snapshots(
  table => 'db.txn',
  older_than => TIMESTAMP '2026-07-06 00:00:00',
  retain_last => 10
);

Đánh đổi cốt lõi: giữ snapshot lâu = time travel/audit xa hơn nhưng tốn dung lượng và metadata; giữ ngắn = rẻ nhưng mất khả năng đọc lại quá khứ. Chọn thời hạn theo nhu cầu nghiệp vụ — bảng chốt số kỳ có thể cần giữ 90 ngày, bảng streaming thô chỉ cần 3–7 ngày. Lưu ý: expire xong thì các snapshot-id/timestamp cũ hơn ngưỡng không còn time travel được nữa; nếu cần neo mốc quan trọng lâu dài, dùng tag (bất biến, không bị expire theo tuổi — xem bài 3) thay vì dựa vào việc giữ toàn bộ lịch sử.

remove_orphan_files — dọn file mồ côi

Orphan file là file nằm trên storage nhưng không snapshot nào tham chiếu — sinh ra từ job ghi chết giữa chừng, task failed để lại file dở, hoặc lỗi commit. Vì không snapshot nào trỏ tới, expire_snapshots không đụng chúng. remove_orphan_files quét thư mục bảng, đối chiếu với metadata, xóa file lạc.

-- MINH HOẠ (Spark) — chỉ xóa file cũ hơn 3 ngày để tránh xóa nhầm file đang ghi
CALL catalog.system.remove_orphan_files(
  table => 'db.txn',
  older_than => TIMESTAMP '2026-07-10 00:00:00'
);

Cảnh báo vận hành: đặt older_than đủ lùi (thường ≥ 3 ngày) để không xóa nhầm file của job đang chạy chưa kịp commit. Đây là thao tác quét toàn thư mục nên đắt — chạy thưa (tuần/tháng), không phải mỗi giờ.

rewrite_manifests — gộp manifest

Ngay cả khi data file đã to đẹp, tầng manifest có thể vẫn phân mảnh: nhiều manifest nhỏ, mỗi cái liệt kê vài file, khiến planning phải mở nhiều manifest. rewrite_manifests gộp và sắp lại manifest (thường theo giá trị partition) để planning quét ít manifest hơn và pruning ở mức partition hiệu quả hơn. Rẻ hơn compaction data nhiều vì chỉ đụng metadata.

-- MINH HOẠ (Spark)
CALL catalog.system.rewrite_manifests('db.txn');

Metadata & planning: để engine đọc ít file nhất

Tốc độ query Iceberg phần lớn được quyết ở bước planning: bỏ qua càng nhiều file càng tốt trước khi đọc. Hai cơ chế cắt tỉa:

  • Partition pruning: bỏ cả partition không khớp điều kiện (ví dụ query một ngày → bỏ mọi partition ngày khác). Phụ thuộc thiết kế partition và hidden partitioning.
  • File pruning theo thống kê: mỗi data file lưu min/max (và null count, số dòng) của từng cột trong manifest. Nếu query lọc amount > 1000000 mà một file có max(amount) = 500000, engine bỏ file đó không cần mở. Đây là lý do sort order quan trọng: sau khi rewrite_data_files sort theo account_id, mỗi file chứa dải account_id hẹp → lọc theo account_id cắt tỉa được nhiều file. File chưa sort thì mỗi file trải rộng toàn dải giá trị, min/max vô dụng cho pruning.

Muốn min/max hữu ích, các metrics phải được thu thập khi ghi. Iceberg cấu hình mức thu thập qua thuộc tính bảng write.metadata.metrics.default (none / counts / truncate(n) / full) và có thể ghi đè theo cột write.metadata.metrics.column.<col>. Mặc định truncate(16) — lưu 16 ký tự đầu cho cột chuỗi, đủ cho pruning mà không phình metadata. Cột thường dùng để lọc (khóa join, cột thời gian, cột nghiệp vụ) nên bật metrics đầy đủ; cột hiếm khi lọc có thể để none để metadata gọn. Bật metrics cho cột chỉ có tác dụng với dữ liệu ghi sau đó — muốn áp cho dữ liệu cũ phải rewrite lại.

Ngoài ra Iceberg còn có bảng metadata để chẩn đoán sức khỏe: files (kích thước/số dòng từng file — soi small files), partitions, manifests, snapshots, history. Đọc files group theo partition là cách nhanh phát hiện partition nào đang ngập file nhỏ cần nén.

Tự động hóa bảo trì

Không ai chạy tay mấy procedure trên mãi. Ba hướng phổ biến:

  1. Spark procedures theo lịch, điều phối bằng Airflow — cách phổ thông nhất khi tự vận hành. Mỗi loại bảo trì một nhịp riêng theo chi phí/lợi ích: compaction dày, expire vừa, orphan/manifest thưa.
  2. Dịch vụ tự tối ưu (managed) — nền tảng như Tabular (nay thuộc Databricks) hay các catalog/managed service tự chạy compaction, expire, clustering ngầm, bạn không phải viết DAG. Đổi lại chi phí dịch vụ và ít quyền kiểm soát chi tiết.
  3. Kết hợp — để engine ghi tự nén nhỏ (nếu hỗ trợ) và job định kỳ lo phần nặng.

Gợi ý nhịp chạy (điểm khởi đầu, chỉnh theo tải thực tế):

ViệcNhịp gợi ýGhi chú
rewrite_data_files (bin-pack)mỗi 1–6 giờ với bảng streaming; hằng ngày với bảng batchchỉ nén partition mới/nóng qua where
rewrite_data_files (sort/z-order)hằng ngày/tuầnđắt, chạy trên partition đã "nguội"
expire_snapshotshằng ngàytheo chính sách giữ (vd 7 ngày)
remove_orphan_fileshằng tuầnolder_than ≥ 3 ngày
rewrite_manifestshằng tuầnrẻ, khi manifest phân mảnh

Đánh đổi chi phí: compaction tiêu CPU/RAM Spark và ghi lại dữ liệu (tốn I/O, tốn tiền storage tạm và compute). Với partition đã nguội (không còn ghi) chỉ cần nén một lần rồi thôi — nén lại vô ích, đốt tiền. Nguyên tắc: chỉ nén phần dữ liệu vừa thay đổi, đừng rewrite-all cả bảng mỗi đêm. Lợi ích (query nhanh hơn, hóa đơn đọc rẻ hơn) phải lớn hơn chi phí compaction thì việc bảo trì mới có lãi.

Use case thực tế

Bối cảnh (số liệu ước lượng minh họa). NCB đưa bảng giao dịch thẻ/chuyển khoản db.txn (mô hình gần với transactions(account_id, amount, kind, created_at)) lên Iceberg trên object storage, nạp gần real-time bằng Flink đọc từ Kafka. Bảng dùng merge-on-read để ghi độ trễ thấp, phân vùng theo ngày (days(created_at)).

Triệu chứng. Sau ~6 tuần, dashboard giám sát giao dịch và các truy vấn đối soát chậm thấy rõ. Chẩn đoán qua bảng metadata files: mỗi partition ngày chứa khoảng 40.000–60.000 data file kích thước 50 KB–2 MB (Flink commit mỗi ~30 giây × nhiều task), cộng hàng nghìn equality delete file tồn đọng. Query lọc một ngày cho một nhóm account_id mất ~90–120 giây, phần lớn là planning và mở hàng vạn file trên S3; hóa đơn GET request tăng vọt.

Xử lý. Đội dựng một Airflow DAG bảo trì:

  1. Compaction + sort mỗi giờ trên partition của ngày hiện tại: rewrite_data_files chiến lược sort theo account_id, target 512 MB, where giới hạn ngày đang ghi. Việc sort vừa gộp file vừa gom giao dịch cùng khách vào cùng file → min/max account_id hẹp lại, pruning theo account_id hiệu quả. Compaction cũng gộp equality delete vào data file, giải quyết tồn đọng delete của MoR.
  2. expire_snapshots hằng ngày, giữ 7 ngày — đủ cho time travel phục vụ đối soát trong tuần và điều tra sự cố, mà không giữ file rác lâu. Các mốc cần lưu dài (ảnh chốt số cuối tháng) được neo bằng tag riêng nên không bị expire.
  3. remove_orphan_files hằng tuần (older_than = 3 ngày) dọn file dở do task Flink/Spark failed để lại; rewrite_manifests hằng tuần gộp manifest.

Kết quả (ước lượng). Sau khi lịch chạy ổn định:

  • Số data file mỗi partition ngày giảm từ ~50.000 xuống ~150–300 file (giảm khoảng 99%).
  • Query đối soát lọc theo ngày + account_id giảm từ ~90–120 giây xuống ~8–15 giây (nhanh khoảng 8–10 lần), chủ yếu nhờ planning ngắn và pruning cắt phần lớn file.
  • Delete file không còn tồn đọng nên độ trễ đọc bảng MoR ổn định thay vì tăng dần.
  • Dung lượng storage sau khi expire giảm ròng (file cũ được giải phóng) dù compaction ghi thêm file mới; số GET request/tháng lên object storage giảm mạnh, kéo chi phí API xuống.

Các con số trên là ước lượng minh họa cho một bảng streaming điển hình, không phải đo lường chính thức; mức cải thiện thực tế tùy khối lượng, phân vùng và mẫu truy vấn.

Ghi nhớ

  • Small files sinh từ streaming/micro-batch, nhiều task, upsert MoR, over-partitioning. Hại: planning chậm, đọc chậm (latency mở file trên object storage), metadata phình, chi phí API tăng.
  • Compaction (rewrite_data_files) gộp file nhỏ về target ~128–512 MB. Bin-pack rẻ, chỉ trị small files; sort/z-order vừa gộp vừa gom dữ liệu liên quan → min/max chặt, pruning tốt hơn. Dùng where/min-input-files để chỉ nén phần nóng.
  • Merge-on-read cần compaction thường xuyên hơn để gộp delete file; copy-on-write không có delete file nên compaction thuần trị small files.
  • Vòng đời snapshot/file: expire_snapshots xóa snapshot cũ và mới thật sự giải phóng dung lượng (đánh đổi với time travel/audit); remove_orphan_files dọn file mồ côi (đặt older_than lùi để không xóa nhầm); rewrite_manifests gộp manifest cho planning nhanh.
  • Metadata & planning: cột min/max giúp file pruning; sort order làm min/max hữu ích; bật đúng metrics (write.metadata.metrics.*) cho cột hay lọc. Dùng bảng metadata files/partitions để soi sức khỏe bảng.
  • Tự động hóa bằng Spark procedures + Airflow theo nhịp riêng từng việc, hoặc dịch vụ managed tự tối ưu. Luôn cân chi phí compaction vs lợi ích đọc: chỉ nén phần vừa thay đổi, đừng rewrite-all mỗi đêm.
  • Xem thêm: tổng quan Iceberg, kiến trúc metadata, ACID & merge-on-read, evolution & partitioning.

Nguồn tham khảo

Bài viết liên quan

So sánh các định dạng dữ liệu (CSV, JSON, XML, Avro, Parquet, ORC) và lý do lưu theo cột nhanh hơn cho phân tích. Bài đi sâu vào row vs columnar storage, nén (Snappy/gzip/zstd), schema evolution, OLTP vs OLAP, object storage và partitioning để tối ưu chi phí lẫn tốc độ truy vấn.

13 thg 7, 2026 10

Data Engineering là ngành xây dựng và vận hành hệ thống biến dữ liệu thô thành dữ liệu sạch, tin cậy, sẵn sàng cho phân tích và AI. Bài giới thiệu vai trò Data Engineer trong vòng đời dữ liệu (nguồn → ingestion → storage → transformation → serving), phân biệt với Analyst/Scientist/ML Engineer, bức tranh hệ sinh thái công cụ và bài toán đưa dữ liệu core banking sang kho phân tích.

13 thg 7, 2026 9

Stream processing là gì, khác biệt batch vs stream (bounded/unbounded), micro-batch (Spark) vs true streaming, Apache Flink là gì và định vị so với Spark Structured Streaming và Kafka Streams. Kiến trúc runtime JobManager/TaskManager, các tầng API, triết lý streaming-first và bối cảnh phát hiện gian lận ngân hàng.

13 thg 7, 2026 8

Vì sao một máy không đủ và cần xử lý phân tán: từ MapReduce, Hadoop/HDFS đến Apache Spark in-memory. Kiến trúc driver–executor–cluster manager, các mức trừu tượng RDD/DataFrame/Dataset, cơ chế lazy evaluation với DAG, và vì sao shuffle (wide dependency) là phần tốn kém nhất. Kèm PySpark, Spark SQL và các kỹ thuật tối ưu (partition, broadcast join, cache, chống skew) cùng khi nào KHÔNG nên dùng Spark.

13 thg 7, 2026 7

Cảm nhận của bạn

Bình luận

Bạn cần để viết bình luận.

Chưa có bình luận. Hãy là người đầu tiên chia sẻ!