0

Google BigQuery - Bài học 3: Lưu trữ dạng cột (Columnar Storage - Capacitor) – Bí mật giúp BigQuery quét hàng Terabyte dữ liệu chỉ trong vài giây

Chào anh em! Ở Bài học 2, chúng ta đã mổ xẻ cách Google tách rời hoàn toàn kho lưu trữ Colossus và bộ máy tính toán Dremel, nối với nhau bằng siêu xa lộ mạng Jupiter. Nhưng hãy thử làm một phép tính nhỏ nhé: Dù mạng Jupiter có nhanh đến mấy và bạn có hàng ngàn nhân CPU (Slots), nếu mỗi lần chạy một câu lệnh thống kê đơn giản trên bảng nặng 10 Terabyte (TB) mà hệ thống đều phải kéo đủ 10 TB qua mạng rồi nạp lên RAM, thì chắc chắn tốc độ không thể nào đạt mức 1 - 2 giây, và hóa đơn tiền Cloud cuối tháng của công ty bạn sẽ "nổ tung"!

Vậy điều kỳ diệu gì đã diễn ra bên trong tầng lưu trữ Colossus? Tại sao một bảng dữ liệu thô nặng 10 TB khi đưa vào BigQuery lại chỉ tốn vài trăm GB lưu trữ, và khi chạy query chỉ mất đúng vài giây?

Câu trả lời nằm ở Capacitor – định dạng lưu trữ dạng cột (Columnar Storage format) thế hệ mới do Google tự phát triển dành riêng cho BigQuery. Hôm nay, chúng ta sẽ soi kính hiển vi vào từng byte dữ liệu bên trong Capacitor!

1. Trực quan hóa sự khác biệt: Row-oriented vs Column-oriented

Để hiểu tại sao lưu trữ dạng cột lại là "chân ái" của Data Warehouse, hãy lấy một ví dụ cực kỳ quen thuộc với anh em Backend: Bảng orders lưu thông tin đơn hàng.

Giả sử chúng ta có 4 dòng dữ liệu đơn giản như sau:

order_id user_id status total_amount
101 5001 COMPLETED 150,000
102 5002 COMPLETED 320,000
103 5003 COMPLETED 150,000
104 5004 CANCELLED 500,000

Cách MySQL / PostgreSQL ghi xuống ổ cứng (Row-oriented):

Các khối dữ liệu (Data Blocks/Pages) trên ổ đĩa sẽ xếp nối đuôi nhau theo từng dòng:

Plaintext

[101, 5001, COMPLETED, 150000] [102, 5002, COMPLETED, 320000] [103, 5003, COMPLETED, 150000] [104, 5004, CANCELLED, 500000]

  • Ưu điểm: Khi bạn chạy INSERT INTO orders VALUES (...) hoặc SELECT * FROM orders WHERE order_id = 101, đầu đọc ổ cứng chỉ cần đến đúng 1 vị trí là lấy được trọn vẹn thông tin của đơn hàng đó.

  • Nhược điểm chí mạng khi phân tích: Khi sếp hỏi "Tổng tiền các đơn hàng COMPLETED là bao nhiêu?" (SELECT SUM(total_amount) FROM orders WHERE status = 'COMPLETED'), đầu đọc ổ cứng buộc phải nhảy qua cả order_id và user_id (những cột không hề dùng tới). Nếu bảng có 50 cột bao gồm cả địa chỉ giao hàng, ghi chú dài lê thê, thì 95% băng thông đọc ổ đĩa là đọc rác!

Cách Capacitor của BigQuery ghi xuống ổ cứng (Column-oriented):

Capacitor xoay ngang bảng dữ liệu lại 90 độ. Mỗi cột được gom riêng ra thành một dải dữ liệu độc lập (File Block riêng):

Plaintext

File/Block Cột order_id:     [101, 102, 103, 104]
File/Block Cột user_id:      [5001, 5002, 5003, 5004]
File/Block Cột status:       [COMPLETED, COMPLETED, COMPLETED, CANCELLED]
File/Block Cột total_amount: [150000, 320000, 150000, 500000]

