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
QueryNodeV24. Proxy 阶段:用户请求被变成内部 mutation
4.1 对应源码
internal/proxy/task_insert.gointernal/proxy/task_insert_streaming.go
4.2 Proxy 做的不是简单转发
从 task_insert.go 的 PreExecute() 可以看出来,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 非常说明问题:
CreateNewGrowingSegmentBufferDataSealSegmentsGetCheckpointEvictBufferSetFlushTimestampClose
这相当于一个:
按 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.yaml的minio: - 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 索引全部建好。典型路径是:
- mutation 进入 WAL
- growing segment 实时可查
- segment seal + flush
- 后台 index build
- 查询面优先使用 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:快速主线
internal/proxy/task_insert.godocs/agent_guides/streaming-system/streaming-system.mdinternal/flushcommon/writebuffer/write_buffer.gointernal/datanode/internal/storage/internal/datacoord/
路线 B:想看 streaming 更细
internal/streamingcoord/internal/streamingnode/pkg/streaming/pkg/mq/msgstream/
13. 用一句话压缩写路径理解
Milvus 的写路径本质是:把带时间语义的 mutation 先记录到 WAL,再由 DataNode 增量物化为 growing segment 和持久化日志文件,最后由 DataCoord 把这些物理结果纳入全局 segment 生命周期。