Cùng một bài toán, hai động cơ: So sánh nội tại InnoDB và Iceberg

Công nghệ13 tháng 10, 2026·10 phút đọc

Câu chuyện thực tế về việc di chuyển hệ thống phân tích cổ phiếu từ MySQL InnoDB (OLTP) sang kiến trúc lakehouse Iceberg + MinIO + Polaris (OLAP). Bài viết phân tích ba đảm bảo cốt lõi của InnoDB, cách Iceberg tổ chức dữ liệu nhiều tầng, và so sánh trực tiếp giữa hai động cơ về đọc, ghi, phân vùng và xử lý ghi đồng thời.

Cùng một bài toán, hai động cơ: So sánh nội tại InnoDB và Iceberg

Cùng một bài toán, hai động cơ: So sánh nội tại InnoDB và Iceberg

Bài viết kể lại hành trình chuyển đổi một hệ thống phân tích dữ liệu chứng khoán từ động cơ OLTP truyền thống (MySQL InnoDB) sang một ngăn xếp OLAP hiện đại (Apache Iceberg trên MinIO, với Polaris làm catalog và Spark làm engine đọc). Tác giả chỉ ra rằng cùng một công việc — đọc dữ liệu giá cổ phiếu và tính toán các chỉ báo kỹ thuật — lại được hai động cơ giải quyết theo những cách hoàn toàn khác nhau.

Lý do chuyển đổi rất đơn giản: một tối ưu duy nhất. Nhưng để tận dụng được tối ưu đó, tác giả phải hiểu toàn bộ kiến trúc lakehouse từ trong ra ngoài — từ cách InnoDB đảm bảo tính nhất quán cho đến cách Iceberg tổ chức metadata theo nhiều tầng.

Ba đảm bảo của InnoDB

Phiên bản đầu tiên của kho phân tích chạy trên InnoDB, và động cơ này hoạt động dựa trên ba trụ cột chính.

Thứ nhất, các hàng lưu toàn bộ cột với nhau. Khi truy vấn một thanh nến (bar) cho một mã cổ phiếu, ta chỉ chạm vào một vị trí duy nhất trên đĩa — chính xác là những gì quá trình nạp dữ liệu (ingest) cần. Nhưng nếu một chỉ báo kỹ thuật chỉ cần hai cột, nó vẫn phải kéo toàn bộ mọi cột qua buffer pool. Đây là điểm yếu chí mạng với khối lượng phân tích lớn.

Thứ hai, chỉ mục tăng tốc đọc nhưng làm chậm ghi. Mỗi chỉ mục là một cây B-tree riêng, và mỗi thao tác ghi phải cập nhật tất cả các cây B-tree trong bảng. Đọc nhanh nhưng ghi đắt — đó là bản chất của chỉ mục. Vì vậy các bảng Raw* chỉ có khóa chính, trong khi các bảng nhận kết quả từ Raw* được đánh chỉ mục và phân vùng nặng.

Thứ ba, undo log cho phép đọc và ghi đồng thời. Khi một hàng bị cập nhật hoặc xóa, giá trị cũ được lưu vào undo log. Nếu một phiên khác đang đọc bảng trong lúc ghi, giá trị cũ sẽ được lấy từ undo log. Cơ chế này gọi là MVCC (Multi-Version Concurrency Control) — nói một cách đơn giản, nó cho phép chèn và cập nhật diễn ra mà không chặn đọc.

So sánh số lượng hàng giữa các phiên bảnSo sánh số lượng hàng giữa các phiên bản

Điều thú vị là tác giả đã vô tình thiết kế bảng InnoDB theo một nguyên lý của OLAP. Bảng financialdata chứa datetime, open, high, low, close — ban đầu là bảng duy nhất lưu datetime. Mọi chỉ báo kỹ thuật muốn truy vấn đều phải join với bảng này để biết ngày được tính. Đó là một trong những thiết kế tồi tệ nhất, và tác giả chỉ nhận ra sau khi cơ sở dữ liệu đã vượt 100GB.

Phân vùng bảng tài chính theo StockIDPhân vùng bảng tài chính theo StockID

Sau 5 năm vận hành phiên bản 1, tác giả nhận ra rằng công việc phân tích chỉ dùng 2-3 cột mỗi bảng, nhưng buộc phải đọc toàn bộ cột. Kết luận: động cơ đã chọn sai ngay từ đầu.

Kiến trúc OLAP của phiên bản 2

Phiên bản 2 chuyển sang Iceberg trên MinIO — các file Parquet nằm sau một catalog Polaris, với Spark xử lý việc đọc. Toàn bộ câu chuyện phân vùng gói gọn trong một mệnh đề DDL:

