Bỏ qua để đến nội dung

Vận hành Apache Iceberg trong production: 4 vấn đề thường gặp và cách xử lý

Ý tưởng lakehouse nghe rất gọn: chọn một định dạng bảng mở như Apache Iceberg, nối nó với các engine và catalog bạn thích, thế là dữ liệu mở, công cụ dùng chung một bảng, mọi thứ đẹp đẽ.

Cái ít ai nói tới là chuyện gì xảy ra sau khi bảng Iceberg thật sự chạy trong production. Khi bảng lớn tới hàng terabyte rồi petabyte, khi pipeline chạy liên tục, và khi cam kết thời gian (SLA — Service Level Agreement) bắt đầu quan trọng, độ phức tạp vận hành mới lộ ra. Đây là lúc đội kỹ thuật buộc phải hiểu hệ thống sâu hơn — hiểu nó hành xử thế nào dưới tải đồng thời, ở quy mô lớn, và khi bảng tiến hóa liên tục.

Ở những môi trường lớn, nơi tải streaming và batch cùng ghi vào một bảng và các tác vụ bảo trì chạy song song, có một nhóm triệu chứng lặp đi lặp lại:

  • Ghi thất bại ở thời điểm commit
  • Đọc báo thiếu file
  • Job bảo trì chạy rất lâu hoặc hỏng khó đoán
  • Metadata phình lên đều đặn

Trang này đi qua bốn vấn đề tương ứng, mỗi vấn đề theo cùng một mạch: mô hình thực thi bên dưới của Iceberg, mẫu lỗi quan sát được trong production, và cách xử lý thực dụng. Nội dung tổng hợp và diễn giải theo whitepaper vận hành Iceberg của Cloudera.

Trong một triển khai production, một bảng Iceberg hiếm khi được ghi bởi một tiến trình duy nhất. Luồng streaming liên tục nối thêm hoặc cập nhật dữ liệu; job batch chạy MERGE INTO, UPDATE, DELETE; và tác vụ bảo trì nền như nén file (compaction) ghi lại file để tối ưu bố cục và chi phí lưu trữ. Cả ba cùng chạm vào một bảng.

Dưới điều kiện đó, lỗi thời điểm commit bắt đầu xuất hiện dưới dạng ngoại lệ kiểm tra tính hợp lệ (validation exception) báo có file xung đột. Điều gây bối rối là hai job có thể đang cập nhật những dòng khác nhau, thậm chí những phân vùng khác nhau, mà một trong hai vẫn hỏng ở thời điểm commit. Người ta hay hiểu nhầm đây là giới hạn về khả năng xử lý đồng thời của Iceberg. Thực ra nó là hệ quả trực tiếp của thiết kế.

Iceberg đảm bảo tính đúng đắn bằng kiểm soát đồng thời lạc quan (OCC — Optimistic Concurrency Control). Thay vì khóa bảng trong lúc thực thi, mỗi thao tác ghi tiến hành độc lập và chỉ được kiểm tra tính hợp lệ tại thời điểm commit.

Mọi thao tác ghi trong Iceberg theo mô hình dựa trên ảnh chụp (snapshot). Một truy vấn ghi bắt đầu bằng việc lập kế hoạch trên một ảnh chụp cụ thể của bảng. Trong pha này, nó xác định tập file dữ liệu cần ghi lại để áp dụng thay đổi — gọi là tập làm việc (working set). Tới thời điểm commit, Iceberg thực hiện một bước kiểm tra then chốt: kế hoạch còn hợp lệ so với trạng thái hiện tại của bảng không. Ràng buộc cốt lõi là không file nào trong tập làm việc được phép đã bị sửa hoặc thay thế kể từ lúc truy vấn bắt đầu. Nếu chỉ một file thay đổi trong lúc đó, thao tác bị từ chối.

Điểm mấu chốt: phát hiện xung đột diễn ra ở mức file dữ liệu, không phải mức từng dòng. Phần lớn thao tác ghi — MERGE INTO, UPDATE, DELETE — diễn đạt theo dòng, nhưng được thực thi bằng cách ghi lại nguyên cả file dữ liệu. Vì vậy hai thao tác trông độc lập ở mức dòng vẫn xung đột nếu chúng chạm cùng một file bên dưới.

