Iceberg 2 — Kiến trúc metadata: catalog, snapshot, manifest

13 thg 7, 2026 3 lượt xem
#architecture
#data-engineering
#snapshot
#metadata
#iceberg

bài tổng quan chúng ta đã thấy Iceberg là một table format mở: nó không thay thế storage (S3, HDFS) hay engine (Spark, Trino, Flink), mà đứng ở giữa để định nghĩa "một bảng gồm những file nào". Toàn bộ sức mạnh của Iceberg — ACID, time travel, schema evolution, query nhanh trên hàng tỉ dòng — đều nằm ở cây metadata. Hiểu đúng cây này là hiểu 80% Iceberg. Bài này mổ xẻ từng tầng, đúng theo spec, rồi cho thấy một câu ghi và một câu đọc thực sự chạm vào những file nào.

Vấn đề mà Iceberg giải quyết

Bảng Hive kiểu cũ định nghĩa bảng bằng thư mục: s3://bucket/txn/dt=2026-07-13/ chứa các file Parquet, và Hive Metastore chỉ lưu danh sách partition. Để biết một partition có những file nào, engine phải LIST thư mục trên object store. Trên S3, LIST là thao tác chậm, phân trang, và eventually consistent — hai job ghi cùng lúc có thể thấy trạng thái khác nhau. Không có khái niệm "phiên bản bảng", nên không có ACID thật, không rollback, không time travel.

Iceberg lật ngược mô hình: bảng là một danh sách file được liệt kê tường minh trong metadata, không phải "mọi thứ nằm trong thư mục này". Danh sách đó được version hoá. Engine không bao giờ cần LIST storage để biết bảng gồm file nào — nó đọc metadata. Đây là thay đổi kiến trúc cốt lõi kéo theo mọi tính năng còn lại.

Ba tầng, năm loại file

Cây metadata Iceberg có thể tóm gọn thành một chuỗi phân cấp. Từ trên xuống:

Đi từng tầng một.

1. Catalog — con trỏ tới metadata hiện hành

Catalog là tầng duy nhất giữ trạng thái có thể thay đổi. Nhiệm vụ của nó cực đơn giản: với mỗi bảng (db.table), catalog lưu đường dẫn tới metadata file hiện hành. Chỉ vậy. Toàn bộ phần còn lại của cây là immutable (bất biến) — file đã ghi thì không sửa, chỉ tạo file mới.

Vì catalog chỉ giữ một con trỏ, "commit một phiên bản bảng mới" quy về đổi con trỏ đó một cách nguyên tử từ v3.metadata.json sang v4.metadata.json. Đây chính là điểm neo cho ACID (xem tầng snapshot).

Có nhiều loại catalog, khác nhau ở chỗ lưu con trỏ ở đâu và cơ chế đổi con trỏ nguyên tử thế nào: REST catalog (chuẩn HTTP mở, engine-agnostic), Hive Metastore, AWS Glue, Nessie (versioning kiểu Git, có branch/tag), Polaris (REST catalog của Snowflake, nay là Apache Polaris). Bài catalog & engine đi sâu phần này. Điểm cần nhớ ở đây: catalog là "sổ hộ khẩu" trỏ tới metadata file, không chứa dữ liệu.

2. Metadata file (*.metadata.json) — bản thiết kế của bảng

Đây là file JSON mô tả toàn bộ trạng thái logic của bảng tại một phiên bản:

  • schema hiện tại và lịch sử schema (mỗi cột có một field-id ổn định — nền tảng cho schema evolution ở bài evolution).
  • partition spec: bảng được phân vùng theo biểu thức nào (ví dụ days(created_at), bucket(16, account_id)). Iceberg dùng hidden partitioning — người viết SQL không cần biết cột partition.
  • sort order: thứ tự sắp xếp dòng bên trong data file (tối ưu cắt tỉa).
  • danh sách snapshot (lịch sử các phiên bản) và current-snapshot-id (phiên bản đang hiện hành).
  • các thuộc tính bảng (properties), UUID bảng, last-updated-ms, v.v.