Khi chạy câu lệnh tính tổng total_amount theo status ở trên:

  1. Dremel chỉ mở đúng 2 khối dữ liệu của cột status và total_amount.

  2. Cột order_id, user_id và 48 cột khác trong bảng tuyệt đối không được chạm tới (0 byte I/O).

Đó là lý do đầu tiên giúp BigQuery cắt giảm ngay lập tức từ 80% – 95% lượng dữ liệu phải đọc so với Database truyền thống! Nhưng đó mới chỉ là bề nổi của tảng băng chìm. Sức mạnh thực sự của Capacitor nằm ở Cơ chế nén (Compression) và Thực thi trực tiếp trên dữ liệu nén.

2. Nghệ thuật nén dữ liệu đỉnh cao của Capacitor: RLE và Dictionary Encoding

Khi bạn xếp dữ liệu theo từng dòng như MySQL, các giá trị đứng cạnh nhau có kiểu dữ liệu hoàn toàn khác nhau (Số nguyên -> Số nguyên -> Chuỗi -> Số tiền), khiến các thuật toán nén (như LZ4, Zstd, Snappy) không đạt được tỷ lệ nén tối đa.

Nhưng khi xếp dữ liệu theo cột, toàn bộ dữ liệu đứng cạnh nhau đều có cùng kiểu dữ liệu và độ trùng lặp cực kỳ cao (Low Cardinality). Lúc này, Capacitor tung ra hai tuyệt chiêu:

Tuyệt chiêu 1: Dictionary Encoding (Mã hóa từ điển)

Giả sử cột status có 100 triệu dòng, nhưng thực tế chỉ xoay quanh 3 giá trị lặp đi lặp lại: 'COMPLETED' (9 bytes), 'PENDING' (7 bytes), 'CANCELLED' (9 bytes).

Nếu lưu chuỗi text thuần túy, 100 triệu dòng sẽ tốn gần 1 Gigabyte.

Capacitor tự động tạo ra một bảng từ điển nhỏ xíu ở đầu file:

  • 0 = 'COMPLETED'

  • 1 = 'CANCELLED'

  • 2 = 'PENDING'

Sau đó, toàn bộ 100 triệu dòng của cột status thay vì lưu chuỗi ký tự dài dòng, nay chỉ lưu các con số 0, 0, 0, 1, 2... tốn đúng vài bit cho mỗi dòng! Dung lượng cột lập tức giảm đi gần 90%.

Tuyệt chiêu 2: Run-Length Encoding (RLE - Mã hóa độ dài chuỗi lặp)

Nếu dữ liệu trong cột được sắp xếp để các giá trị giống nhau nằm cạnh nhau, ví dụ cột status có 5.000 dòng đầu tiên đều là 'COMPLETED' (mã từ điển là 0), tiếp theo là 200 dòng 'CANCELLED' (mã là 1).

Thay vì viết 5.000 số 0 liên tiếp (0, 0, 0, 0...), thuật toán RLE của Capacitor chỉ ghi một cặp số duy nhất:

Plaintext

(Giá trị: 0, Số lần lặp: 5000), (Giá trị: 1, Số lần lặp: 200)

Hãy tưởng tượng: 5.000 dòng dữ liệu giờ đây được gom lại thành đúng 2 con số nguyên!

Kỹ thuật Re-ordering (Tự động đảo thứ tự dòng của Capacitor)

Một điểm cực kỳ thông minh của Capacitor mà ít người biết: Khi bạn nạp dữ liệu vào bảng (ngay cả khi bạn chưa cấu hình Clustering), bộ máy ghi của Capacitor sẽ chạy một thuật toán xáo trộn và sắp xếp lại thứ tự các dòng (Row Re-ordering / Shuffling) sao cho các cột có nhiều giá trị trùng lặp được gom lại gần nhau nhất để tối ưu hóa cho thuật toán nén RLE!

3. "Ma thuật" Operating Directly on Compressed Data: Tính toán mà... không cần giải nén!

