Bỏ qua để đến nội dung
Search & RAG

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.

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ốcTrả lời câu hỏi
① WALMất điện bây giờ thì có mất dữ liệu không?Không
② áp vào segmentTruy vấn bây giờ có thấy điểm này không?
③ flushKhởi động lại có phải phát lại WAL không? → sau flush thì không cần
④ indexTruy vấn có nhanh không? → trước đó là quét tuần tự (7.4)

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_mb32Kích thước một segment WAL
wal_segments_ahead0Số 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.

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ề khithao tác đã vào WALthao tác đã áp xong vào segment
Ghi xong rồi đọc ngay có thấy?có thể chưathấy
Throughput ghicaothấ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.

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.

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.

  1. “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=false cộ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 ②.
  2. Bật wait=true cho toàn bộ pipeline nạp dữ liệu. Throughput ghi sập, và nếu có prevent_unoptimized thì hệ thống đứng hẳn.
  3. Giảm flush_interval_sec vì “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.
Phần 7 — Qdrant