Mỗi lần commit tạo ra một metadata file mới (v4.metadata.json) sao chép trạng thái cũ rồi thêm snapshot mới. File cũ vẫn còn — đó là lý do time travel về phiên bản cũ chỉ là đọc lại metadata file cũ.

3. Manifest list — mục lục của một snapshot

Mỗi snapshot trỏ tới đúng một manifest list — một file Avro (tên kiểu snap-8342...-1.avro). Manifest list liệt kê tất cả các manifest file thuộc snapshot đó, kèm thống kê tổng hợp ở mức partition cho từng manifest:

  • khoảng giá trị partition (partition-range) mà manifest bao phủ,
  • số data file thêm/xoá/hiện hữu,
  • tổng số dòng, tổng bytes.

Nhờ những thống kê này, engine có thể loại nguyên một manifest khỏi kế hoạch quét mà chưa cần mở nó, nếu partition-range không giao với điều kiện WHERE. Đây là tầng cắt tỉa thứ nhất.

4. Manifest file — sổ chi tiết data file + thống kê cột

Manifest file (cũng là Avro) là nơi thống kê chi tiết nhất. Mỗi bản ghi trong manifest mô tả một file (data file hoặc delete file) với:

  • đường dẫn file, định dạng, kích thước, số dòng;
  • partition data: giá trị partition cụ thể của file đó;
  • thống kê cột theo từng cột: lower_bound (min), upper_bound (max), null_count, value_count, và (tuỳ) nan_count;
  • trạng thái file: ADDED / EXISTING / DELETED.

Chính lớp thống kê min/max/null theo cột này cho phép column pruning: nếu hỏi WHERE amount > 1e9 mà một data file có upper_bound(amount) = 5e8, engine bỏ qua file đó luôn — không đọc một byte dữ liệu nào.

5. Data file và delete file

  • Data file: file dữ liệu thật, thường là Parquet (cũng hỗ trợ ORC, Avro). Đây là nơi chứa các dòng.
  • Delete file: cơ chế Merge-on-Read để hỗ trợ DELETE/UPDATE/MERGE mà không phải viết lại toàn bộ data file. Có hai kiểu:
    • positional delete: đánh dấu "xoá dòng thứ N trong file X" (theo đường dẫn file + số thứ tự dòng).
    • equality delete: đánh dấu "xoá mọi dòng có id = 12345" (theo giá trị cột) — rất hợp với CDC/upsert, xem bài streaming/CDC.

Khi đọc, engine hợp nhất data file với các delete file áp dụng để trả kết quả đúng. Nén định kỳ (compaction) sẽ "vật chất hoá" các delete này để tránh tích tụ, chủ đề của bài maintenance & performance.

Snapshot — trái tim của ACID và time travel

Một snapshot là ảnh chụp bất biến của bảng tại một thời điểm: nó gồm một snapshot-id, timestamp, operation (append/overwrite/delete/replace), và con trỏ tới manifest list của nó.

Mỗi lần commit sinh một snapshot mới trỏ tới tập manifest mới. Vì mọi tầng bên dưới catalog đều bất biến, việc "chuyển bảng sang phiên bản mới" chỉ là đổi current-snapshot-id (thông qua đổi con trỏ metadata ở catalog) — một thao tác nguyên tử. Từ đó:

  • ACID: người đọc luôn thấy một snapshot nhất quán; người ghi tạo snapshot mới song song mà không đụng snapshot đang đọc; commit hoặc thành công trọn vẹn (đổi con trỏ) hoặc thất bại trọn vẹn.
  • Time travel: đọc ... FOR VERSION AS OF <snapshot-id> hay FOR TIMESTAMP AS OF <ts> chỉ là chọn một snapshot cũ trong danh sách rồi đi theo manifest list của nó.
  • Rollback: đưa current-snapshot-id về một snapshot cũ — vẫn là đổi con trỏ.

Chi tiết cơ chế ACID, isolation level, và time travel nằm ở bài ACID & time travel.

Vì sao query nhanh: planning cắt tỉa file, không LIST S3

