7.4 Segment — đơn vị thật của Qdrant
Đây là mục quan trọng nhất trong Nhóm 2. Nếu bạn chỉ đọc một trang về “bên trong Qdrant”, đọc trang này.
Mô hình sai mà gần như ai cũng mang trong đầu lúc đầu:
“Một collection có một index HNSW. Tôi thêm điểm, index lớn dần.”
Mô hình đúng:
Một collection được chia thành nhiều segment. Mỗi segment là một cơ sở dữ liệu nhỏ, hoàn chỉnh, độc lập — có kho vector riêng, kho payload riêng, index riêng. Một truy vấn chạy trên mọi segment rồi hợp kết quả lại.
Collection └── Shard (1..n) ← đơn vị phân tán, xem 7.15 └── Segment (1..m) ← đơn vị lưu trữ + index ├── vector storage (float32 / float16 / uint8 / turbo4) ├── payload storage (Gridstore) ├── vector index (HNSW — hoặc không có, nếu segment còn nhỏ) ├── payload indexes (keyword, integer, text, …) └── id mapper (ID bên ngoài ⇄ ID nội bộ)Gần như mọi hành vi khó hiểu của Qdrant đều giải thích được từ sơ đồ này.
Vì sao lại chia nhỏ như vậy
Phần tiêu đề “Vì sao lại chia nhỏ như vậy”Vì HNSW có một tính chất khó chịu: nó không thích bị sửa. Thêm một điểm thì được; xoá nhiều điểm, hoặc đổi tham số, thì cấu trúc đồ thị xuống cấp và cách chữa duy nhất là dựng lại. Dựng lại một đồ thị 100 triệu điểm là chuyện của hàng giờ.
Chia thành segment biến bài toán “dựng lại một đồ thị khổng lồ” thành “dựng lại vài đồ thị vừa phải, lần lượt, trong khi các đồ thị còn lại vẫn phục vụ truy vấn”. Đây là đánh đổi kinh điển, giống hệt lý do LSM-tree tồn tại trong các cơ sở dữ liệu ghi nhiều (3.12 nói về vấn đề tương tự cho inverted index).
Appendable và non-appendable
Phần tiêu đề “Appendable và non-appendable”Hai loại segment, và sự khác nhau quyết định rất nhiều thứ:
| Appendable | Non-appendable | |
|---|---|---|
| Ghi thêm điểm mới | ✅ | ❌ |
| Đọc | ✅ | ✅ |
| Xoá (mềm) | ✅ | ✅ |
| Index HNSW | thường chưa có | có |
| Lưu trữ | ghi được | thường mmap, chỉ đọc |
Một collection luôn phải có ít nhất một segment appendable — nếu không thì không ghi được gì. Điểm mới luôn rơi vào segment appendable. Khi segment đó đủ lớn, optimizer sẽ biến nó thành non-appendable, xây HNSW cho nó, và mở một segment appendable mới.
Đây là câu trả lời cho một câu hỏi rất hay gặp:
“Tôi vừa upsert xong, sao truy vấn vẫn chậm / kết quả vẫn kỳ lạ?”
Vì điểm mới của bạn đang nằm trong một segment chưa có HNSW. Truy vấn phải quét tuần tự segment đó. Nó vẫn đúng — chỉ là đắt. Và nếu bạn ghi nhanh hơn tốc độ optimizer xây index, phần quét tuần tự đó lớn dần lên. 7.6 là mục nói về đúng chuyện này.
Ngưỡng đo bằng KB, không phải bằng số điểm
Phần tiêu đề “Ngưỡng đo bằng KB, không phải bằng số điểm”Đây là chi tiết gây nhầm nhiều nhất trong toàn bộ cấu hình Qdrant.
Các ngưỡng như indexing_threshold_kb, memmap_threshold, full_scan_threshold_kb
đo bằng kilobyte, không phải số điểm. Và quy ước quy đổi được ghi thẳng trong
config.yaml:
Note: 1Kb = 1 vector of size 256
Nghĩa là 1 KB ≈ một vector 256 chiều (256 × 4 byte = 1024 byte). Vậy giá trị mặc định
indexing_threshold_kb: 10000 tương đương:
| Số chiều | Số điểm ứng với 10.000 KB |
|---|---|
| 256 | ~10.000 |
| 768 | ~3.300 |
| 1536 | ~1.700 |
| 3072 | ~830 |
Người đọc indexing_threshold: 10000 rồi nghĩ “index bật khi có 10.000 điểm” sẽ ước
lượng sai gấp sáu lần trên embedding 1536 chiều. Nhớ quy tắc: chia 10.000 cho
(số chiều ÷ 256).
Versioning — vì sao Qdrant không loạn dữ liệu khi mất điện
Phần tiêu đề “Versioning — vì sao Qdrant không loạn dữ liệu khi mất điện”Mỗi segment giữ số phiên bản của thay đổi cuối cùng áp lên nó, và ngoài ra mỗi điểm cũng có phiên bản riêng. Số phiên bản đến từ WAL, nơi mọi thao tác được đánh số tuần tự (7.5).
Cơ chế rất đơn giản và rất hiệu quả:
Khởi động lại sau sự cố: với mỗi thao tác trong WAL (theo thứ tự số): với mỗi segment: nếu số thao tác <= phiên bản segment đã có → bỏ qua (đã áp rồi) ngược lại → áp lạiVì phép “áp lại” là idempotent theo phiên bản, Qdrant có thể phát lại WAL một cách mù quáng mà không sợ nhân đôi dữ liệu. Đây là lý do một segment có thể được flush xuống đĩa muộn hơn segment khác mà hệ thống vẫn nhất quán.
Id mapper — và vì sao nó có mặt
Phần tiêu đề “Id mapper — và vì sao nó có mặt”Bạn dùng ID kiểu u64 hoặc UUID. HNSW cần chỉ số nguyên liên tục, dày đặc, bắt đầu từ 0, vì nó lưu láng giềng bằng mảng. Id mapper là bảng dịch giữa hai thế giới đó, theo cả hai chiều.
Chi tiết ít người để ý nhưng có hệ quả: ID nội bộ bị “cháy” khi bạn xoá điểm. Chỗ trống chỉ được thu hồi khi vacuum optimizer dựng lại segment. Đó là một nửa lý do vì sao xoá nhiều mà dung lượng không giảm ngay (7.2).
Số segment: núm vặn giữa latency và throughput
Phần tiêu đề “Số segment: núm vặn giữa latency và throughput”Một truy vấn phải chạm mọi segment. Từ đó suy ra được cả hai chiều đánh đổi:
Nhiều segment nhỏ Ít segment lớn───────────────────── ─────────────────────+ một truy vấn chạy song song + mỗi truy vấn chạm ít index hơn trên nhiều segment → latency thấp → tổng công ít hơn → throughput cao+ merge/dựng lại rẻ, phục hồi nhanh + đồ thị HNSW lớn hơn thì hiệu quả hơn− tổng công lớn hơn cho mỗi truy vấn − dựng lại lâu, có khi hàng giờ− nhiều đồ thị HNSW nhỏ → recall kém hơn − sau đợt ghi lớn, backlog lâu tanKhuyến nghị của Qdrant (số dẫn từ nguồn ngoài):
- Muốn latency thấp (ít truy vấn đồng thời, cần trả lời nhanh): đặt
default_segment_numberbằng số nhân CPU — mỗi truy vấn được chia cho tất cả các nhân. - Muốn throughput cao (nhiều truy vấn đồng thời): ít segment lớn, ví dụ
default_segment_number: 2vớimax_segment_sizelớn — mỗi truy vấn dùng ít nhân, nhiều truy vấn chạy song song.
Mặc định default_segment_number: 0 nghĩa là tự chọn theo số CPU. Với đa số hệ, đây
là lựa chọn đúng, và bạn chỉ nên đụng vào sau khi đã đo.
Bài đo của Qdrant về chính đánh đổi này cho thấy khoảng cách rất rõ (số dẫn từ nguồn
ngoài, xem Đọc thêm): cấu hình một segment duy nhất đạt 3,2 ms median lúc ổn định
nhưng cần hơn một giờ để tiêu hết backlog sau khi nạp; cấu hình chặn segment ở
100.000 KB tiêu backlog trong 283,9 giây (nhanh hơn ~12,7×) nhưng median lúc ổn định
là 17,3 ms. Cùng một dữ liệu, cùng một máy — chỉ khác cách chia segment.
Ba câu để chẩn đoán
Phần tiêu đề “Ba câu để chẩn đoán”Khi Qdrant hành xử lạ, hỏi ba câu này theo thứ tự:
- Có bao nhiêu segment, và bao nhiêu segment đã có index? Xem
/collections/{name}và/telemetry. Nhiều điểm chưa index = truy vấn đang quét tuần tự. - Collection đang màu gì? Vàng mãi nghĩa là optimizer không đuổi kịp (7.6).
- Ngưỡng của tôi tính bằng KB hay tôi đang tưởng là số điểm? Xem lại bảng quy đổi ở trên.