Trong các hệ thống truyền thống có bật tính năng nén (như InnoDB Page Compression), quy trình đọc dữ liệu là:

Đọc dữ liệu nén từ đĩa →\rightarrow Tốn CPU giải nén (Decompress) ra RAM →\rightarrow Kiểm tra điều kiện WHERE / Tính COUNT, SUM.

Nhưng với Capacitor + Dremel, Google thiết kế bộ máy thực thi để chạy câu lệnh SQL trực tiếp ngay trên dữ liệu đang nén!

Hãy xem cách Dremel xử lý câu lệnh sau trên khối dữ liệu đã nén bằng Dictionary + RLE ở trên:

SQL

SELECT COUNT(*) FROM orders WHERE status = 'COMPLETED';

  1. Dremel nhìn vào Dictionary của file Capacitor, thấy 'COMPLETED' tương ứng với mã 0.

  2. Nó đọc đoạn RLE: (Giá trị: 0, Số lần lặp: 5000).

  3. Dremel lập tức cộng ngay con số 5000 vào tổng kết quả mà không cần phải bung (giải nén) 5.000 dòng đó ra RAM, cũng không cần chạy vòng lặp for 5.000 lần để đếm từng dòng!

Quá trình này kết hợp với tập lệnh phần cứng SIMD (Single Instruction, Multiple Data - Vectorized Execution) trên CPU của Google, cho phép một xung nhịp CPU có thể so sánh điều kiện WHERE cho hàng chục giá trị cùng một lúc.

4. Min/Max Block Pruning: Bỏ qua hàng triệu dòng chỉ bằng một cái liếc mắt

Mỗi file Capacitor trên Colossus được chia thành nhiều khối nhỏ (Row Groups / Data Blocks). Ở phần Header (Metadata) của mỗi khối, Capacitor luôn lưu sẵn các thông số thống kê của từng cột trong khối đó, quan trọng nhất là: Min Value, Max Value, và Null Count.

Giả sử bạn chạy câu lệnh tìm một đơn hàng hoặc lọc theo khoảng tiền:

SQL

SELECT user_id, total_amount 
FROM orders 
WHERE total_amount > 10000000; -- Tìm đơn hàng trên 10 triệu VNĐ

Khi hàng ngàn Leaf Nodes của Dremel lao xuống đọc các khối dữ liệu:

  • Block 1: Header ghi total_amount: [Min: 50.000, Max: 2.000.000]. Dremel thấy giá trị lớn nhất của cả Block này mới có 2 triệu (nhỏ hơn 10 triệu) →\rightarrow Bỏ qua toàn bộ Block 1 (Skip!) mà không cần đọc nội dung bên trong.

  • Block 2: Header ghi total_amount: [Min: 100.000, Max: 25.000.000] →\rightarrow Có khả năng chứa đơn trên 10 triệu →\rightarrow Dremel mới bắt đầu đọc sâu vào Block 2.

Cơ chế tự động loại bỏ các khối dữ liệu không thỏa mãn điều kiện ngay từ vòng gửi xe này gọi là Block Pruning (Cắt tỉa khối). Và đây cũng chính là nền tảng cốt lõi của tính năng Table Clustering mà chúng ta sẽ học ở Phần 4!

5. Xử lý dữ liệu lồng nhau (Nested & Repeated Fields): Di sản từ bài báo Dremel

Một câu hỏi cực hay dành cho anh em đã từng làm việc với JSON hoặc MongoDB: "Nếu dữ liệu của tôi không phẳng (Flat), mà một đơn hàng có một mảng items chứa nhiều sản phẩm bên trong (Nested Array of Structs), thì làm sao Capacitor lưu thành dạng cột được?"

Năm 2010, Google công bố bài báo khoa học chấn động giới Data có tên "Dremel: Interactive Analysis of Web-Scale Datasets" (tiền thân của cả định dạng Apache Parquet nổi tiếng ngày nay). Trong đó, Google giới thiệu hai thông số kỹ thuật thiên tài đi kèm mỗi giá trị trong cột là:

  • Repetition Level (Mức độ lặp): Cho biết giá trị này đang lặp lại ở cấp độ mảng (Array) nào.

  • Definition Level (Mức độ định nghĩa): Cho biết có bao nhiêu trường cha (Struct/Nested) phía trên nó có giá trị khác NULL.

