Nguyên lý hoạt động của Log-Structured Merge Tree (LSM)
Mục tiêu: Hiểu cơ chế ghi nhật ký (append-only) thay thế cho ghi ngẫu nhiên (random write) để tối ưu hóa thông lượng I/O trên disk.
Giải thích: Trong cơ sở dữ liệu truyền thống (B-Tree), mỗi lần update có thể yêu cầu ghi vào một vị trí ngẫu nhiên trên disk, gây ra hiện tượng "seek" tốn kém. LSM tổ chức dữ liệu thành các tầng (Levels). Dữ liệu mới được ghi vào một file log tuyến tính (MemTable/SSTable) trước, sau đó mới được hợp nhất (merge) dần xuống các tầng thấp hơn khi đầy.
Thực hiện: Chúng ta sẽ tạo một script mô phỏng cơ chế ghi append-only của LSM để minh họa sự khác biệt so với ghi ngẫu nhiên.
cat > /tmp/lsm_simulation.sh > $LOG_FILE
echo "TXN_002: Key=User_B, Value=200" >> $LOG_FILE
echo "TXN_003: Key=User_A, Value=150 (Update)" >> $LOG_FILE
# Mô phỏng quá trình Merge (Compact)
# Trong thực tế, Seastar sẽ đọc các dòng trên, gộp User_A (lấy giá trị mới nhất) và ghi vào SSTable mới
echo "=== Simulating Compaction ==="
# Đọc log, gộp key, ghi ra file compacted
awk -F': ' '
{
key = $2
gsub(/, Value.*/, "", key)
value = $2
gsub(/.*Value=/, "", value)
data[key] = value
}
END {
for (k in data) print k " -> " data[k]
}' $LOG_FILE | sort > /tmp/lsm_compacted.sst
echo "=== Kết quả sau Compact ==="
cat /tmp/lsm_compacted.sst
EOF
chmod +x /tmp/lsm_simulation.sh
/tmp/lsm_simulation.sh
Kết quả mong đợi: Bạn sẽ thấy file log ban đầu chứa nhiều bản ghi cho cùng một Key (User_A xuất hiện 2 lần), sau khi chạy script "Compaction", file SSTable chỉ còn 1 dòng duy nhất với giá trị mới nhất (150). Đây là cách LSM giảm thiểu write amplification ban đầu.
Verify: Kiểm tra file kết quả bằng lệnh cat /tmp/lsm_compacted.sst. Đảm bảo User_A có giá trị 150.
Mô hình xử lý đồng thời và Non-blocking I/O trong Seastar
Cơ chế Reactor và Future/Promise
Mục tiêu: Hiểu cách Seastar xử lý hàng triệu I/O operations mà không bị block thread chính.
Giải thích: Khác với mô hình Thread-pool truyền thống, Seastar sử dụng mô hình "One Thread per Core". Mỗi core CPU chạy một "Reactor loop" riêng biệt. Khi một request I/O được gửi đi, Seastar không chờ (blocking) mà trả về một đối tượng Future. Khi I/O hoàn tất, kernel báo về, Reactor sẽ kích hoạt Promise tương ứng và tiếp tục xử lý logic tiếp theo. Điều này loại bỏ chi phí context switching.
Thực hiện: Tạo một file C++ đơn giản minh họa luồng xử lý non-blocking của Seastar (dù chưa compile, để hiểu cấu trúc code).
cat > /tmp/seastar_io_model.cpp
Kết quả mong đợi: Bạn thấy cấu trúc code sử dụng future và co_await. Lưu ý không có hàm pthread_create hay std::thread trong phần xử lý I/O.
Verify: Kiểm tra số lượng thread thực tế mà Seastar sử dụng (sau khi chạy application) bằng lệnh ps -eLf | grep seastar | wc -l. Con số này thường bằng số CPU cores, không phải hàng nghìn threads.
Cấu trúc dữ liệu vật lý: Segments, Keys và Log Files
Tổ chức file trên Disk
Mục tiêu: Hiểu cách Seastar lưu trữ dữ liệu thực tế trên disk dưới dạng các segment file.
Giải thích: Seastar không lưu dữ liệu dưới dạng một file lớn duy nhất. Dữ liệu được chia thành các Segments (thường 32MB hoặc 64MB). Mỗi segment là một file SSTable (Sorted String Table) hoặc một Log file. Mỗi record bên trong gồm: Key (dữ liệu định danh), Value (dữ liệu thực), Timestamp (để phân giải xung đột), và Checksum (kiểm tra lỗi disk).
Thực hiện: Tạo cấu trúc thư mục và file mẫu mô phỏng cấu trúc segment của Seastar.
mkdir -p /tmp/seastar_db/data/segment_001
mkdir -p /tmp/seastar_db/data/segment_002
# Tạo file segment đầu tiên (Log file ban đầu)
# Định dạng giả lập: Key|Value|Timestamp|Checksum
echo "user:1001|John Doe|1715600000000|0xABC123" > /tmp/seastar_db/data/segment_001/data.log
echo "user:1002|Jane Smith|1715600001000|0xDEF456" >> /tmp/seastar_db/data/segment_001/data.log
# Tạo file segment thứ hai (Kết quả sau khi compact/merge)
# Dữ liệu đã được sắp xếp và loại bỏ bản cũ
echo "user:1001|John Doe (Updated)|1715600050000|0xNEW123" > /tmp/seastar_db/data/segment_002/sstable.dat
echo "user:1002|Jane Smith|1715600001000|0xDEF456" >> /tmp/seastar_db/data/segment_002/sstable.dat
# Xem cấu trúc cây thư mục
find /tmp/seastar_db -type f -exec ls -lh {} \;
Kết quả mong đợi: Bạn thấy hai thư mục segment riêng biệt. Segment 1 chứa file log (ghi append), Segment 2 chứa file sstable (đã compact). Mỗi dòng là một record có đầy đủ Key, Value, Timestamp.
Verify: Kiểm tra kích thước file bằng du -h /tmp/seastar_db/data/. Đảm bảo các file tồn tại và có nội dung đúng định dạng pipe-delimited.
Vai trò của Sharding trong việc phân phối tải trên nhiều CPU Cores
Sharding và Affinity
Mục tiêu: Hiểu cách Seastar phân chia dữ liệu (Sharding) để tận dụng đa nhân CPU.
Giải thích: Seastar sử dụng kỹ thuật Sharding dựa trên hash của Key. Công thức: Core_ID = Hash(Key) % Num_Cores. Điều này đảm bảo tất cả các request cho cùng một Key luôn đi đến cùng một CPU core, giúp tận dụng cache CPU (L1/L2) và giảm contention. Mỗi core chỉ quản lý một phần dữ liệu (Shard) của database. I/O được phân tán đều ra các core, tránh điểm nghẽn (bottleneck) ở một core duy nhất.
Thực hiện: Viết script bash để mô phỏng thuật toán phân phối Key vào các Core (Shard) giống như Seastar.
cat > /tmp/seastar_sharding.sh
Kết quả mong đợi: Script in ra danh sách các Key được phân bổ vào các Core từ 0 đến 3. Bạn sẽ thấy các Key khác nhau có thể rơi vào cùng một Core, và các Key gần nhau (về giá trị hash) cũng có thể nằm cùng Core.
Verify: Chạy lại script nhiều lần. Đảm bảo cùng một Key (ví dụ "user:1") luôn trả về cùng một Core ID (tính toán determinstic). Điều này chứng minh tính nhất quán của sharding.
Tổng kết kiến trúc cơ bản
Mục tiêu: Khẳng định lại mối liên hệ giữa LSM, Non-blocking I/O và Sharding trong Seastar.
Giải thích: Seastar kết hợp 3 yếu tố: LSM giúp tối ưu hóa Write I/O bằng cách chuyển đổi random write thành sequential write. Non-blocking I/O (Reactor) giúp CPU không bị idle khi chờ Disk. Sharding giúp phân tải dữ liệu đều lên tất cả CPU cores, biến một máy chủ đơn thành một cụm xử lý song song.
Thực hiện: Kiểm tra cấu hình CPU hiện tại của server để xác định số lượng shard tối đa có thể sử dụng.
nproc
lscpu | grep "Core(s) per socket"
cat /proc/cpuinfo | grep "processor" | wc -l
Kết quả mong đợi: Bạn thấy số lượng logical processors. Khi triển khai Seastar database, bạn sẽ khởi động ứng dụng với tham số --io-queues hoặc --num-shards bằng với số lượng cores này để đạt hiệu năng tối đa.
Verify: Đảm bảo số cores nhận được khớp với cấu hình phần cứng của Ubuntu 24.04. Đây là tham số quan trọng cho các phần tiếp theo của series.
Điều hướng series:
Mục lục: Series: Triển khai Database Log-Structured với Seastar trên Ubuntu 24.04
« Phần 2: Xây dựng và cài đặt framework Seastar từ source code
Phần 4: Triển khai database Log-Structured đơn giản với Seastar »