Milvus 写路径时序图版

目标:把 Milvus 中一次 insert / delete 请求如何经过 Proxy、WAL/Streaming、DataNode、DataCoord、对象存储最终变成可查询 segment 的过程讲清楚。


1. 先给一句话版本

Milvus 的写路径不是“客户端发请求,数据库直接写索引”。它更像:

客户端 mutation → Proxy 规范化 → WAL/Streaming 记录事实 → DataNode 组织 growing segment → flush 生成 binlog/deltalog/statslog → DataCoord 更新元数据 → 后台索引构建 → QueryNode 加载提供查询。


2. 主要参与者

  • Client / SDK:发起 insert / delete / upsert
  • Proxy:请求校验、schema 绑定、RowID/TS 分配、路由
  • StreamingClient / MQ client:统一写入 WAL 或消息层
  • StreamingNode / MQ backend:承载 PChannel/Topic 上的 mutation log
  • DataNode:消费 mutation,组织 growing segment,触发 flush
  • DataCoord:维护 segment 生命周期、flush 状态和后续任务
  • Object Storage:存放 binlog、deltalog、statslog、后续 index 文件
  • Index builder / background task:异步为 flushed segment 建索引
  • QueryNodeV2:后续加载 flushed/indexed segments,同时保留 growing 数据实时可见

3. 插入请求的详细时序

Client
  ↓ insert()
Proxy
  ↓ 校验 collection/schema,补 rowID / timestamp
StreamingClient.Append / msgstream produce

WAL backend (PChannel / topic)
  ↓ consume
DataNode

WriteBuffer / growing segment
  ↓ 达到 flush/seal 条件
sync manager / storage codec

Object Storage (binlog / statslog / deltalog)

DataCoord 更新 segment metadata

异步 index build / compaction / query load
QueryNodeV2

4. Proxy 阶段:用户请求被变成内部 mutation

4.1 对应源码

  • internal/proxy/task_insert.go
  • internal/proxy/task_insert_streaming.go

4.2 Proxy 做的不是简单转发

task_insert.goPreExecute() 可以看出来,Proxy 至少会做这些事:

  • 校验 collection name
  • 检查 request size 上限
  • 从 globalMetaCache 获取 collection ID 和 schema
  • 校验 schema timestamp / version
  • 自动分配 RowID
  • 为每一行填充 timestamp
  • 处理 dynamic field / namespace / struct flatten 等逻辑

4.3 这一层的意义

这一步的本质是:

把客户端层的“表意请求”变成系统层的“可执行 mutation”。

它和传统数据库里的 parser/binder 虽不完全一样,但角色有点像“请求规范化 + schema binding + internal record assembly”。


5. WAL / Streaming 阶段:mutation 先进入事实源

5.1 当前架构主张

docs/agent_guides/streaming-system/streaming-system.md 中明确写到:

WAL is the single source of truth for all data mutations and metadata changes.

这说明写路径中最关键的设计不是“直接写存储”,而是:

先把 mutation 可靠落到 WAL 语义中。

5.2 channel 模型

当前 streaming 体系里:

  • PChannel:物理通道
  • VChannel:逻辑通道
  • CChannel:控制通道

对于 DML:

  • Client → Proxy → StreamingClient.Append → StreamingNode → WAL backend

5.3 为什么这一步重要

这意味着:

  • mutation 顺序和恢复基础来自 WAL
  • 后续的 growing segment、binlog、index 都可以看作对 WAL 的物化结果或派生状态
  • 写入 durability 和查询 freshness 被解耦

6. DataNode 阶段:日志变成 growing segment

6.1 DataNode 的角色

DataNode 是写路径中真正消费 mutation 并做数据组织的地方。它不是简单搬运工,而是要:

  • 消费 insert/delete
  • 组织 growing segment
  • 管理 per-channel buffer
  • 判断何时 seal / flush
  • 通过 sync manager 写入对象存储

6.2 核心抽象:WriteBuffer

对应文件:

  • internal/flushcommon/writebuffer/write_buffer.go

