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

7.6 Optimizer: ba loại, và vì sao latency nhấp nháy

Optimizer là tiến trình nền dọn dẹp segment. Nó là nguyên nhân số một của hiện tượng “Qdrant lúc nhanh lúc chậm mà không hiểu vì sao”.

Lý do rất trực tiếp: optimizer và truy vấn tranh nhau đúng một bộ tài nguyên — CPU, băng thông bộ nhớ, I/O. Qdrant nói thẳng điều này trong docs. Bạn không tắt được sự tranh chấp đó; bạn chỉ chọn được nó xảy ra khi nào và ở mức nào.

OptimizerKích hoạt khiViệc nó làm
Vacuumtỷ lệ điểm đã xoá trong một segment ≥ deleted_threshold segment có ≥ vacuum_min_vector_number vectorDựng lại segment, thu hồi chỗ của điểm đã xoá
Mergesố segment nhiều hơn default_segment_number mong muốnGộp các segment nhỏ nhất lại
Indexinglượng dữ liệu trong segment vượt indexing_threshold_kb (hoặc memmap_threshold)Xây HNSW, chuyển sang mmap, áp quantization

Mặc định, lấy từ config.yaml v1.19.0:

optimizers:
deleted_threshold: 0.2 # 20% điểm đã xoá thì mới vacuum
vacuum_min_vector_number: 1000 # segment nhỏ hơn thế thì kệ nó
default_segment_number: 0 # 0 = tự chọn theo số CPU
max_segment_size_kb: null # null = tự chọn theo số CPU
indexing_threshold_kb: 10000 # 0 = tắt hẳn việc xây index
flush_interval_sec: 5
max_optimization_threads: null # null = tự do; 0 = TẮT optimizer

Nhớ lại 7.4: các ngưỡng _kb đo bằng kilobyte, quy ước 1 KB ≈ 1 vector 256 chiều. indexing_threshold_kb: 10000 trên embedding 1536 chiều là khoảng 1.700 điểm, không phải 10.000.

Proxy segment — vì sao tối ưu không làm rớt truy vấn

Phần tiêu đề “Proxy segment — vì sao tối ưu không làm rớt truy vấn”

Câu hỏi tự nhiên: nếu optimizer đang dựng lại một segment, truy vấn chạm vào segment đó thì sao?

Câu trả lời là proxy segment. Trong lúc dựng lại, Qdrant bọc segment cũ bằng một lớp proxy: đọc vẫn phục vụ được từ bản cũ, ghi mới được ghi vào cả nơi cần thiết, và khi bản mới xong thì tráo. Không có cửa sổ nào mà dữ liệu biến mất.

Đây là điểm khác biệt thật giữa một vector database và một thư viện ANN: với hnswlib, “dựng lại index” nghĩa là bạn tự lo phần phục vụ trong lúc dựng.

Tải ghi cao
├──► điểm mới dồn vào segment appendable
│ │
│ ▼
│ segment này CHƯA CÓ HNSW
│ │
▼ ▼
optimizer cố xây mọi truy vấn phải QUÉT TUẦN TỰ segment đó
index │
│ ▼
└──► tranh CPU/IO ◄─── truy vấn chậm đi
optimizer càng chậm → segment chưa index càng lớn → truy vấn càng chậm

Đây là một vòng lặp dương. Khi nó bắt đầu, hệ không tự thoát ra — nó chỉ thoát khi tải ghi giảm.

Bài đo của chính Qdrant về hiện tượng này cho các con số sau (số dẫn từ nguồn ngoài, xem Đọc thêm):

Tình huốngMedian latencyGhi chú
Index liên tục, ổn định~4 msmốc so sánh — suy ra từ tỷ lệ 60× mà bài nêu, không phải số bài ghi trực tiếp
Tắt hẳn indexing256,6 ms~60× chậm hơn — đây là chi phí thật của quét tuần tự
Bật lại index sau khi hoãn, khôngprevent_unoptimized2,7 s (đuôi tới 12,1 s)truy vấn quét đi quét lại đúng cái backlog đang phình
Cùng tình huống, prevent_unoptimized10,2 ms (từ 780 ms)cải thiện ~76× ở giai đoạn tiêu backlog