Đây là điểm khiến Iceberg vượt Hive về hiệu năng planning. Khi engine nhận một câu SELECT ... WHERE created_at = DATE '2026-07-13' AND amount > 1e9, bước query planning diễn ra hoàn toàn trên metadata:

  1. Từ catalog lấy metadata file, lấy snapshot hiện tại → manifest list.
  2. manifest list, loại các manifest có partition-range không giao với 2026-07-13 (partition pruning tầng 1).
  3. manifest file còn lại, loại tiếp từng data file bằng partition data và column stats (amountupper_bound < 1e9 thì bỏ) (pruning tầng 2).
  4. Trả về tập data file tối thiểu cần đọc.

Toàn bộ quá trình này không có một lệnh LIST thư mục nào trên S3 — engine chỉ đọc vài file metadata nhỏ (JSON + Avro). Hive thì ngược lại: phải LIST từng partition directory để biết file, và không có column stats sẵn nên hay quét thừa. Với bảng hàng chục nghìn partition, khác biệt là hàng phút so với hàng chục mili-giây ở khâu planning.

Một câu ghi diễn ra thế nào

Giả sử chạy INSERT/MERGE vào bảng. Vì mọi tầng dưới catalog là bất biến, ghi = tạo file mới từ dưới lên rồi đổi con trỏ ở trên cùng:

  1. Ghi data file mới (và delete file nếu là MERGE/UPDATE/DELETE) vào storage.
  2. Ghi manifest file mới liệt kê các file vừa tạo kèm thống kê cột.
  3. Ghi manifest list mới cho snapshot mới (gồm manifest mới + các manifest cũ còn hiệu lực).
  4. Ghi metadata.json mới (v4) thêm snapshot mới và đặt nó làm current-snapshot-id.
  5. Commit ở catalog: đổi con trỏ bảng từ v3v4 một cách nguyên tử.

Bước 5 dùng optimistic concurrency control: catalog chỉ đổi con trỏ nếu con trỏ hiện tại vẫn là v3 (điều kiện "compare-and-swap"). Nếu một job khác đã commit v4 trước, phép so sánh thất bại → job này retry: đọc lại metadata mới nhất, tái áp dụng thay đổi của mình, thử commit lại. Nhờ vậy nhiều writer chạy song song vẫn an toàn, không khoá bảng, không ghi đè lẫn nhau.

Ví dụ cấu trúc thư mục và metadata.json (minh hoạ)

Cấu trúc thư mục điển hình của một bảng Iceberg trên S3 (minh hoạ, tên file rút gọn):

s3://lake/warehouse/core/transactions/
├── data/
│   ├── created_at_day=2026-07-13/part-0001.parquet
│   ├── created_at_day=2026-07-13/part-0002.parquet
│   └── created_at_day=2026-07-12/part-0003.parquet
└── metadata/
    ├── v3.metadata.json          ← con trỏ catalog đang trỏ tới đây
    ├── v4.metadata.json          ← sau commit tiếp theo
    ├── snap-8342...-1.avro        ← manifest list của snapshot
    ├── a1b2...-m0.avro            ← manifest file
    └── c3d4...-m1.avro

Một metadata.json rút gọn để minh hoạ (không phải cấu hình thật, đã lược nhiều trường):

{
  "format-version": 2,
  "table-uuid": "b3d1...-a91f",
  "current-schema-id": 0,
  "schemas": [
    { "schema-id": 0, "fields": [
      { "id": 1, "name": "id", "type": "long", "required": true },
      { "id": 2, "name": "account_id", "type": "long" },
      { "id": 3, "name": "amount", "type": "decimal(18,2)" },
      { "id": 4, "name": "created_at", "type": "timestamptz" }
    ]}
  ],
  "partition-specs": [
    { "spec-id": 0, "fields": [
      { "source-id": 4, "name": "created_at_day", "transform": "day" }
    ]}
  ],
  "current-snapshot-id": 8342078109,
  "snapshots": [
    {
      "snapshot-id": 8342078109,
      "timestamp-ms": 1752364800000,
      "summary": { "operation": "append", "added-data-files": "2" },
      "manifest-list": "s3://.../metadata/snap-8342...-1.avro"
    }
  ]
}