flowchart TB
  F["File F1 (Parquet)<br/>chứa Dòng A · Dòng B · Dòng C"]
  Q1["Job 1 — sửa Dòng A"] -->|"ghi lại cả F1"| F
  Q2["Job 2 — sửa Dòng B"] -->|"cũng ghi lại cả F1"| F
  F --> R{"Commit: F1 có bị đổi<br/>kể từ lúc bắt đầu không?"}
  R -->|"Job 1 commit trước — OK"| OK["✓ Thành công"]
  R -->|"Job 2 dựa trên F1 cũ"| NO["✗ Bị từ chối<br/>ValidationException"]
  • MERGE quét rộng. MERGE INTO thiếu điều kiện lọc chọn lọc sẽ quét và ghi lại rất nhiều file. Tập làm việc phình to, kéo theo xác suất ít nhất một file bị thao tác khác sửa trước khi commit.
  • Streaming và batch chồng lấn. Luồng streaming nối thêm vào các phân vùng gần đây, trong khi batch chạy trên các khoảng thời gian trùng. Hai bên tranh nhau ở mức file; tần suất nạp càng cao thì batch càng dễ chạy trên ảnh chụp cũ.
  • Truy vấn ghi chạy lâu. Thời gian thực thi càng dài, cửa sổ để trạng thái bảng thay đổi càng rộng — tới lúc commit thì khả năng có file đã bị sửa càng cao.
  • Bảo trì đấu với nạp liệu. Nén file (như rewrite_data_files) và luồng nạp liên tục cùng ghi lại các tập file chồng lấn, nên chịu chung luật kiểm tra ở thời điểm commit dù về mặt khái niệm chúng độc lập.

Xử lý xung đột commit là chuyện căn chỉnh thiết kế tải với cơ chế kiểm tra ở mức file:

  • Giảm chồng lấn file. Thiết kế lược đồ phân vùng để tách vùng ghi của các job, và giới hạn truy vấn vào tập dữ liệu hẹp hơn.
  • Kiểm soát đồng thời. Xếp lịch job batch tránh trùng với luồng streaming, hoặc cô lập tải theo phân vùng.
  • Rút ngắn thời gian job. Job chạy nhanh hơn thì cửa sổ trạng thái bảng có thể đổi ngắn lại, giảm xác suất bị từ chối.
  • Phối hợp bảo trì với nạp liệu. Chạy nén file trong giai đoạn nạp thấp, hoặc thu hẹp phạm vi của nó. Không phối hợp thì gần như cầm chắc xung đột.

Vấn đề 2 — Dọn orphan file gây hết bộ nhớ (OOM)

Phần tiêu đề “Vấn đề 2 — Dọn orphan file gây hết bộ nhớ (OOM)”

Dọn orphan file nhằm gỡ bỏ những file không còn được ảnh chụp hợp lệ nào tham chiếu — thường sinh ra từ lần ghi hỏng hoặc thao tác bảo trì dở dang. Iceberg cung cấp thủ tục để làm việc này (như remove_orphan_files). Nhưng ở quy mô production lớn, thao tác này co giãn rất kém: job chạy dai dẳng, thời gian khó đoán, và nhiều trường hợp báo lỗi hết bộ nhớ (OOM — Out Of Memory).

Đây là điểm phản trực giác quan trọng nhất của cả trang: chi phí dọn orphan file không tỉ lệ với số orphan, mà tỉ lệ với tổng số file trong kho lưu trữ của bảng.

Khác với phần lớn thao tác Iceberg — vốn dựa vào metadata (manifest và snapshot) để tránh quét kho — việc dọn orphan phải dựng lại tập file thực có trong kho nhưng không được metadata tham chiếu. Nó gồm hai bước đắt:

  1. Liệt kê toàn bộ file trong kho của bảng — quét mọi thư mục và phân vùng dưới gốc bảng. Chi phí bước này tăng tuyến tính theo tổng số file.
  2. So khớp danh sách đó với tập file mà metadata tham chiếu — phần chênh lệch chính là orphan. Danh sách trung gian này thường phải nạp về driver để xử lý.