Đọc bảng này cho đúng: hoãn indexing để “nạp cho nhanh” là một sai lầm đắt. Trực giác “tắt index lúc nạp, bật lại sau” nghe hợp lý nhưng bài đo bác bỏ nó — giai đoạn phục hồi sau đó tệ hơn nhiều so với việc cứ để index chạy liên tục.

Có từ v1.17.1, vẫn được đánh dấu là thử nghiệm. Ý tưởng một dòng:

Điểm vừa ghi vẫn được lưu bền vững, nhưng không cho truy vấn nhìn thấy cho tới khi segment chứa nó tối ưu xong.

Nghĩa là truy vấn không bao giờ phải quét segment chưa index. Vòng lặp dương ở trên bị cắt.

Cái giá — và phải nói rõ vì nó không nhỏ:

  1. Điểm mới không tìm thấy được ngay. Bạn đổi độ tươi lấy độ ổn định. Chỉ dùng khi trễ vài giây tới vài chục giây trước lúc dữ liệu tìm thấy được là chấp nhận được.
  2. Bắt buộc wait=false trên mọi request ghi. Với wait=true, client ngồi chờ tới lúc index xong, gây timeout và tắc nghẽn đầu hàng (7.5).

Thứ tự xử lý khi truy vấn chậm dưới tải ghi

Phần tiêu đề “Thứ tự xử lý khi truy vấn chậm dưới tải ghi”

Qdrant đưa ra một danh sách chín bước theo thứ tự. Đây là bản rút gọn có chú giải, và thứ tự quan trọng — đừng nhảy xuống bước 8 trước:

#ViệcVì sao ở vị trí này
1Bật prevent_unoptimized (kèm wait=false)Rẻ nhất, tác dụng lớn nhất, hoàn tác dễ
2Giảm kích thước batch ghiBatch lớn tạo cú sốc I/O; đổi throughput lấy latency
3Giảm optimizer_cpu_budgetCho optimizer ít CPU hơn — nó chậm lại, truy vấn nhanh lên
4Chỉnh max_optimization_threads, max_indexing_threadsTinh chỉnh của bước 3
5Dùng delayed fan-out khi có replicaKhông phải replica nào cũng bị hỏi cùng lúc
6Bật I/O bất đồng bộ (io_uring, chỉ Linux)Giảm chờ I/O của rescore/đọc nguội
7Giảm max_segment_sizeSegment nhỏ dựng nhanh hơn; đổi latency ổn định lấy tốc độ phục hồi
8Thêm replica (scale ngang)Tốn tiền
9Máy to hơn (scale dọc)Tốn tiền hơn

Về optimizer_cpu_budget: mặc định 0 nghĩa là tự chọn và luôn chừa lại ít nhất một CPU. Số âm nghĩa là “trừ đi từng ấy CPU khỏi số CPU khả dụng” — đây thường là cách diễn đạt đúng ý bạn hơn là đặt một con số tuyệt đối.

Về max_optimization_threads: đặt 0 sẽ tắt hoàn toàn optimizer. Đừng làm vậy trừ khi bạn hiểu rõ hậu quả ở bảng đo phía trên.

Qdrant còn khuyên một điều dễ bỏ qua: nếu đĩa cho phép, hãy đặt deleted_threshold cao hơn mặc định 0,2 — để vacuum không chen vào giữa lúc đang phục vụ truy vấn.

Ba nơi để nhìn, theo thứ tự tăng dần độ chi tiết:

  1. Màu collection — vàng mãi = optimizer chưa đuổi kịp (7.2).
  2. /collections/{name} — so points_count với indexed_vectors_count. Khoảng cách giữa hai số này chính là phần đang bị quét tuần tự.
  3. /metrics/telemetry — số tác vụ tối ưu, latency theo endpoint (7.20).

Một metric duy nhất nếu chỉ được chọn một: khoảng cách giữa points_countindexed_vectors_count. Nó đi trước triệu chứng latency, nên nó là cảnh báo sớm chứ không phải khám nghiệm tử thi.

Phần 7 — Qdrant