Chú ý: manifest-list trỏ tới file Avro, chứ metadata.json không liệt kê trực tiếp data file — đó là việc của manifest. Field-id ("id": 1..4) là số ổn định, không đổi khi rename cột.

Use case thực tế

Bối cảnh NCB. Bảng core.transactions lưu giao dịch lõi, khoảng 1,8 tỉ dòng, phân vùng day(created_at), đã tích luỹ ~3 năm1.100 partition-ngày, mỗi ngày ~200 data file Parquet → tổng ~220.000 data file. Câu hỏi vận hành phổ biến từ đội rủi ro: "liệt kê giao dịch > 1 tỉ VND trong ngày hôm qua".

Nếu để trên Hive: engine phải LIST directory của partition ngày (và Metastore trả partition), rồi mở metadata footer của từng Parquet để lấy row-group stats. Với các câu quét vài ngày hoặc khi cache lạnh, khâu liệt kê + mở footer trên S3 dễ tốn hàng chục giây tới vài phút chỉ để lập kế hoạch, chưa đọc dữ liệu.

Trên Iceberg, planning đi qua metadata:

  • Đọc metadata.json + manifest list → loại toàn bộ 1.099 partition-ngày khác, chỉ giữ manifest bao phủ ngày hôm qua (partition pruning tầng 1).
  • Trong manifest của ngày đó, dùng upper_bound(amount) để loại các data file có max < 1 tỉ — thực tế phần lớn giao dịch nhỏ, nên nhiều file bị cắt (column pruning tầng 2).
  • Kết quả: từ ~220.000 file, planning khoanh còn ~200 file/ngày rồi lọc tiếp còn vài chục file cần đọc.

Chi phí planning chuyển từ "LIST + mở hàng nghìn footer trên S3" sang đọc vài file Avro/JSON tổng cỡ vài MB, đưa thời gian lập kế hoạch xuống cỡ mili-giây tới dưới một giây (số liệu ước lượng, phụ thuộc engine, độ phân mảnh manifest và cấu hình). Quan trọng hơn: số lời gọi API tới S3 giảm mạnh — với object store tính tiền theo request, đây vừa là lợi ích tốc độ vừa là lợi ích chi phí. Đội DE NCB duy trì lợi thế này bằng compaction định kỳ để manifest và data file không phân mảnh quá mức (xem bài maintenance).

Cùng cây metadata này còn cho đội rủi ro chạy time travel: "số dư giao dịch bảng nhìn thấy lúc cuối ngày T-1" chỉ là đọc snapshot của thời điểm đó — phục vụ đối soát và audit mà không cần bảng lịch sử riêng.

Ghi nhớ

  • Cây metadata Iceberg: catalog → metadata.json → manifest list → manifest file → data file / delete file. Chỉ catalog là mutable (giữ con trỏ); mọi tầng dưới đều bất biến.
  • metadata.json giữ schema (có field-id ổn định), partition spec, sort order, danh sách snapshot và current-snapshot-id. Nó không liệt kê data file trực tiếp.
  • Manifest list (1 file Avro/snapshot) mang thống kê mức partition; manifest file mang thống kê mức cột (min/max/null_count) cho từng data file. Đây là hai tầng cắt tỉa.
  • Snapshot = ảnh chụp bất biến trỏ tới một manifest list. Commit = tạo snapshot mới rồi đổi con trỏ nguyên tử ở catalog → nền tảng của ACID, time travel, rollback.
  • Query planning đọc metadata + dùng stats để cắt tỉa file, không LIST thư mục S3 — khác hẳn Hive, nên nhanh và rẻ request hơn nhiều.
  • Một câu ghi đi từ dưới lên: data file → manifest → manifest list → metadata → commit ở catalog với optimistic concurrency (retry khi xung đột).
  • Delete file (positional/equality) hiện thực Merge-on-Read cho DELETE/UPDATE/MERGE và CDC mà không viết lại toàn bộ data file.
  • Nhiều loại catalog (REST, Hive, Glue, Nessie, Polaris) chỉ khác nhau ở nơi lưu con trỏ và cách commit nguyên tử — bản chất cây metadata không đổi.

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ẻ!