Milvus 2.6.x 主线强调存算分离与 Streaming/Query/Data 三分(第 1–14 篇)。Qdrant 走另一条常见生产路径:Rust 实现的向量库,单节点即可完整服务,分布式用 分片 + 副本 扩展。本文按官方 Storage、Optimizer、Distributed deployment、Filtering 文档做对照,重点回答两个容易被「谁的 HNSW 更快」掩盖的问题:单进程栈里数据到底怎么落盘、集群里哪些操作真正需要共识。不写成 Qdrant 教程。
本文是「向量检索引擎」系列第 15 篇(共 19 篇)。→ 系列目录
版本锚定:Qdrant 官方文档(Storage、Optimizer、Distributed deployment、Filtering);与 Milvus 对照时机制以本系列已钉的 2.6.x 为准。
一、产品形态对照
| 维度 | Milvus 2.6.x(本系列) | Qdrant(官方文档) |
|---|---|---|
| 典型部署 | Proxy + Coordinator + 多类 Worker + 对象存储 + WAL | 单二进制可跑全功能;集群为对等 peer |
| 写日志 | Woodpecker / Kafka / Pulsar(第 5 篇) | 每个 collection 先写 WAL,按序号落到对应 segment |
| 历史数据 | Sealed 在对象存储,Query Node 加载 | segment
本地(及副本)持有,appendable/non-appendable
两态 |
| 过滤 | 表达式 → bitset → Knowhere(第 11 篇) | payload 条件 + 建议建 payload index |
| 水平扩展 | Channel/shard + Worker 扩缩 | Collection 拆
shard,replication_factor
控制副本 |
| 集群共识 | Coordinator 单活跃,无 Raft 式投票 | Raft 管拓扑与 collection 结构,不管点操作 |
核心差异不是「谁的 HNSW 更好」,而是 状态放哪、组件拆多细、共识覆盖到什么粒度、运维面多大。
二、最小故事:同一条 insert,两套系统怎么落地
设想客户端对同一批 100 条向量分别发 insert
给 Milvus 集群和单节点
Qdrant,都立刻返回成功。这条请求接下来发生的事完全不同:
Milvus 侧(第 3、5 篇已钉):Proxy 按 shard 规则拆包,送到负责该 vchannel 的 Streaming Node;Streaming Node 给这批操作分配 TSO,写入 Woodpecker/Kafka/Pulsar 一类 WAL;提交后应用到 Growing Segment,才真正可搜。整条链路跨越 Proxy、Streaming Node、WAL 存储三个独立进程/组件。
Qdrant 侧(Storage 文档):数据变更分两步——先写入 Write-Ahead-Log,WAL 给每个操作分配一个全局有序的序号;然后变更进入 segment。每个 segment 记录「最后应用的变更版本」以及「每个点的版本」:如果新变更的序号小于该点当前版本,更新器直接忽略这条变更。这条规则本身就是一种幂等保护——重放 WAL 时,旧序号的变更不会覆盖已经更新到更新版本的点。
两边都遵循「先日志、后数据」的经典模式(ARIES 谱系),但作用域不同:Milvus 的 WAL 提交是跨进程网络往返(Proxy → Streaming Node → WAL 存储),Qdrant 单节点场景下 WAL 与 segment 更新都在同一进程内,网络往返只发生在分布式部署时的副本复制路径上。这解释了单节点 Qdrant 为何常被描述为「写入延迟更低」——不是算法更快,而是少了跨进程组件。
三、单节点甜区
Qdrant 官方 Distributed deployment 明确:若最看重成本,可跑 单节点——同时写明不适合生产高可用(重启即停服、无副本则难自愈)。
工程含义:
- 中小规模 RAG、原型、边车式检索:单进程 Qdrant 运维面远小于「MinIO + etcd + 多 Worker」的 Milvus 集群。
- 垂直扩展有上限;官方 overview 亦强调最终要靠 sharding / replication / segment 配置做水平与吞吐优化。
Milvus 也有单机/精简部署形态,但本系列内核叙事默认云原生四分;选型时比较的是你真正要运维的拓扑,不是营销页上的「也能 Docker 一键起」。
四、Qdrant 单进程栈:Segment 里装的是什么
官方 Storage 文档把一个 collection 内的数据切成多个 segment,每个 segment 独立持有:
| 组成 | 官方定义 | 与 Milvus 的对照落点 |
|---|---|---|
| 向量存储 | In-memory(全放 RAM,速度最高)或 memmap(映射磁盘文件,靠 page cache)二选一 | 对应 Query Node 段内向量数据是否常驻内存(第 8 篇 Knowhere 假设) |
| payload 存储 | InMemory(启动即加载进 RAM)或 OnDisk(经 Gridstore 读写,省内存但访问延迟高) | 对应 Milvus 标量字段与索引的存放(第 11 篇) |
| id mapper | 维护内部 id 与外部 id 的映射 | 对应 Milvus 主键索引(第 13 篇) |
| 向量/payload 索引 | HNSW、payload index 等 | 对应 Knowhere VecIndex + 标量索引(第 8、11
篇) |
segment 有 appendable(可读写)与
non-appendable(只读,只能删除)两种状态;collection
内至少要保留一个 appendable segment。这个二分与
Milvus 的 Growing/Sealed(第 3
篇)目标一致——可变的实时结构 +
不可变的优化结构——但落点不同:Qdrant 的
appendable/non-appendable 是同一节点内、由后台
optimizer 决定的存储形态切换,Milvus 的
Growing/Sealed 则是分别绑定到不同 Worker
角色(Streaming Node vs Query Node)、需要跨进程
handoff 的状态迁移。
把两套系统的整体分层画在一张图上,能更直接看出「单进程栈」与「四层拆分」的差别:
flowchart TB
subgraph qdrant ["Qdrant single-process stack"]
qcoll["Collection"]
qshard["Shard"]
qseg["Segment"]
qvec["Vector storage<br/>in-memory or memmap"]
qpay["Payload storage<br/>InMemory or OnDisk + Gridstore"]
qidx["HNSW index + payload index"]
qidmap["Id mapper"]
qopt["Background optimizer<br/>merge / indexing / vacuum"]
qcoll --> qshard --> qseg
qseg --> qvec
qseg --> qpay
qseg --> qidx
qseg --> qidmap
qopt -.->|"rebuild via proxy segment"| qseg
end
subgraph milvus ["Milvus four layers"]
access["Access: Proxy"]
coord["Coordinator: DDL/TSO/handoff"]
sn["Streaming Node: WAL + Growing"]
qn["Query Node: Sealed search"]
dn["Data Node: index build/compaction"]
storage["Storage: etcd + object store + WAL"]
access --> coord
access --> sn
coord --> sn
coord --> qn
coord --> dn
sn --> storage
qn --> storage
dn --> storage
end
图中最值得注意的差异是 optimizer 与 segment
的关系:Qdrant 的 merge / indexing / vacuum
是同一进程内针对 segment 的后台任务,直接改写
SegmentHolder 里的引用;Milvus 的 Data Node
建索与 compaction 是独立 Worker 角色,要经
Coordinator 调度、经对象存储中转,再由 handoff 缝合到 Query
Node(第 10 篇)。两者做的事同构(后台整理 +
索引构建),但组件边界差一整层网络与协调。
4.1 后台 optimizer 四类任务
官方 Optimizer 文档列出四种优化器,各自的触发条件与目的不同:
| Optimizer | 目的 | 触发条件(官方) |
|---|---|---|
| Merge | 合并过多的小 segment | segment 数超过
default_segment_number(默认等于 CPU
数),至少取三个最小 segment 合并 |
| Indexing | 给普通 segment 建 HNSW 索引、启用 memmap | segment 大小超过 indexing_threshold_kb |
| Vacuum | 回收软删占用的空间,修复因软删产生的「索引碎片」 | 删除比例超过 deleted_threshold |
| Config mismatch | segment 的 HNSW/量化配置与当前 collection 配置不一致时重建 | collection 配置变更后 |
优化过程中,被优化的 segment 通过 proxy segment(写时复制)保持可读:变更先落到一个新的可写 segment,优化完成后原地替换。这与 Milvus compaction 完成后由 Coordinator 发起 handoff(第 10 篇)目标一致——都是「后台重写不能阻塞前台服务」,Qdrant 用进程内的 copy-on-write 包装,Milvus 用跨 Worker 的视图切换。
把 Merge optimizer
的触发条件代入一个具体节点规格,能看出这个默认值背后的直觉:一台
16 核的节点,default_segment_number 默认等于
16,意味着单个 collection 的 segment 数量长期维持在 16
左右——这个数字既不是任意选的,也不是”越多越好”:segment
数太少(例如强行合并成 1
个),单次写入触发的重建成本会很高,且失去了并行处理多个
segment 的能力;segment 数太多,每次 search 需要扇出到更多
segment 再合并,单次查询的调度开销上升。用 CPU
数作为默认阈值,隐含的假设是”segment
数大致对齐可并行处理的核数”——这和 Milvus 里 segment
大小策略要在”堆积风险”与”过度分裂”之间取平衡(第 3、10
篇)是同一类工程直觉的两种参数化方式:一个按 CPU
数定数量,一个按字节数定大小。
五、分布式:Raft 管拓扑、不管点数据
Distributed deployment(A 级要点)中最容易被忽视的一句话是:Qdrant 用 Raft 共识协议维护集群拓扑与 collection 结构的一致性,但对点(point)的操作不经过共识基础设施。换句话说,Raft 管的是「collection 有哪些、shard 怎么分、副本在哪个 peer」,不管「这一条 insert 具体写没写成功」——那部分靠副本集上的复制路径(write_consistency_factor,见第六节)解决。
这条边界画出了两套完全不同的一致性代价:
| 操作类别 | 是否经过 Raft | 一致性保证来源 |
|---|---|---|
| 创建/删除 collection、shard 迁移、加/减 peer | 是 | Raft 日志复制 + 多数派确认 |
| insert / update / delete 某个点 | 否 | 副本集复制 + write_consistency_factor |
官方给出的高可用建议:至少运行三个投票节点——两节点集群无法在任一节点不可用时形成多数派,Raft 就无法选出或确认 leader,直到两节点重新可通信。这与经典分布式共识的多数派下限完全一致:Raft 论文(Ongaro & Ousterhout, USENIX ATC 2014)证明的正是「多数派存活即可继续服务」这一性质,\(n\) 节点集群最多容忍 \(\lfloor (n-1)/2 \rfloor\) 节点故障。
flowchart TB
subgraph control ["Control plane: goes through Raft"]
createColl["create/drop collection"]
shardOp["shard transfer / rebalance"]
peerOp["add/remove peer"]
end
subgraph data ["Data plane: bypasses Raft"]
insertOp["insert/update/delete a point"]
end
control --> raftLog["Raft log<br/>majority ack required"]
data --> replSet["Replica set on shard<br/>write_consistency_factor ack"]
raftLog --> topology["Cluster topology + collection metadata"]
replSet --> pointData["Point vectors + payload in segments"]
其余分布式要点(官方 Distributed deployment):
- Collection 由一个或多个 shard
组成;
replication_factor(默认 1)控制每 shard 的副本数,生产建议 ≥2。 - Shard transfer:可通过 cluster setup API 显式把某个 shard 从一个 peer 迁到另一个 peer,用于扩容或缩容;缩容时先把 shard 迁走再移除 peer。
- 故障恢复:若失败节点重启,consensus 会触发复制流程补齐它错过的更新;若节点永久失败且集群规模 ≥3 节点,可以恢复丢失的 shard——恢复操作本身也走 Raft,因此同样要求超过半数节点健康;小于 3 节点的集群无法执行这类恢复。
- 共识检查点(Consensus Checkpointing):Raft 定期为系统状态生成一致快照以简化日志管理与提升性能;该 API 可在任意非 leader 节点触发,实际会转发给当前 leader 生成快照。
与 Milvus Channel / vchannel(第 3 篇)对照:两者都把集合切成可并行单元,但 Qdrant shard 是 带完整点存储的对等分片,扩缩与恢复走统一的 Raft + 副本机制;Milvus channel 绑定 Streaming Node,历史段另由 Query Node 从对象存储加载,协调靠单活跃 Coordinator 而非多数派投票。Milvus 没有 Raft 式的集群拓扑共识——Coordinator 本身的高可用依赖外部 etcd 选主,不是本文对照的重点,感兴趣可回读第 4 篇。
5.1
写一致性:write_consistency_factor
write_consistency_factor(默认
1)定义一次写操作需要多少个副本确认才能向客户端返回成功。调大它可以让写操作更能容忍网络分区(因为已确认的副本数更多),但代价是需要更多副本处于活跃状态才能完成写入;若设成等于
replication_factor,未完全复制的更新会被直接拒绝,从而省去后续的额外同步。这与
Milvus 的一致性级别(第 12 篇的
Strong/Bounded/Session/Eventually)是两个不同轴上的旋钮:Milvus
调的是「查询愿意等多旧的可见性水位」,Qdrant 的
write_consistency_factor
调的是「写入愿意等多少个副本确认」——前者作用于读路径,后者作用于写路径,不能直接类比谁更强。
六、Payload 过滤
Filtering:搜索/检索时可对
payload 与点 id
设条件;业务属性(库存、地域、价格带)往往无法全部进入
embedding。
官方强调:要性能,应对过滤字段建 payload
index,且 最好在摄入数据前
建好;一旦建了字段索引,无论 payload 存储类型是 InMemory
还是
OnDisk,该字段的全部取值都会常驻内存,避免过滤时触发磁盘访问。条件可用
must / should /
must_not 等子句嵌套,表达任意布尔组合。
对照 Milvus:
| Milvus | Qdrant | |
|---|---|---|
| 标量载体 | 字段 + 布尔表达式 | JSON-like payload |
| 加速结构 | 标量索引 + bitset 进 Knowhere | payload index(建索后强制常驻内存) |
| 存储位置可调 | 无独立「payload 落盘」开关 | on_disk_payload 可选择整体落盘节省内存 |
| 选择度陷阱 | 同第 11 篇 / frontier/09 | 同样存在;索引只降过滤成本,不改变选择度本身 |
不要假设「Qdrant 过滤一定比 Milvus 快」——取决于索引是否建、选择度、以及是否在图遍历中有效剪枝(同第 8、11 篇的 bitset 讨论)。
七、何时选 Qdrant 而不是上 Milvus 集群
| 更偏向 Qdrant | 更偏向 Milvus 集群 |
|---|---|
| 团队要少组件、快上线 | 已有对象存储/K8s 平台,要存算分离弹性 |
| 数据中小、可垂直扩展一阵 | 十亿级、索引构建要独立扩 Data/Index 类负载 |
| payload 过滤模型贴业务 JSON | 要强绑定多一致性级别产品语义(第 12 篇) |
| 分片副本模型可接受、能接受 Raft 3 节点门槛 | 需要 Woodpecker/零盘 WAL 等云原生写路径 |
| 建索/compaction 与在线查询共用同一批 segment 即可 | 需要把离线建索独立成资源池(第 10 篇) |
第 18 篇给出决策树;本篇只钉对照坐标。
八、常见误解
误解一:Qdrant 是「轻量版 Milvus」,功能是 Milvus
的子集。
两者是不同的架构选择,不是能力包含关系。Qdrant 的 segment 级
optimizer(merge/indexing/vacuum/config mismatch)、Raft
拓扑管理、write_consistency_factor 等机制在
Milvus 里没有直接对应物;Milvus
的存算分离、多一致性级别、离线 Data Node 资源池在 Qdrant
里也没有直接对应物。两边解决问题的方式不同,不是谁功能更全。
误解二:Qdrant 集群里所有操作都要走
Raft,所以写入吞吐受共识协议限制。
第五节已明确:点操作(insert/update/delete)绕过共识基础设施,走的是副本集复制路径,受
write_consistency_factor 约束,不是 Raft
日志复制。真正受 Raft 限制的是拓扑变更类操作(建
collection、shard 迁移、增减
peer),这类操作频率远低于点写入。
误解三:只要副本数(replication_factor)设得高,集群就能容忍任意比例的节点故障。
副本数只影响数据面的冗余;控制面的拓扑变更与故障恢复仍然要求
超过半数 Raft 投票节点健康。一个 3
节点集群里即使某个 collection 的副本数设成
3,一旦两个节点同时故障,剩下一个节点也无法独自完成需要共识的拓扑级恢复操作。
误解四:Qdrant 的 segment
数量应该尽量少,合并成一个大 segment 性能最好。 第
4.1 节已经说明 default_segment_number 默认按
CPU 数设置,不是越少越好:过少的 segment
数会让单次写入触发的重建成本过高,也失去了跨 segment
并行处理的能力。Merge optimizer 的目标是”合并过多的小
segment”,不是”最终只剩一个 segment”——把 segment
数量当成一个需要归零的指标,是对 optimizer
设计目标的误读。
九、学术谱系与开放问题
9.1 谱系
| 层 | 代表 | 在本篇的落点 |
|---|---|---|
| 共识协议 | Ongaro, D., Ousterhout, J., In Search of an Understandable Consensus Algorithm, USENIX ATC 2014(Raft) | Qdrant 拓扑与 collection 结构一致性的直接来源 |
| WAL + 版本号幂等 | 经典「日志序号去重」思路(ARIES 谱系的工程变体) | Qdrant segment 按点版本号丢弃过期变更(第二节) |
| 后台整理不阻塞前台 | 与 LSM/列存 merge、Milvus compaction 同属一类工程分形 | proxy segment 写时复制(第四节) |
| 段式存储引擎 | 类似 Bleve/Lucene 的 segment 抽象(不可变段 + 后台合并) | Qdrant segment 的 appendable/non-appendable 二态 |
Milvus 没有采用 Raft 式多数派共识来管理集群拓扑,而是让单活跃 Coordinator 持有权威视图(第 4 篇),高可用靠外部 etcd 选主;Qdrant 把这一层显式做成了 Raft。这不是谁的设计更先进,而是两种不同的工程取舍:单活跃 Coordinator 减少了共识协议的实现与运维复杂度,代价是 Coordinator 故障切换依赖外部组件;Raft 把拓扑一致性收进自己的二进制里,代价是必须接受多数派下限(至少 3 节点才能自愈)这一约束。
9.2 开放问题
- Qdrant 自定义 shard key 与 Milvus partition 在多租户下的运维成本比较——两者都能按业务键切分数据,但 shard key 影响的是物理分片,partition 更接近逻辑过滤域(第 3 篇),语义不完全对齐。
- 两边 HNSW 参数默认值不同导致的「同数据不同召回」评测规范:Qdrant 的 indexing/memmap 阈值与 Milvus 的 segment 大小策略都会间接影响单次索引训练看到的数据分布(对照第 8 篇 Knowhere 的「同段训练同段搜索」假设)。
- 混合云(厂商托管)下,
write_consistency_factor与 Milvus 一致性级别的产品化程度差异,是否应纳入 SLO 对照而非只比开源内核。
十、小结
三句话小结:Qdrant 用单进程内的 segment(向量存储 + payload 存储 + id mapper + 索引)与四类后台 optimizer 换取部署简单,分布式扩展靠 Raft 管拓扑、副本集管数据两条独立路径;Milvus 用跨进程的 Access/Coordinator/Worker/Storage 四层与单活跃 Coordinator 换取存算分离弹性,一致性靠 GuaranteeTs 而非多数派投票;两边的过滤都是「标量条件进检索」,载体分别是 payload index 与 bitset/表达式,选型的关键是你愿意在哪一层付出运维复杂度,而不是哪家 HNSW 实现更快。
下一篇:Lance / LanceDB 对照。
参考资料
- Qdrant Documentation, Storage(segment 组成:vector storage、payload storage、id mapper、appendable/non-appendable、WAL + 版本号)。
- Qdrant Documentation, Optimizer(merge/indexing/vacuum/config mismatch 四类 optimizer、proxy segment 写时复制、阈值参数)。
- Qdrant Documentation, Distributed
deployment(Raft
管拓扑不管点操作、
replication_factor、write_consistency_factor、shard transfer、故障恢复的多数派下限、Consensus Checkpointing)。 - Qdrant Documentation,
Filtering(payload、clauses、payload
index、
on_disk_payload)。 - Ongaro, D., Ousterhout, J., In Search of an Understandable Consensus Algorithm, USENIX ATC 2014(Raft 论文;多数派容错的理论来源)。
- 本系列第 1、3、4、8、10、11、12、13、14 篇;系列 index。
返回 系列目录 | 上一篇:副本与故障恢复 | 下一篇:Lance 对照
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【向量检索引擎】ANN 算法工程接口:从 HNSW/IVF 到 Knowhere 契约
把 HNSW、IVF、DiskANN、Flat 收成引擎侧 Train/Build/Load/Search 契约与构建期/查询期参数面;用生命周期图与召回–QPS–内存三角说明索引如何贴着 Segment,并与 db-frontier/08、第 8 篇 Knowhere 分工。
【向量检索引擎】Collection · Partition · Segment · Channel:Growing 到 Sealed 的状态机
用最小故事钉住 Milvus 2.6.x 数据模型:Collection/Partition、vchannel/pchannel 与 Streaming Node 绑定,Growing/Sealed、flush 与 handoff 状态机,并纠正「一个 Collection 一张大图」等常见误解。
【向量检索引擎】对象存储上的 Segment 布局:快照、索引与寻址代价
以 flush 后去对象存储里查看文件为线索,拆解 Milvus 2.6.x 的对象路径结构(insert_log/delta_log/stats_log/index_files)、V1 按字段拆分与 V2 按段整合 Parquet 的寻址代价差异,并给出官方文档中可核对的 API 调用量级引用。
【向量检索引擎】Knowhere:向量索引执行引擎与插件契约
按官方 Knowhere 文档说明其在 Milvus 中的位置、相对 Faiss 的扩展(bitset、SIMD 选择、二进制度量)、VecIndex 类层次与 IDMAP/IVF/HNSW 等类型,用插件注册、CPU/GPU 分发与 bitset 进查询三张图钉住工程契约,并与 db-frontier/08 的算法细节分工。