Nhờ 2 con số nhỏ xíu này, Capacitor có thể tách từng trường bên trong một cục JSON/Array phức tạp ra thành các cột vật lý hoàn toàn độc lập!

Ví dụ: Bạn có cột items là một mảng chứa product_id và price. Khi bạn chỉ cần tính SUM(items.price), Capacitor chỉ đọc đúng cột con items.price mà không thèm đọc items.product_id, đồng thời không cần tốn CPU để parse toàn bộ chuỗi JSON như trong MySQL!

6. Bài học xương máu cho anh em Kỹ sư khi viết SQL trên BigQuery

Hiểu rõ cơ chế của Capacitor sẽ giúp bạn thay đổi hoàn toàn thói quen viết SQL hàng ngày:

  1. SELECT * là tội ác chống lại ví tiền:

    Trong MySQL, SELECT id, name FROM users LIMIT 10 và SELECT * FROM users LIMIT 10 có tốc độ gần như nhau vì đằng nào ổ cứng cũng phải đọc cả dòng lên. Nhưng trong BigQuery, SELECT * ép Dremel phải mở và đọc tất cả các file cột của Capacitor! Bảng có 100 cột mà bạn SELECT * thì bạn trả tiền gấp 50 lần so với việc chỉ SELECT 2 cột cần thiết.

  2. Tại sao LIMIT 10 không giúp giảm tiền trên BigQuery?

    Nhiều anh em mới dùng BigQuery hay gõ SELECT * FROM big_table LIMIT 10 để xem thử mẫu dữ liệu và đinh ninh rằng "Chỉ lấy 10 dòng thì tốn mấy KB thôi". Thực tế: Các cột được nén và lưu rải rác trên Colossus, khi bạn chạy query (đặc biệt khi có WHERE hoặc ORDER BY), các Leaf Nodes vẫn phải quét qua toàn bộ các mảnh cột liên quan rồi mới cắt lấy 10 dòng ở bước cuối. (Muốn xem trước dữ liệu miễn phí, hãy dùng tab Preview trên giao diện hoặc dùng TABLESAMPLE!).

  3. Chọn kiểu dữ liệu chuẩn xác:

    Thay vì lưu trạng thái đơn hàng bằng những chuỗi dài lê thê hoặc lưu JSON dạng STRING (văn bản thô khiến Capacitor không thể tách cột con), hãy sử dụng kiểu dữ liệu STRUCT, ARRAY, hoặc kiểu JSON nguyên bản để Capacitor phát huy tối đa thuật toán nén RLE và Dictionary.

Tổng kết Bài học 3

Tốc độ "thần sầu" của Google BigQuery không đến từ phép thuật, mà đến từ cấu trúc lưu trữ Capacitor bên dưới tầng Colossus:

  • Columnar Layout: Chỉ đọc đúng những cột có mặt trong câu lệnh SQL.

  • Dictionary & RLE Compression: Nén dữ liệu theo cột cực mạnh và tự động sắp xếp lại dòng để tối ưu độ nén.

  • Direct Execution & Block Pruning: Đọc Metadata (Min/Max) để bỏ qua các khối dữ liệu thừa và tính toán trực tiếp ngay trên dữ liệu đang nén.

Chúng ta đã đi qua Dremel (Compute), Colossus (Storage) và Capacitor (Định dạng lưu trữ). Vậy còn mảnh ghép cuối cùng giúp hàng ngàn máy chủ này phối hợp nhịp nhàng như một cơ thể sống mà không bao giờ bị tắc nghẽn là gì?

Hẹn gặp lại anh em ở Bài học 4: Mạng lưới Jupiter & Borg – Cách BigQuery luân chuyển dữ liệu nội bộ với băng thông hàng Petabit/giây!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.