Hệ quả: thao tác này bỏ qua ưu thế metadata của Iceberg và tụt về kiểu quét ở tầng lưu trữ. Vì vậy chi phí là O(tổng số file trong kho), không phải O(số orphan). Ngay cả khi có rất ít orphan, thao tác vẫn có thể cực kỳ tốn kém — đúng cảnh xuất hiện ở các bảng nạp file nhỏ với tần suất cao.

flowchart LR
  A["Liệt kê TOÀN BỘ file trong kho<br/>chi phí ∝ tổng số file"] --> C
  B["Tập file metadata tham chiếu"] --> C
  C["So khớp lấy phần chênh = orphan"] --> D["Gom về driver để xóa"]
  D --> E["Bảng lớn ⇒ driver OOM,<br/>job treo hàng giờ"]
  • Treo và hết bộ nhớ ở bảng lớn. Bảng nạp liệu theo giờ tích lũy hàng nghìn file mỗi phân vùng; khi kích hoạt dọn orphan, job có thể chạy hàng giờ mà không có tiến độ nhìn thấy, rồi cuối cùng bung lỗi hết bộ nhớ ở driver.
  • Chi phí co theo kích thước bảng, không theo kích thước vấn đề. Số orphan thường nhỏ, nhưng thời gian và tài nguyên lại tỉ lệ với tổng kích thước bảng.
  • Nghẽn ở driver. Dù nhiều phần chạy phân tán, các bước cần gom và tổng hợp metadata file lại dồn về driver — nên lỗi thường cục bộ ở đó, kể cả khi cụm dư tài nguyên.
  • Dùng sai như bảo trì định kỳ. Xếp dọn orphan vào lịch bảo trì đều đặn (cạnh nén file và hết hạn ảnh chụp) khiến nó lặp lại việc quét toàn bảng dù đa số lần không có orphan đáng kể.

Vì thao tác này hành xử khác hẳn các thủ tục bảo trì khác, cách xử lý xoay quanh việc kiểm soát phạm vi, tần suất và đặc tính thực thi:

  • Chỉ dùng khi khắc phục sự cố, không xếp lịch định kỳ. Dọn orphan sinh ra để dọn hậu quả của lần ghi/thao tác hỏng — chạy nó định kỳ trên bảng lớn là ép quét toàn bảng ngay cả khi không có orphan.
  • Thu hẹp phạm vi liệt kê file. Yếu tố chi phí chính là kích thước vùng lưu trữ bị quét. Giới hạn vào phân vùng hoặc thư mục con nơi đã biết có sự cố sẽ giảm hẳn lượng dữ liệu phải xử lý.
  • Tính tới đặc tính nghẽn-ở-driver khi lập kế hoạch. Tăng bộ nhớ driver chỉ trì hoãn lỗi chứ không giải quyết gốc; sự ổn định đến từ việc giảm lượng dữ liệu phải tổng hợp, không phải từ việc cấp thêm tài nguyên.
  • Căn chỉnh theo tốc độ lớn của bảng. Lịch cố định không theo kịp việc số file tăng liên tục; căn chiến lược dọn theo quy mô bảng để chi phí vận hành tỉ lệ với nhu cầu thật.

Trong các triển khai Iceberg production, có một lớp lỗi hay gặp khi đọc: một file dữ liệu hoặc metadata mà truy vấn tham chiếu không còn tồn tại ở vị trí lưu trữ mong đợi. Người ta hay hiểu nhầm đây là hỏng dữ liệu trong bảng. Phân tích các sự cố thực tế cho thấy khác: đây không phải hỏng dữ liệu, mà là sự lệch pha giữa trạng thái ảnh chụp mà truy vấn giả định và trạng thái vật lý thực của kho lưu trữ.

Mỗi truy vấn làm việc trên một ảnh chụp cụ thể — ảnh chụp đó định nghĩa các file metadata, manifest và đúng tập file dữ liệu sẽ đọc. Khi truy vấn bắt đầu, nó ngầm nói: “tôi sẽ đọc ảnh chụp này, và các file này phải còn tồn tại.” Nhưng có một giả định quan trọng: Iceberg giả định mọi file được tham chiếu vẫn còn suốt thời gian truy vấn chạy.

