7.5 WAL, flush, và ý nghĩa của `wait`
Câu hỏi của mục này rất hẹp và rất quan trọng: từ đúng thời điểm nào thì dữ liệu bạn vừa ghi là an toàn, và từ đúng thời điểm nào thì nó tìm thấy được?
Hai câu hỏi đó không cùng một câu trả lời. Lẫn chúng là nguồn gốc của phần lớn bug “dữ liệu biến mất” trong hệ RAG.
Đường đi của một lần ghi
Phần tiêu đề “Đường đi của một lần ghi”client gửi upsert │ ▼ ① ghi vào WAL ──────────────► gán số thứ tự tuần tự │ ✅ AN TOÀN từ đây: mất điện không mất dữ liệu ▼ ② áp vào segment (trong RAM) │ ✅ TÌM THẤY ĐƯỢC từ đây ▼ ③ flush segment xuống đĩa ───► mặc định mỗi flush_interval_sec: 5 giây │ ▼ ④ optimizer xây index ───► TÌM NHANH từ đây (trước đó là quét tuần tự)Bốn mốc, và mỗi mốc trả lời một câu hỏi khác nhau:
| Mốc | Trả lời câu hỏi |
|---|---|
| ① WAL | Mất điện bây giờ thì có mất dữ liệu không? → Không |
| ② áp vào segment | Truy vấn bây giờ có thấy điểm này không? → Có |
| ③ flush | Khởi động lại có phải phát lại WAL không? → sau flush thì không cần |
| ④ index | Truy vấn có nhanh không? → trước đó là quét tuần tự (7.4) |
WAL — write-ahead log
Phần tiêu đề “WAL — write-ahead log”Qdrant làm mọi thay đổi qua hai bước. Bước một: ghi vào write-ahead log, nơi mọi thao tác được sắp thứ tự và gán một số tuần tự. Bước hai: áp thao tác đó vào segment và cấu trúc index.
Số tuần tự này chính là “phiên bản” ở 7.4, và nó là thứ khiến việc phát lại WAL sau sự cố trở nên an toàn: segment nào đã áp thao tác số N rồi thì bỏ qua, chưa áp thì áp.
Hai tham số, cả hai nằm trong storage.wal
(config.yaml v1.19.0):
| Tham số | Mặc định | Ý nghĩa |
|---|---|---|
wal_capacity_mb | 32 | Kích thước một segment WAL |
wal_segments_ahead | 0 | Số segment WAL cấp phát trước |
wal_segments_ahead đáng chú ý ở tải ghi cao: cấp phát trước file WAL nghĩa là lúc cần
xoay vòng, hệ thống không phải dừng lại để xin đĩa cấp chỗ. Đây là kiểu tối ưu chỉ có
nghĩa khi bạn đã thấy răng cưa trên đồ thị latency lúc ghi.
wait — tham số nhỏ, hậu quả lớn
Phần tiêu đề “wait — tham số nhỏ, hậu quả lớn”Mọi thao tác cập nhật nhận một tham số wait:
wait=false (mặc định) | wait=true | |
|---|---|---|
| API trả về khi | thao tác đã vào WAL | thao tác đã áp xong vào segment |
| Ghi xong rồi đọc ngay có thấy? | có thể chưa | thấy |
| Throughput ghi | cao | thấp hơn nhiều |
Quy tắc thực dụng:
Nạp hàng loạt →
wait=false. Ghi trong luồng người dùng mà ngay sau đó phải đọc lại →wait=true.
Và một cái bẫy: nếu bạn bật prevent_unoptimized (7.6), bạn bắt buộc
phải để wait=false trên mọi request ghi. Lý do: với prevent_unoptimized, điểm mới bị
giữ lại chưa cho tìm thấy cho tới khi segment tối ưu xong; wait=true khi đó nghĩa là
client ngồi chờ tới lúc index xong — dưới tải ghi liên tục, việc này gây timeout và
tắc nghẽn đầu hàng làm đứng cả pipeline cập nhật.
Flush và flush_interval_sec
Phần tiêu đề “Flush và flush_interval_sec”Mặc định flush_interval_sec: 5. Đây không phải khoảng thời gian dữ liệu có nguy cơ
mất — WAL đã lo phần đó ở mốc ①. Nó là khoảng thời gian giữa các lần ép segment xuống đĩa,
và nó quyết định bạn phải phát lại bao nhiêu WAL khi khởi động lại.
Giảm nó xuống thì khởi động lại nhanh hơn nhưng I/O nhiều hơn. Với đa số hệ, giá trị mặc định là đúng và bạn không nên đụng vào.
mmap: vì sao “vector nằm trên đĩa” không có nghĩa là “chậm”
Phần tiêu đề “mmap: vì sao “vector nằm trên đĩa” không có nghĩa là “chậm””Qdrant luôn lưu vector xuống đĩa. Khác biệt giữa “trong RAM” và “trên đĩa” thực chất là khác biệt về cách file được ánh xạ vào bộ nhớ, không phải về việc file có tồn tại hay không.
Với mmap, file trên đĩa được ánh xạ vào không gian địa chỉ ảo của tiến trình. Đọc một
vector là đọc một địa chỉ bộ nhớ; nếu trang đó đã nằm trong page cache của hệ điều hành
thì tốc độ ngang RAM, nếu chưa thì hệ điều hành lặng lẽ nạp nó từ đĩa.
Hệ quả rất thực tế, và nó phản trực giác:
Một collection “trên đĩa” trên máy có nhiều RAM rảnh sẽ chạy gần như nhanh bằng collection “trong RAM” — vì page cache giữ hết. Vấn đề chỉ xuất hiện khi tập dữ liệu nóng lớn hơn RAM còn lại, và khi đó đặc tính đĩa (IOPS, NVMe hay không) trở thành yếu tố quyết định.
Đây cũng là lý do đo Qdrant bằng “dung lượng RSS của tiến trình” hầu như luôn cho kết luận
sai. Mô hình bộ nhớ đầy đủ — pinned / cached / cold — là 7.10.
Gridstore thay cho RocksDB
Phần tiêu đề “Gridstore thay cho RocksDB”Trước đây Qdrant dùng RocksDB để lưu payload và một số cấu trúc phụ. Từ v1.13 Qdrant giới thiệu Gridstore, kho lưu trữ tự viết, và tới v1.17 thì RocksDB bị gỡ hoàn toàn.
Điều này có một hệ quả vận hành cụ thể mà bạn cần biết trước khi nâng cấp:
⚠️ Không nhảy thẳng từ v1.15.x lên v1.17.x. Phải đi qua v1.16.x để dữ liệu được chuyển đổi. Nói chung, nguyên tắc nâng cấp Qdrant là đi từng minor một, không nhảy cóc. Xem thêm ràng buộc phiên bản của snapshot ở 7.19.
Ba lỗi hay gặp quanh mục này
Phần tiêu đề “Ba lỗi hay gặp quanh mục này”- “Ghi xong đọc không thấy” và kết luận Qdrant mất dữ liệu. Gần như luôn là
wait=falsecộng với đọc từ một replica chưa kịp bắt kịp (7.16). Dữ liệu không mất — nó đang ở mốc ① mà bạn đang đọc ở mốc ②. - Bật
wait=truecho toàn bộ pipeline nạp dữ liệu. Throughput ghi sập, và nếu cóprevent_unoptimizedthì hệ thống đứng hẳn. - Giảm
flush_interval_secvì “sợ mất dữ liệu”. Nhầm mốc. WAL mới là thứ chống mất dữ liệu; flush chỉ ảnh hưởng thời gian khởi động lại.