这里定义的 WriteBuffer 非常说明问题:

  • CreateNewGrowingSegment
  • BufferData
  • SealSegments
  • GetCheckpoint
  • EvictBuffer
  • SetFlushTimestamp
  • Close

这相当于一个:

按 channel 分片的 mutable segment buffer manager。

6.3 你可以怎么理解它

如果借用存储引擎语言:

  • WAL 是 mutation log
  • WriteBuffer 有点像带 segment 语义的 memtable 层
  • flush 是把内存态组织成对象存储中的持久化 segment 日志文件

7. flush 阶段:从内存段到持久化对象

7.1 持久化产物

Milvus 写路径最终会产生:

  • binlog:插入数据相关
  • deltalog:删除相关
  • statslog:统计信息

这些文件最终进入对象存储(MinIO/S3)。

7.2 对应实现与配置

  • 配置:configs/milvus.yamlminio:
  • Go 侧:internal/storage/
  • C++ 侧:internal/core/src/storage/

7.3 为什么是对象存储而不是本地盘主存储

因为 Milvus 的目标是:

  • 计算存储分离
  • segment 可跨节点加载
  • 故障恢复时不依赖单节点本地盘
  • 索引和数据文件都能被统一远端访问

8. DataCoord 阶段:segment 元数据推进

8.1 DataCoord 不是数据写入执行者,但它决定 segment 生命周期

DataCoord 需要跟踪:

  • 哪些 segment 仍是 growing
  • 哪些已经 sealed
  • 哪些已经 flush 完成
  • 哪些可以进入 index build
  • 哪些需要 compaction

8.2 这一步的本质

DataNode 负责“把 mutation 变成数据”, DataCoord 负责“把数据纳入全局 segment 生命周期”。

这两者分开,才有可能做到:

  • 写入并发扩展
  • 后台任务解耦
  • 统一的 metadata 驱动调度

9. 索引构建不是写路径同步动作

9.1 索引通常在 flush 之后异步构建

Milvus 不会在插入请求返回前把 segment 索引全部建好。典型路径是:

  1. mutation 进入 WAL
  2. growing segment 实时可查
  3. segment seal + flush
  4. 后台 index build
  5. 查询面优先使用 indexed sealed segment

9.2 为什么这样设计

否则写路径会被大规模索引构建拖垮。

Milvus 用这种方式平衡:

  • freshness:靠 growing path
  • efficiency:靠 sealed/indexed path

10. 删除请求在写路径中的位置

delete 不会立刻把所有历史 segment 物理重写。它通常走:

  • delete mutation 进入 WAL
  • DataNode / QueryNode 在逻辑上维护 delete 信息
  • 后台通过 L0 delete compaction、mix compaction 等逐渐清理物理布局

这也是为什么 compaction 在 Milvus 里不是“附属功能”,而是写路径长期正确性的组成部分。


11. 写路径的时间语义

Milvus 的写路径里有一个很重要的点:timestamp 不是可有可无的字段,而是系统可见性和 snapshot 语义的基础。

Proxy 在写入预处理阶段填充 timestamp,后续:

  • WAL 记录 mutation 顺序
  • segment/buffer/checkpoint 推进
  • QueryNode 按一致性级别和 guarantee timestamp 决定可见性

所以可以把写路径理解成:

带有时间标签的 mutation 流被持续物化成 segment。


12. 写路径里最值得精读的源码顺序

路线 A:快速主线

  1. internal/proxy/task_insert.go
  2. docs/agent_guides/streaming-system/streaming-system.md
  3. internal/flushcommon/writebuffer/write_buffer.go
  4. internal/datanode/
  5. internal/storage/
  6. internal/datacoord/

路线 B:想看 streaming 更细

  1. internal/streamingcoord/
  2. internal/streamingnode/
  3. pkg/streaming/
  4. pkg/mq/msgstream/

13. 用一句话压缩写路径理解

Milvus 的写路径本质是:把带时间语义的 mutation 先记录到 WAL,再由 DataNode 增量物化为 growing segment 和持久化日志文件,最后由 DataCoord 把这些物理结果纳入全局 segment 生命周期。