Vấn đề là Iceberg không theo dõi người đọc đang hoạt động, và về thiết kế nó không cần — nhờ cô lập theo ảnh chụp (snapshot isolation), mỗi truy vấn độc lập với các thay đổi của bảng. Đọc luôn diễn ra trên ảnh chụp bất biến, và vòng đời file do metadata quyết định, không có sự phối hợp nào với việc thực thi truy vấn. Hệ quả: một thao tác bảo trì (như nén file, hết hạn ảnh chụp, hay dọn orphan) có thể xóa những file mà một truy vấn đang chạy trên ảnh chụp cũ vẫn cần.

flowchart TB
  S["Truy vấn bắt đầu trên ảnh chụp S1<br/>cần file F1 · F2 · F3"] --> R["Đang đọc..."]
  M["Bảo trì chạy nền:<br/>hết hạn ảnh chụp / dọn file"] --> D["Xóa F2 khỏi kho"]
  R --> A["Truy vấn đọc tới F2"]
  D --> A
  A --> X["✗ NotFoundException:<br/>vị trí không tồn tại"]

Có hai đường dẫn tới lỗi này. Biến mất vật lý: file bị xóa khỏi kho trong khi người đọc còn cần — thường khi nạp liệu và bảo trì chạy độc lập trên các engine khác nhau, và ảnh chụp được đặt hết hạn trong khoảng rất ngắn. Biến mất logic qua tiến hóa ảnh chụp: file còn nằm vật lý trong kho nhưng không còn được metadata nào tham chiếu — từ góc nhìn Iceberg, file không được manifest nào trỏ tới thì coi như không thuộc tập dữ liệu, và truy vấn bắt đầu trên ảnh chụp cũ sẽ hỏng khi ảnh chụp đó không còn được giữ.

Xử lý NotFoundException đòi căn chỉnh quản lý vòng đời ảnh chụp, tiến hóa metadata và hành vi truy vấn:

  • Đặt thời hạn giữ ảnh chụp theo vòng đời người đọc, không theo tối ưu lưu trữ. Thời hạn giữ ảnh chụp quyết định trạng thái bảng lịch sử còn sống bao lâu. Nếu cửa sổ giữ ngắn hơn truy vấn dài hoặc checkpoint của streaming, người đọc có thể tham chiếu ảnh chụp đã biến mất. Thời hạn giữ nên vượt thời gian truy vấn, chu kỳ checkpoint và cửa sổ phục hồi.
  • Tách việc xóa file vật lý khỏi chính sách hết hạn ảnh chụp. Hết hạn ảnh chụp và xóa file là hai quá trình riêng. Nếu dọn chạy quá gắt, file cần cho ảnh chụp vừa hết hạn có thể bị xóa quá sớm. Chèn một khoảng đệm giữa hết hạn và xóa để file còn khả dụng cho người đọc đang hoạt động.
  • Phối hợp bảo trì với tải truy vấn. Bảo trì liên tục làm tiến hóa trạng thái bảng; chạy độc lập thì nó phá vỡ giả định của người đọc. Căn tần suất bảo trì theo mẫu tải và tránh thao tác toàn bảng gắt gao sẽ giảm rủi ro tiến hóa ảnh chụp vượt mặt việc thực thi truy vấn.

Mỗi commit trong Iceberg không phải một phép nối thêm đơn giản vào metadata. Nó tạo ra một file metadata.json mới, một manifest list mới, và một hoặc nhiều manifest file mô tả các file dữ liệu được thêm hoặc bỏ. Cấu trúc phân tầng này là nền của mô hình cô lập theo ảnh chụp, nhưng cũng gây ra một dạng khuếch đại ghi metadata.

Ngay cả với thao tác tối thiểu — nối một file hoặc xóa một tập nhỏ bản ghi — hệ thống vẫn phải ghi lại nhiều tầng metadata. Các thao tác sửa dữ liệu hiện có thường phải ghi lại manifest theo kiểu copy-on-write (ghi bản mới khi sửa). Trường hợp xấu, việc này ghi lại một phần lớn manifest hiện có dù thay đổi logic rất nhỏ. Kết quả: chi phí thao tác metadata tỉ lệ với kích thước metadata hiện có của bảng, chứ không phải với kích thước thay đổi.