CREATE TABLE IF NOT EXISTS raw_bars (
  `symbol` string, `time_stamp` timestamp, `open` double,
  `high` double, `low` double, `close` double, `volume` bigint,
  `vwap` double, `trade_count` bigint, `is_valid` boolean)
USING iceberg PARTITIONED BY (bucket(16,`symbol`));

Một trong những điểm mạnh của Iceberg là nó tính toán và lưu mọi thông tin phân vùng trong metadata. Nếu muốn thêm một cột vào phân vùng, ta trả giá khi đọc chứ không phải khi ghi. Dữ liệu được nhóm theo phiên bản phân vùng, cắt tỉa độc lập theo từng phiên bản, rồi các kết quả cắt tỉa được hợp nhất sau — bước này gọi là split planning.

Trong phiên bản 1, phân vùng một bảng mất từ vài ngày đến cả tuần, và nếu muốn thay đổi phân vùng, ta phải phân vùng lại toàn bộ bảng. Đó là lý do tác giả chọn hash 100 — với khoảng 13.000 mã cổ phiếu chia thành các nhóm 130 mã, sẽ mất nhiều năm trước khi cần phân vùng lại.

Đường đọc dữ liệu

Lakehouse gồm ba thành phần: Apache Polaris, MinIO và Apache Iceberg.

Polaris đóng vai trò catalog — với tác giả, nó là một cơ sở dữ liệu Postgres cộng với một API để đọc ghi. Nhiệm vụ chính của nó là lưu con trỏ đến file metadata.json cho mỗi bảng Iceberg. Mọi truy vấn bắt đầu bằng việc hỏi con trỏ đó, và mọi commit kết thúc bằng việc cập nhật nó. Polaris chỉ chấp nhận một cập nhật tại một thời điểm — đây là điều giữ cho các cập nhật bảng mang tính nguyên tử (atomic).

MinIO là lưu trữ vật lý — nơi chứa các file Parquet, manifest, manifest list và metadata.json. Cấu trúc file giống hệt một S3 Bucket.

Iceberg là bộ não. Nó cung cấp hai tầng tối ưu truy vấn, cộng thêm một tầng tối ưu ở mức data file. Tác giả ví các tầng này như những con búp bê Nga lồng nhau:

  • Data file (búp bê nhỏ nhất): dữ liệu trong bảng được ghi thành file Parquet, tối ưu cho hệ thống cột — cho phép chọn cột cần đọc thay vì phải đọc hết. Mỗi file Parquet có các row group khác nhau và một bản tóm tắt (min/max, số null) cho mỗi nhóm cột.

  • Manifest: các data file được nhóm lại theo manifest. Manifest theo dõi min/max và số null cho mỗi cột, đồng thời theo dõi giá trị phân vùng, kích thước và vị trí của từng data file. Manifest cũng theo dõi Delete Files — các hàng đã bị xóa hoặc cập nhật. Việc hợp nhất Data Files và Delete Files xảy ra ở mỗi lần đọc, gọi là merge on read. Cơ chế này đặc biệt hữu ích cho streaming: ghi lại những gì thay đổi rồi xử lý sau luôn nhanh hơn việc thay đổi ngay.

  • Manifest List (búp bê lớn nhất): bản tóm tắt từ tầng manifest. Nó cung cấp min/max giá trị phân vùng cho mỗi manifest và phiên bản phân vùng mà manifest được ghi dưới đó — điều này làm cho split planning khả thi.

Metadata Iceberg: các cụm commitMetadata Iceberg: các cụm commit

Tiến trình ghi: đường commit

Data file là bất biến. Một thao tác ghi sẽ tạo ra các file mới. Quá trình nạp dữ liệu diễn ra qua một lệnh MERGE:

MERGE INTO stockalgo.market.raw_bars AS rb
USING new_raw_bars AS nrb
ON rb.symbol = nrb.symbol AND rb.time_stamp = nrb.time_stamp
WHEN MATCHED THEN UPDATE SET ...
WHEN NOT MATCHED THEN INSERT ...

Các hàng khớp được ghi lại thành file mới — gọi là copy on write, kẻ thù truyền kiếp của merge on read. Các hàng không khớp trở thành append.

Writer xây dựng manifest mới, manifest list mới cho snapshot mới, rồi một file metadata mới với snapshot được nối vào lịch sử. Sau đó nó yêu cầu Polaris hoán đổi con trỏ từ file metadata cũ sang file mới. Cú hoán đổi này mang tính nguyên tử, và đó chính là toàn bộ commit.

