第 1 篇 给出了 Milvus 四层坐标系。排障与读源码时,真正反复出现的不是「向量库」三个字,而是一组更硬的名词:Collection、Partition、Segment、Channel(vchannel / pchannel),以及 Segment 在 Growing 与 Sealed 之间的迁移。
本文只建立数据模型与状态机,不展开 WAL 实现细节(第 5 篇)也不展开 Knowhere 索引类型(第 8 篇)。读完应能回答:一条 insert 先落在哪种 Segment 上?何时变成不可变?谁负责把它交给 Query Node?
本文是「向量检索引擎」系列第 3 篇(共 19 篇)。→ 系列目录
版本锚定:Milvus v2.6.21 Data Processing、Architecture Overview、Streaming Service。
一、最小故事:一条 insert 落在哪
客户端对 Collection docs
插入一批向量。引擎不会把它们塞进「一个名为 docs
的大数组」,而是:
- 按 shard 规则把行分到某个 vchannel;
- vchannel 映射到 pchannel,绑定到一台 Streaming Node;
- 该节点赋 TSO、写 WAL,追加进当前 Growing Segment;
- 策略触发后 Growing flush 成 Sealed,落对象存储;
- Data Node 建索引后,Coordinator handoff,由 Query Node 加载历史段。
记住这条故事,后面所有名词都是在给它命名。若排障时只盯着 Collection 名,而忽略 Channel / Segment,就会在「数据在不在」「索引好了没」「该问哪台节点」上反复迷路。
二、Collection 与 Partition:逻辑容器
Collection
是用户可见的向量(及标量)表:有
schema(主键、向量字段、标量字段等),是 insert
/ search 的默认作用域。官方架构与数据流都以
Collection 为顶层逻辑对象。
Partition 是 Collection 内的逻辑分区,用于按业务键隔离数据(例如按租户、按时间桶)。分区影响数据落在哪些 channel / segment 上的组织方式,但 不改变 Growing / Sealed 的段语义——后文状态机对每个 segment 成立。
flowchart TB
coll["Collection"]
p0["Partition A"]
p1["Partition B"]
s0["Segments ..."]
s1["Segments ..."]
coll --> p0 --> s0
coll --> p1 --> s1
本系列不把 schema 演进、动态字段的每一种 API 写成手册;需要时以官方 User Guide 为准。内核视角记住一点即可:检索与写入的调度单元落到 Channel 与 Segment,而不是「整个 Collection 一个大数组」。
三、Channel:shard 与 Streaming Node 的绑定
官方 Data Processing 写明:
- Collection 可配置多个 shard;每个 shard 对应一个 虚拟通道(vchannel)。
- 每个 vchannel 映射到一个 物理通道(pchannel)。
- 每个 pchannel 绑定到特定 Streaming Node。
Proxy 在写入时按 shard 路由规则拆包,把同一 vchannel 的数据送到对应 Streaming Node。于是:
- 水平扩展写入:增加 shard / Streaming Node,而不是把所有 insert 打进单日志。
- 顺序与一致性作用域:Streaming Service 文档强调,Message 属于某个 VChannel,并在该 channel 内维持操作顺序不变量(例如同一 channel 上 Insert 相对 DropCollection 的先后)。
flowchart LR
coll["Collection"]
v1["vchannel shard0"]
v2["vchannel shard1"]
p1["pchannel"]
p2["pchannel"]
sn1["Streaming Node A"]
sn2["Streaming Node B"]
coll --> v1
coll --> v2
v1 --> p1 --> sn1
v2 --> p2 --> sn2
与第 5 篇的衔接:WAL 不是「全集群一个文件」,而是 多个可独立扩展的日志;任一时刻,一个 WAL 组件只允许在一个 Streaming Node 上服务(fencing + Streaming Coordinator 保证)。本文先记住绑定关系,WAL 形态留给第 5 篇。
四、Segment:Growing 与 Sealed
Streaming Node 把已提交的 WAL 条目切成离散的 Segment。官方明确定义两类:
| 类型 | 定义(官方 Data Processing) | 可变性 | 谁主要服务查询 |
|---|---|---|---|
| Growing | 尚未持久化到对象存储的数据 | 可变、持续追加 | Streaming Node 本地维护并参与实时查询 |
| Sealed | 已全部持久化到对象存储 | 不可变 | Query Node 从对象存储加载后查询 |
这是整条系列最重要的二分:
- 实时性依赖 Growing:WAL 提交后即可被 Growing 路径搜到(Architecture 插入流)。
- 大规模历史检索与向量索引依赖 Sealed:索引构建在 Data Node 上对 Sealed 进行,输入输出都走对象存储(Data Processing · Index building)。
4.1 Flush:Growing → Sealed
Growing 变为 Sealed 的过程叫 flush。官方描述触发时机:Streaming Node 在该 segment 上 已摄入并写完所有可用 WAL 条目、底层 WAL 无更多 pending 记录 时,将该 segment finalize,使之面向只读优化。
Streaming Service 还提到 WAL 组件按策略管理 segment 生命周期,例如内存条件、segment 大小、空闲时间等——具体阈值是运维参数,本篇只保留 策略驱动的生命周期,不伪造默认阈值数字。
4.2 Handoff:历史数据交给 Query Node
当 Growing 被 flush 成 Sealed,或 Data Node 完成 compaction 后,Coordinator 发起 handoff:把该段从「流式侧持有的增长数据」转为「历史数据」,并在 Query Node 之间分配 Sealed,平衡内存、CPU 与 segment 数量,释放冗余副本(Data Processing · Handoff)。
stateDiagram-v2
[*] --> Growing: WAL commit append
Growing --> Sealed: flush
Sealed --> Indexed: DataNode index build
Sealed --> Compacted: DataNode compaction
Indexed --> Serving: handoff to QueryNode
Compacted --> Serving: handoff to QueryNode
「Indexed」在此是叙述辅助态:官方强调 每个 segment 可有自己的索引,建索由 Data Node 完成;Query Node 加载新建索引并替换对应的增长数据视图(Architecture 插入流第 7 步)。排障时「有数据但召回差」经常落在 Sealed 已可见但索引未完成 / 未 handoff——第 10、17 篇展开。
4.2.1
源码落点:Growing 状态在元数据里(milvus
v2.6.21)
DataCoord 侧用 datapb.SegmentInfo
包装段元数据;构造 Growing 段时显式按
commonpb.SegmentState_Growing 初始化分配与
flush 时钟:
// milvus-io/milvus v2.6.21
// internal/datacoord/segment_info.go
func NewSegmentInfo(info *datapb.SegmentInfo) *SegmentInfo {
s := &SegmentInfo{SegmentInfo: info}
if s.GetState() == commonpb.SegmentState_Growing {
s.allocations = make([]*Allocation, 0, 16)
s.lastFlushTime = time.Now().Add(-1 * paramtable.Get().DataCoordCfg.
SegmentFlushInterval.GetAsDuration(time.Second))
s.lastWrittenTime = getZeroTime()
}
s.deltaRowcount.Store(-1)
return s
}这说明「Growing / Sealed」不是文档修辞,而是
元数据状态机上的枚举;flush 后状态迁到
flushed/sealed 一类只读态,再进入建索与
handoff(具体枚举名以该 tag 的
commonpb.SegmentState 为准)。二次索引
coll2Segments / channel2Segments
也在同文件,对应「调度单元是 Channel + Segment」。
4.3 flush 与 handoff 时序(教学)
sequenceDiagram
participant SN as StreamingNode
participant OS as ObjectStore
participant DN as DataNode
participant Coord as Coordinator
participant QN as QueryNode
SN->>SN: Growing append
SN->>OS: flush Sealed binlog
Coord->>DN: schedule index / compact
DN->>OS: write index files
Coord->>QN: handoff
QN->>OS: load Sealed + index
五、一次写入在模型上的落点
把官方插入流压成模型层步骤(不含 SDK 细节):
- 客户端
insert→ Proxy 校验。 - 按 shard 规则拆到若干 vchannel 数据包。
- 对应 Streaming Node 赋 TSO,做一致性检查,写入 WAL;提交后不丢(崩溃可 replay)。
- 异步切分 / 追加到 Growing Segment;此时可被增量查询路径看到。
- 策略满足 → flush → Sealed 落对象存储。
- Data Node 建索引 / compaction → Coordinator handoff → Query Node 加载。
TSO 提供操作全序;同一 VChannel 内的 Message 顺序决定该 shard 上写操作的可见顺序(Streaming Service)。一致性级别(Strong / Bounded / …)如何裁剪可见集合,见第 12 篇;本篇只要求承认:顺序首先是 channel 局部的。
六、一次查询在模型上的落点
官方查询流:
- Collection 拆成多 segment;Streaming Node 持 Growing,Query Node 持 Sealed。
- Proxy 向相关 shard 的 Streaming Node 并发请求。
- 每个 Streaming Node:搜本地 Growing;联系 Query Node 取历史(Sealed)结果;聚合成 shard 结果。
- Proxy 合并各 shard 结果返回。
因此「Collection 上的一次 search」在引擎里是 多 Growing + 多 Sealed 的候选归并,不是单次 FAISS 调用。Segcore / Knowhere 在段内做什么,见第 7–8 篇;跨段 reduce 见第 9 篇。
flowchart TB
proxy["Proxy"]
sn["Streaming Node Delegator"]
grow["Growing search"]
qn["Query Node Sealed search"]
proxy --> sn
sn --> grow
sn --> qn
sn -->|"shard reduce"| proxy
七、教学数值:可见性与索引质量不是同一时刻
下面用 教学示意(非实测)帮助记忆时间窗。设 \(t=0\) 时 insert 的 WAL 提交成功:
| 时刻(示意) | 客户端可感知 | 引擎内部 |
|---|---|---|
| \(t=0\) | insert 返回成功 | Message 在 WAL;Growing 可搜 |
| \(t=t_f\) | 仍可搜到(Growing 或刚 Sealed) | flush 完成,对象存储有 binlog |
| \(t=t_i\) | 召回形态可能变化 | Data Node 写出 index files |
| \(t=t_h\) | 历史路径由 Query Node 服务 | handoff 完成,增长视图被替换 |
\(t_f\)、\(t_i\)、\(t_h\) 的相对顺序与间隔依赖策略与队列深度(第 5、10、17 篇)。一致性级别(第 12 篇)裁剪的是「读请求保证看到哪个时间戳之前的写」,不等于「索引已经建好」。
八、常见误解
- 「一个 Collection 对应一张全局 HNSW」。生产是 每 Sealed 一份索引;查询必须归并多段候选。
- 「insert 返回成功 = 对象存储上已有 Sealed + 索引」。返回成功通常意味着 WAL 路径可提交;flush / 建索 / handoff 是后续异步阶段。
- 「Partition 改变了 Growing/Sealed 语义」。Partition 是逻辑隔离;段状态机对每个 segment 相同。
- 「shard 数等于 Query Node 数」。shard(vchannel)绑定的是 Streaming / WAL 所有权;Query Node 按 segment 负载分配 Sealed,二者相关但不相等。
九、学术谱系与工程间隙
9.1 谱系:动态向量数据 vs 静态 ANN 基准
| 阶段 | 工作 / 系统 | 假设 |
|---|---|---|
| ANN 基准 | ann-benchmarks、Big-ANN 等 | 多以 静态集 建索引再查询 |
| 向量数据管理系统 | Wang et al., SIGMOD 2021 | 强调 动态更新 与查询并存 |
| Milvus 2.6.x | Growing / Sealed + handoff | 用段状态机落实「可变增量 + 不可变历史」 |
Growing/Sealed 二分与列存的「可变缓冲 + 不可变 part」、LSM 的「MemTable + SST」是 同一类工程分形:前台写进可变结构,后台固化为只读、可建重索引的单元。差异在于固化单元上挂的是 高维 ANN 索引,而不是 B-Tree 页或列块(对照 columnar-engine、rocksdb 的 part/SST 直觉,勿混实现)。
9.2 工程间隙
- 论文或博客常画「一个 Collection 一个 HNSW」;生产是 每 Sealed segment 一份索引,查询必须归并。
- flush / handoff 延迟窗口内,数据可能「WAL 已提交、索引未就绪」——可见性与召回质量不是同一时刻切换。
- 对象存储上的 Sealed 不可变,但 compaction 会用新 Sealed 替换旧布局;Query Node 视图由 Coordinator 调度,不是「文件一旦写出永不迁移」。
9.3 开放问题
- 最优 segment 大小:太大则 flush/建索毛刺明显,太小则 segment 数爆炸、归并与元数据成本上升——依赖负载,缺普遍解析解。
- 多向量字段 / 多索引 时,segment 内多份索引的生命周期如何与单次 handoff 对齐(第 10、18 篇)。
- 与湖表 snapshot 的跨系统对齐:同一 embedding 版本在 Iceberg 与 Milvus 之间如何证明一致(lakehouse/21 边界)。
十、小结
三句话小结
- Collection / Partition 是逻辑容器;真正调度单元是 Channel + Segment。
- Growing 服务实时可变数据;Sealed 服务不可变历史与重索引。
- flush → index/compaction → handoff 把数据从流式侧迁到 Query Node 历史路径。
本机实验边界(2026-07-25)
写作机为 2 核 / ~2 GiB、无
Docker,无法起 Milvus
standalone。可复现脚本已提供:reproduce/exp_03_segment_lifecycle.sh。在本机执行时打印
BOUNDARY: docker not found 并以退出码 2
结束——未观测到真实 Growing/Sealed
状态列表,正文也不伪造。在 ≥4 GiB + Docker +
MILVUS_URI 的环境按脚本注释复现即可。
下一篇可先读写路径深化:Proxy 与 Coordinator,或 Streaming Node 与 Woodpecker WAL。
参考资料
- Milvus Documentation v2.6.x, Data Processing(insertion、Growing/Sealed、flush、index building、query、handoff)。
- Milvus Documentation v2.6.x, Architecture Overview(四层、insert/search 示例流)。
- Milvus Documentation v2.6.x, Streaming Service(VChannel、Message、WAL 组件与 segment 生命周期策略)。
- milvus-io/milvus
v2.6.21:
internal/datacoord/segment_info.go(NewSegmentInfo、SegmentState_Growing、channel/collection 二次索引)。 - Wang et al., Milvus: A Purpose-Built Vector Data Management System, SIGMOD 2021(动态向量数据管理系统动机)。
- 复现脚本:reproduce/exp_03_segment_lifecycle.sh(需 Docker Milvus;本写作机未跑通)。
- 第 1 篇 全景、系列 index。
返回 系列目录 | 上一篇:ANN 工程接口 | 下一篇:Proxy 与 Coordinator
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【向量检索引擎】Query Node 与 Segcore:段级 search 如何执行
按 Milvus 2.6.x Data Processing 与 Architecture 拆解 Query Node 对 Sealed 的加载与段级检索,说明 Streaming Node 上 Growing 路径与 Query Delegator 如何拼成一次 search,用最小故事与常见误解钉住 Segcore 与 Knowhere 的层次边界。
【向量检索引擎】Data Node:compaction 与 index build
说明 Milvus 2.6.x 中 Data Node 作为离线 Worker 如何承接 Coordinator 下发的建索引与 compaction,输入输出如何进出对象存储;用最小故事、常见误解与队列积压图说明索引堆积如何拖垮查询新鲜度与资源争用。
【向量检索引擎】对象存储上的 Segment 布局:快照、索引与寻址代价
以 flush 后去对象存储里查看文件为线索,拆解 Milvus 2.6.x 的对象路径结构(insert_log/delta_log/stats_log/index_files)、V1 按字段拆分与 V2 按段整合 Parquet 的寻址代价差异,并给出官方文档中可核对的 API 调用量级引用。
【向量检索引擎】副本、负载与故障恢复:读缓存式副本与 WAL 单所有者
按官方 In-Memory Replica 与 Streaming Service 文档拆解副本组、shard leader、Proxy 缓存 failover 与 WAL Wait for Ready;用多副本拓扑图与迁移时序图说明 Milvus 的读副本更接近只读缓存池而非共识复制,并与 Chain Replication、PacificA 对照。