Cơ chế Snapshot và Versioning của Apache Iceberg
Apache Iceberg lưu trữ dữ liệu dưới dạng các snapshot không đổi (immutable). Mỗi lần thực hiện ghi (append, update, delete), Iceberg tạo một snapshot mới thay vì sửa đổi file dữ liệu cũ.
Snapshot chứa thông tin metadata về trạng thái của bảng tại thời điểm đó, bao gồm danh sách các file data file và manifest file. Cơ chế này đảm bảo tính nhất quán và cho phép truy vấn dữ liệu ở quá khứ.
Để hiểu rõ, ta sẽ tạo một bảng mẫu và thực hiện các thao tác ghi để sinh ra nhiều phiên bản snapshot.
duckdb -c "CREATE TABLE iceberg_demo AS SELECT 1 AS id, 'initial' AS value;"
Kết quả: Bảng iceberg_demo được tạo với 1 dòng dữ liệu đầu tiên. Iceberg tự động tạo snapshot version 0.
duckdb -c "INSERT INTO iceberg_demo VALUES (2, 'second'), (3, 'third');" --append
Kết quả: Hai dòng mới được thêm vào. Iceberg tạo snapshot version 1. Dữ liệu cũ không bị xóa mà vẫn tồn tại dưới dạng snapshot cũ.
duckdb -c "SELECT * FROM iceberg_demo;"
Kết quả: Hiển thị 3 dòng dữ liệu (id 1, 2, 3) từ snapshot hiện tại (version 1).
duckdb -c "DESCRIBE iceberg_demo;"
Kết quả: Hiển thị cấu trúc bảng. Cần kiểm tra thông tin snapshot bằng lệnh riêng.
duckdb -c "CALL iceberg.list_snapshots('iceberg_demo');" --append
Kết quả: Danh sách các snapshot với column 'snapshot_id', 'parent_id', 'summary' và 'timestamp'. Bạn sẽ thấy 2 snapshot (version 0 và version 1).
Thực hiện truy vấn Time Travel để xem dữ liệu quá khứ
Time Travel cho phép truy vấn bảng tại một snapshot cụ thể hoặc thời điểm cụ thể trong quá khứ. Điều này cực kỳ hữu ích để debug dữ liệu hoặc phục hồi trạng thái trước khi xảy ra lỗi.
Cú pháp chuẩn của Iceberg trong DuckDB là sử dụng từ khóa 'AT SNAPSHOT' hoặc 'AT TIMESTAMP' ngay sau tên bảng.
duckdb -c "SELECT * FROM iceberg_demo AT SNAPSHOT 0;"
Kết quả: Chỉ trả về 1 dòng (id=1, value='initial'). Đây là trạng thái bảng ở snapshot version 0.
duckdb -c "SELECT * FROM iceberg_demo AT SNAPSHOT 1;"
Kết quả: Trả về 3 dòng (id 1, 2, 3). Đây là trạng thái hiện tại.
duckdb -c "SELECT * FROM iceberg_demo AT TIMESTAMP '2024-01-01 00:00:00';"
Kết quả: Nếu thời gian này trước khi tạo snapshot 0, trả về 0 dòng. Nếu nằm giữa snapshot 0 và 1, trả về dữ liệu của snapshot 0 gần nhất.
Để test chính xác thời gian, ta cần biết timestamp của snapshot. Lệnh dưới đây lấy timestamp của snapshot 0.
duckdb -c "SELECT snapshot_id, timestamp FROM iceberg_demo AT SNAPSHOT 0 LIMIT 1;"
Kết quả: Hiển thị ID snapshot và thời gian tạo. Dùng giá trị timestamp này để thử truy vấn Time Travel bằng thời gian.
duckdb -c "UPDATE iceberg_demo SET value = 'updated' WHERE id = 1;" --append
Kết quả: Dòng id=1 được cập nhật. Iceberg tạo snapshot version 2. Dữ liệu cũ vẫn còn trong snapshot 1.
duckdb -c "SELECT * FROM iceberg_demo AT SNAPSHOT 1;"
Kết quả: id=1 vẫn có value='initial' (giá trị cũ), chứng minh Time Travel hoạt động độc lập với các thay đổi hiện tại.
Quản lý lịch sử phiên bản: Rollback, Drop snapshot và Cleanup
Quản lý phiên bản giúp kiểm soát dung lượng và phục hồi lỗi. Iceberg hỗ trợ rollback toàn bộ bảng về một snapshot cũ hoặc xóa một snapshot cụ thể.
Rollback thực chất là thiết lập snapshot cũ làm snapshot hiện tại (current). Các snapshot mới hơn sẽ trở thành 'orphan' và có thể bị xóa.
duckdb -c "CALL iceberg.rollback('iceberg_demo', 1);" --append
Kết quả: Bảng iceberg_demo quay lại trạng thái snapshot version 1. Snapshot version 2 (chứa bản cập nhật) không còn là current nữa.
duckdb -c "SELECT * FROM iceberg_demo;"
Kết quả: id=1 có value='initial' (giá trị trước khi update). Snapshot version 2 vẫn tồn tại trong lịch sử nhưng không còn là default.
Để xóa hoàn toàn một snapshot khỏi lịch sử (ví dụ snapshot 2 không cần nữa), dùng lệnh drop snapshot.
duckdb -c "CALL iceberg.drop_snapshot('iceberg_demo', 2);" --append
Kết quả: Snapshot version 2 bị xóa khỏi metadata. Các file data file không được tham chiếu bởi bất kỳ snapshot nào sẽ trở thành rác (orphan files).
Để xóa dữ liệu rác (orphan files) khỏi storage (S3, HDFS, local), cần chạy lệnh expire snapshots.
duckdb -c "CALL iceberg.expire_snapshots('iceberg_demo', 0);" --append
Kết quả: Xóa các snapshot đã được đánh dấu xóa và file data file không còn tham chiếu. Lưu ý: tham số 0 nghĩa là xóa tất cả snapshot cũ ngay lập tức, thực tế nên dùng retention policy.
duckdb -c "CALL iceberg.list_snapshots('iceberg_demo');" --append
Kết quả: Chỉ còn lại snapshot version 0 và version 1 (nếu version 1 là current). Version 2 đã mất hẳn.
Cấu hình chính sách giữ lại snapshot (Retention Policy) tự động
Để tự động dọn dẹp các snapshot cũ, Iceberg hỗ trợ các tham số cấu hình retention policy. Có thể cấu hình trong metadata của bảng hoặc khi gọi lệnh expire.
Chính sách phổ biến nhất là giữ lại snapshot theo số lượng (min snapshots to keep) hoặc theo thời gian (min snapshots to keep for X days).
duckdb -c "CALL iceberg.set_property('iceberg_demo', 'retention.min-snapshots-to-keep', '5');" --append
Kết quả: Thiết lập bảng iceberg_demo luôn giữ ít nhất 5 snapshot gần nhất. Các snapshot cũ hơn sẽ bị xóa tự động khi chạy expire.
duckdb -c "CALL iceberg.set_property('iceberg_demo', 'retention.max-snapshots-to-keep', '100');" --append
Kết quả: Giới hạn tối đa 100 snapshot. Nếu vượt quá, snapshot cũ nhất sẽ bị xóa trước khi tạo snapshot mới (tùy thuộc vào logic của engine).
duckdb -c "CALL iceberg.set_property('iceberg_demo', 'retention.min-snapshots-to-keep', '3');" --append
Kết quả: Cập nhật lại policy xuống còn 3 snapshot để test hiệu quả cleanup.
Khi chạy expire snapshots, Iceberg sẽ tự động tính toán và xóa các snapshot vượt quá chính sách này.
duckdb -c "CALL iceberg.expire_snapshots('iceberg_demo');" --append
Kết quả: Lệnh này chạy mà không cần tham số thời gian, nó sẽ tự động áp dụng policy đã cấu hình (min-snapshots-to-keep) để xóa các snapshot thừa.
duckdb -c "CALL iceberg.list_snapshots('iceberg_demo');" --append
Kết quả: Kiểm tra lại danh sách snapshot. Số lượng snapshot còn lại sẽ không nhỏ hơn 3 (theo policy vừa set).
Verify kết quả toàn bộ quy trình
Để đảm bảo toàn bộ quy trình quản lý phiên bản hoạt động đúng, ta sẽ thực hiện một bài test tổng hợp: tạo dữ liệu, rollback, xóa snapshot và kiểm tra lại.
duckdb -c "INSERT INTO iceberg_demo VALUES (4, 'test_rollback');" --append
Kết quả: Tạo snapshot mới (version 3). Bảng hiện có 4 dòng.
duckdb -c "SELECT count(*) FROM iceberg_demo;"
Kết quả: Trả về 4.
duckdb -c "CALL iceberg.rollback('iceberg_demo', 1);" --append
Kết quả: Rollback về version 1. Bảng quay lại trạng thái 3 dòng (id 1, 2, 3).
duckdb -c "SELECT count(*) FROM iceberg_demo;"
Kết quả: Trả về 3. Chứng minh rollback thành công.
duckdb -c "CALL iceberg.drop_snapshot('iceberg_demo', 3);" --append
Kết quả: Xóa snapshot version 3 (chứa dòng id=4). File data file chứa dòng id=4 trở thành orphan.
duckdb -c "CALL iceberg.expire_snapshots('iceberg_demo');" --append
Kết quả: Dọn dẹp file orphan. Dung lượng storage giảm xuống.
duckdb -c "CALL iceberg.list_snapshots('iceberg_demo');" --append
Kết quả: Danh sách snapshot không còn version 3. Chỉ còn version 0, 1 (và có thể version 2 nếu chưa bị xóa).
duckdb -c "SELECT * FROM iceberg_demo;"
Kết quả: Dữ liệu hiện tại vẫn là 3 dòng (id 1, 2, 3). Không có lỗi truy xuất dữ liệu.
duckdb -c "SELECT * FROM iceberg_demo AT SNAPSHOT 3;"
Kết quả: Lệnh này sẽ báo lỗi hoặc trả về 0 dòng vì snapshot 3 đã bị xóa vĩnh viễn. Đây là kết quả mong đợi khi drop snapshot.
Điều hướng series:
Mục lục: Series: Xây dựng Data Lakehouse Serverless với DuckDB, Apache Iceberg và Ubuntu 24.04
« Phần 5: Xây dựng quy trình ETL Serverless: Load, Transform, và Append
Phần 7: Tối ưu hóa hiệu năng: Indexing, Partitioning và Compaction »