Khi hai writer chạy đua, kẻ nhanh hơn kiểm tra phiên bản hiện tại của bảng có khớp với phiên bản nó đã đọc không, rồi giành chiến thắng trong cú hoán đổi. Kẻ chậm hơn thấy sự không khớp và kiểm tra xem các data file đã thay đổi trong phiên bản mới có phải là những file nó đang xử lý không. Nếu khác, nó áp dụng lại thay đổi lên snapshot mới và thử hoán đổi lần nữa. Nếu giống, nó thất bại và thử lại theo logic retry.

Đây là optimistic locking — trái ngược hoàn toàn với advisory lock của phiên bản 1: trong v1, writer thứ hai chờ đến lượt; trong v2, nó thua và thử lại.

Metadata Iceberg: các lần hoán đổi con trỏMetadata Iceberg: các lần hoán đổi con trỏ

Đọc cũng không phải chờ. Một reader giữ snapshot mà nó bắt đầu, và các file bất biến không thể thay đổi bên dưới nó. Undo log của InnoDB tồn tại chính vì đảm bảo này — động cơ lưu phiên bản hàng cũ để reader tiếp tục làm việc trong lúc ghi. Iceberg bỏ qua việc lưu trữ vì phiên bản cũ chính là file cũ.

Nếu một thao tác ghi chết giữa chừng, nó để lại các data file mồ côi và không có snapshot. Không reader nào từng thấy nó, và việc retry an toàn vì khóa MERGE khiến thao tác có tính idempotent.

So sánh trực tiếp hai động cơ

Câu hỏiInnoDB (v1)Iceberg + Spark + Polaris (v2)
Điều gì làm đọc nhanh?Chỉ mục B-tree phụ, trả giá ở mỗi lần ghiMetadata tại thời điểm ghi, ba tầng cắt tỉa
Ghi tốn gì?Hàng cập nhật tại chỗ cộng mọi chỉ mụcFile mới bất biến cộng một commit snapshot
Chi phí thay đổi phân vùngXây lại toàn bộ bảng, vài ngày đến một tuầnThay đổi metadata, split planning xử lý file cũ
Quét cột như thế nào?Toàn bộ hàng qua buffer poolChỉ những cột mà truy vấn gọi tên
Writer đồng thờiAdvisory lock do tác giả quản lýHoán đổi con trỏ lạc quan, kẻ thua thử lại
Reader thấy gì?Các phiên bản hàng MVCCMột snapshot bất biến
Ghi chết giữa chừngBảng tạm, anti join, xóa sau commitFile mồ côi, không snapshot, retry idempotent
Đếm số hàngQuét chỉ mục qua từng hàngTóm tắt snapshot, đọc metadata
Hóa đơn bảo trìBảo trì chỉ mục và phân vùngCompaction và hết hạn snapshot

Vì sao điều này quan trọng với người dùng Việt Nam

Với các đội kỹ thuật tại Việt Nam đang xây dựng hệ thống phân tích dữ liệu, câu chuyện này mang lại vài bài học thiết thực.

Thứ nhất, chọn động cơ sai sẽ khiến bạn trả giá bằng nhiều năm kỹ thuật. Tác giả mất 9 tháng và khoảng 8.000 từ để hiểu và chuyển đổi — nhưng đổi lại, truy vấn chỉ đọc 2 trong số 36 cột thay vì đọc hết.

Thứ hai, mô hình lakehouse ngày càng phổ biến và có tính chuẩn hóa cao. Netflix, Snowflake, BigQuery và Delta Lake đều chạy một biến thể của kiến trúc này: thống kê được đóng dấu tại thời điểm ghi, cắt tỉa trước khi đọc dữ liệu, và giải mã theo cột. Học một lần là hiểu được phần lớn các hệ thống còn lại — chỉ khác tên gọi giữa các nhà cung cấp.

Thứ ba, với những ai tự vận hành homelab như tác giả, bộ MinIO + Iceberg + Polaris + Spark là một ngăn xếp hoàn toàn miễn phí, có thể thay thế các giải pháp thương mại đắt đỏ, và cấu trúc file tương thích S3 giúp việc chuyển lên cloud sau này dễ dàng hơn.

Điểm mấu chốt: cả hai động cơ đều trả lời cùng một câu hỏi — làm sao để đọc nhanh, ghi an toàn, và cho phép truy cập đồng thời. Nhưng InnoDB trả lời bằng undo log và khóa, còn Iceberg trả lời bằng file bất biến và hoán đổi con trỏ nguyên tử.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