Hiệu ứng này rõ nhất ở tải có commit thường xuyên — như streaming hoặc nạp micro-batch. Những cập nhật nhỏ và liên tục lặp đi lặp lại kích hoạt ghi lại metadata, và chi phí co theo bảng. Nó biểu hiện hai cách: độ trễ ghi tăng (mỗi commit phải dựng và công bố nguyên tử một trạng thái metadata mới), và lập kế hoạch truy vấn đắt hơn (engine phải đọc và xử lý các cấu trúc metadata thường xuyên bị ghi lại). Nói cách khác, Iceberg tránh quét file dữ liệu thừa, nhưng một phần chi phí dịch sang xử lý metadata — thứ lớn lên theo độ phức tạp của cấu trúc metadata chứ không theo độ lớn của thay đổi dữ liệu.

Hướng đi: gộp cấp bậc metadata (đề xuất v4)

Phần tiêu đề “Hướng đi: gộp cấp bậc metadata (đề xuất v4)”

Whitepaper Cloudera nêu một hướng đơn giản hóa cấu trúc: khái niệm Root Manifest (manifest gốc) thay cho manifest list làm điểm vào của một ảnh chụp. Thay vì giữ cây phân tầng nhiều mức (metadata.json → manifest list → manifest), cấu trúc được làm phẳng để ảnh chụp trỏ thẳng tới một manifest gốc, rồi manifest gốc trỏ tới các data manifest và delete manifest, rồi tới file:

Root Manifest → Data / Delete Manifests → Files

Thay đổi này khiến mỗi commit chỉ cần sửa phần bị ảnh hưởng của cấu trúc, thay vì cả trạng thái bảng — nên chi phí metadata co theo chính thao tác chứ không theo kích thước lũy kế của đồ thị metadata. Ý nghĩa với tải commit-nhiều rất lớn: chi phí commit tỉ lệ với số file thay đổi, giảm khuếch đại ghi metadata, hạ độ trễ commit, và ổn định hiệu năng khi nạp liệu liên tục. Manifest gốc còn có thể làm lớp tổng hợp thống kê mức file (min/max, số đếm), cho phép cắt tỉa sớm khi lập kế hoạch truy vấn.

Bốn vấn đề trên nói lên một điều: phần lớn sự cố production của Iceberg là hệ quả trực tiếp của thiết kế, không phải lỗi ngẫu nhiên. Thách thức không chỉ là vá từng cái, mà là hiểu đúng các nguyên thủy chi phối hành vi của hệ thống.

Những hệ thống chạy tốt ở quy mô lớn là những hệ thống căn chỉnh mẫu tải — nạp liệu, bảo trì và thực thi truy vấn khớp với các cơ chế bên dưới này. Lược đồ phân vùng, lịch chạy job, chính sách giữ ảnh chụp và tần suất commit không phải lựa chọn cấu hình vặt — chúng định hình cách hệ thống hành xử dưới tải đồng thời và tiến hóa liên tục, và rốt cuộc quyết định Iceberg chạy tốt tới đâu trong production.

Với các tổ chức tại Việt Nam đang đưa lakehouse vào lõi phân tích — ngân hàng, viễn thông, khu vực công — bốn vấn đề này không phải chuyện lý thuyết. Chúng xuất hiện đúng ở những nơi đau nhất: luồng giao dịch nạp liên tục (streaming) chồng lên job đối chiếu cuối ngày (batch) trên cùng một bảng; báo cáo gửi cơ quan quản lý chạy truy vấn dài trong khi bảo trì nền dọn file; bảng dữ liệu chi tiết cuộc gọi hay giao dịch phình file nhỏ theo từng giờ.

Điều đáng nói với người ra quyết định: đây là những vấn đề giải được bằng thiết kế và vận hành đúng, không phải giới hạn buộc phải chấp nhận. Nhưng giải được thì cần hiểu hệ thống ở mức này — và đó là phần mà một đội chưa từng vận hành lakehouse ở quy mô lớn hay trả giá bằng những sự cố “khó hiểu” vào đúng kỳ cao điểm.

Apache Iceberg (Open Data Lakehouse) — Nền tảng: định dạng bảng mở, ACID, ảnh chụp, du hành thời gian và Lakehouse Optimizer.

Data Engineering (Spark) — Dựng luồng nạp và biến đổi ghi vào bảng Iceberg; nơi phần lớn tải ghi bắt nguồn.

Data Warehouse — Ba engine đọc bảng Iceberg; nơi phần lớn truy vấn dài bắt nguồn.

Chia sẻ: