首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Milvus 3.0 流式架构:16 个 PChannel + 5 层拦截器,自建 WAL 比 Kafka 快 5.8 倍

Milvus 3.0 流式架构:16 个 PChannel + 5 层拦截器,自建 WAL 比 Kafka 快 5.8 倍

原创
作者头像
术哥
发布2026-07-12 12:23:53
发布2026-07-12 12:23:53
610
举报
文章被收录于专栏:运维有术运维有术

🚩 2026 年「术哥无界」系列实战文档 X 篇原创计划 第 164 篇,Milvus 最佳实战「2026」系列第 19

大家好,欢迎来到 术哥无界 | ShugeX | 运维有术

我是术哥,一名专注于 AI 编程、AI 智能体、Agent Skills、MCP、云原生、AIOps、Milvus 向量数据库的技术实践者与开源布道者

Talk is cheap, let's explore。无界探索,有术而行。

封面图
封面图

如果你在 2.x 时代用过 Milvus 大规模集群,可能踩过这种坑:在高并发写入时插一条数据,有时能看到,有时看不到。原因不复杂——DML 走 Proxy→msgstream→DataNode 这条链路,DDL 走 Proxy→etcd→RootCoord 那条链路,两条链路物理隔离,谁也不等谁。一个 CreateCollection 还没在 etcd 里落地,另一个 Proxy 已经把 Insert 写进 msgstream 了。

Milvus 3.0 做的事情就是把这两条分裂的路径合成一条——所有变更都写进同一个 WAL(Write-Ahead Log,预写日志),由 WAL 自己维护全局可见性。StreamingCoord 负责分配 PChannel(物理通道),StreamingNode 负责拦截器链(一种链式责任链模式,每层只关注单一职责)和 WAL 读写,StreamingClient 嵌在 Proxy 和 QueryNode 里。整条链路就这么三个人,路径只剩一条。

这篇文章基于 Milvus 3.0 源码(internal/streamingnode/server/wal/interceptors/internal/streamingcoord/server/pkg/streaming/docs/agent_guides/streaming-system/)和 Milvus 官方博客 we-replaced-kafka-pulsar-with-a-woodpecker-for-milvus.md,讲清楚 3.0 的流式架构是怎么设计的、为什么这样设计、以及 2.x 的哪些坑被解决了。

1. WAL-first 架构变革:从双轨到单轨

Milvus 2.x 的写入路径是个多组件拼装的玩意儿。一条 Insert 消息至少要经过 Proxy(封装消息)→ Pulsar/Kafka(持久化和分发)→ DataNode(消费并落盘)三道关。每道关都有自己的重试逻辑、自己的错误码、自己的重连策略。

结果就是故障定位困难,跨组件一致性没人能拍胸脯保证。

更麻烦的是 DDL。CreateCollection 不走 Pulsar/Kafka,走 etcd + RootCoord。两条物理路径怎么同步?答案是靠 Proxy 层的先查 etcd 再写 msgstream这种应用层 hack——能跑通,但不优雅,也不严格。

3.0 的破局点就一句话:streaming-system.md:7 那行定义:

Milvus uses a log-structured WAL (Write-Ahead Log) as its single source of truth for all data mutations and metadata changes.

所有变更——Insert/Delete/Flush/CreateCollection/CreateIndex/RBAC——都走同一条 WAL 写入路径。这意味着 3.0 的 DDL 不再走原来的 etcd 通道,所有元数据变更也通过 WAL 落地(etcd 仍承担部分元数据存储与 Woodpecker meta 角色)。

架构全景图:2.x 多组件拼装 vs 3.0 统一 WAL
架构全景图:2.x 多组件拼装 vs 3.0 统一 WAL

这种统一带来的收益是结构性的。WAL 天然带顺序,TimeTick(时间戳序号,Milvus 用它做 MVCC 即多版本并发控制)天然能挂在 WAL 消息上。跨通道一致性不再是应用层要操心的事。

底层有了顺序,应用层就可以从竞态防护的泥潭里抽身出来。

三个核心组件各司其职:

  • StreamingCoord:单例,运行在 RootCoord/MixCoord 进程内,负责 PChannel 分配和跨通道原子广播
  • StreamingNode:多实例,独立进程,gRPC 端口 22222,负责 WAL 读写和拦截器链执行
  • StreamingClient:进程内库,嵌入 Proxy 和 QueryNode,通过 streaming.WAL() 访问

源码规模上,internal/streamingcoord/server/ 约 60 个 Go 文件,internal/streamingnode/server/wal/interceptors/ 13 个子目录(五层拦截器 + 工具),pkg/streaming/ 约 100 个 Go 文件。这不是一次小重构,是把写入路径整条重写了。

四种数据流被文档明确区分(streaming-system.md:17-23):

  1. DML:Client → Proxy → StreamingClient.Append → StreamingNode → WAL Backend
  2. DDL/DCL:Client → Proxy → StreamingClient.Broadcast → StreamingCoord.Broadcaster → 多 PChannel 原子广播 → WAL Backend
  3. WALInternal:TimeTick/Flush/CreateSegment/Txn 等由 WAL 系统自己生成,不来自外部客户端
  4. Replicated:Primary WAL → CDC ChannelReplicator → Secondary Proxy → Secondary WAL

前两种是用户数据,后两种是系统自生和跨集群——这种分类暗示了一件事:3.0 把很多原本散落在 Proxy/DataNode 里的逻辑下沉到了 WAL 内部

比如 TimeTick(基于 2.x 通用设计推断)。2.x 由 Proxy 周期性写进 msgstream;3.0 直接由 StreamingNode 的 timetick 拦截器生成,跳过 WAL 后端持久化(non-persisted TimeTick)。

官方博客解释过这个设计动机——Kafka/Pulsar 不支持 ephemeral(临时性)信号绕过持久化,Milvus 自建 Woodpecker 才能做到。

2. 三层通道模型:物理并行、逻辑隔离、全局排序

通道设计是理解 3.0 流式架构的钥匙。3.0 定义了三种通道,对应三种不同维度的问题。

PChannel:物理并行度的基本单元

PChannel(Physical Channel,物理通道)是 WAL 后端 topic 的 1:1 映射。命名格式是 <prefix>_<index>,比如 by-dev-rootcoord-dml_0

关键参数(configs/milvus.yamlrootCoord.dmlChannelNum):默认 16 个。启动时固定,运行时不增不减(除非 AlterReplicateConfig 触发拓扑变更)。

PChannel 由 StreamingCoord 的 Balancer 分配给具体的 StreamingNode。每次分配会让 term(任期号)自增——这是 fencing(写入隔离)旧写入者的关键。新的写入者持有新 term,旧 term 的写入会被 WAL 后端拒绝(ErrFenced)。

VChannel:集合级隔离

VChannel(Virtual Channel,虚拟通道)是集合级分片的基本单位。命名格式是 <pchannel_name>_<collectionID>v<shardIndex>,比如 by-dev-rootcoord-dml_0_12345v0 表示 PChannel by-dev-rootcoord-dml_0、集合 12345、shard 0。

VChannel 的核心特性是:不同集合的 VChannel 可以共享同一个 PChannel——这是多路复用的关键。一个 PChannel 可以容纳成百上千个 VChannel 的消息,靠 VChannel 头做路由区分。

CChannel:全局排序的锚点

CChannel(Control Channel,控制通道)是个特殊存在——命名是 <pchannel_name>_vcchan永久绑定到某个固定的 PChannel(集群初始化时分配,永不改变)。每个 PChannel 都对应一个 CChannel。

CChannel 的作用是提供集群级单点排序——所有跨 PChannel 的原子广播都通过 CChannel 的 TimeTick 来排序执行。这一点对理解 Broadcaster 机制至关重要,后面会专门讲。

三层通道模型:PChannel → VChannel → CChannel 的嵌套关系
三层通道模型:PChannel → VChannel → CChannel 的嵌套关系

设计动机:解决 2.x 跨通道一致性问题

为什么 3.0 要引入三层通道?2.x 只有一种通道(DML 走 msgstream,DDL 走 etcd),物理上是隔离的——隔离就意味着没法做跨通道的一致性保证

3.0 的解法是把并行度隔离性全局排序拆成三个独立维度:

  • 物理通道(PChannel)提供并行度——16 个 PChannel 就能让 16 路写入并行
  • 虚拟通道(VChannel)提供集合级隔离——不同集合互不影响,但能共享底层物理资源
  • 控制通道(CChannel)提供全局排序——所有跨 PChannel 的操作都能找到一个统一的时序锚点

官方博客还透露了一个背景:2.x 时代 Milvus 搞过 VShard(Virtual Shard)来解决 Kafka topic 数量限制的问题——Kafka 一个集群能支持的 topic 数是有限的,Milvus 集合多了 topic 不够用。

3.0 的 VChannel 在概念上是 VShard 的继任者,但通过 WAL 内生支持,不再受外部 MQ 限制。

PChannel 状态机在 pkg/proto/streaming.proto:46-56 里定义:UNKNOWN → UNINITIALIZED → ASSIGNING → ASSIGNED → UNAVAILABLE → ASSIGNING → ...。ASSIGNED 是正常状态,UNAVAILABLE 用于标记故障节点,触发再平衡。

3. 五层拦截器链:洋葱模型的设计哲学

StreamingNode 收到 Append 请求后,不会直接写 WAL——它会先把消息丢进一条拦截器链。这条链有五层,每层只关心自己的语义。

注册顺序(internal/streamingnode/server/walmanager/manager_impl.go:29-35):

代码语言:go
复制
[]interceptors.InterceptorBuilder{
    redo.NewInterceptorBuilder(),      // 1. 重试
    lock.NewInterceptorBuilder(),      // 2. 锁
    replicate.NewInterceptorBuilder(), // 3. CDC 复制
    timetick.NewInterceptorBuilder(),  // 4. 时间戳分配
    shard.NewInterceptorBuilder(),     // 5. Segment 分配
}

顺序不是随意的——这是经典洋葱模型(越外层越先执行、越内层越后执行)。每层只做一件事,下游依赖上游的处理结果。

redo 层:处理可重试错误

最外层是 redo。遇到 ErrRedo(TimeTick 过旧、Segment 未就绪、Fenced)就重试——在进入锁之前先尝试恢复,避免下游做无意义的工作。对 Insert 消息,会等待 Growing Segment 分配就绪。

lock 层:PChannel 级和 VChannel 级双层锁

lock 拦截器只有 65 行,但设计很精炼。它维护两个锁:

  • glock(PChannel 级 sync.RWMutex)
  • vchannelLocker(VChannel 级 KeyLock)

锁规则分三种情况:

  • ExclusiveRequired 消息 + PChannel 级:调用 glock.Lock() + txnManager.FailTxnAtVChannel("")——中断该通道所有事务
  • ExclusiveRequired 消息 + VChannel 级:只锁该 VChannel,中断该 VChannel 的事务
  • DML 消息:共享锁(glock.RLock + vchannelLocker.RLock)

关键细节在注释里:ManualFlush 这种排他消息在释放锁前调用 FailTxnAtVChannel()原因是 ManualFlush 之后还提交事务会破坏 WAL 消息顺序——commit 之后的 insert 会出现在 Flush 之后的位置

这一行注释把为什么需要排他锁的来龙去脉解释清楚了。

replicate 层:CDC 复制分支

replicate 拦截器做两件事:

  1. AlterReplicateConfig 消息:调用 SwitchReplicateMode() 切换复制模式;如果带 ForcePromote=true,追加成功后调用 txnManager.RollbackAllInFlightTransactions() 回滚所有进行中的事务
  2. 其他消息:调用 BeginReplicateMessage();如果返回 ErrNotHandledByReplicateManager,直接 append 不处理

角色控制是显式的:PRIMARY 角色拒绝带 replicate header 的消息,SECONDARY 角色拒绝不带 replicate header 的消息——但 TimeTick/CreateSegment/Flush 等 WAL 自控消息绕过拦截器(因为它们本来就不应该被复制)。

timetick 层:最复杂的一层

timetick 拦截器是五层里代码量较多的一个(222 行),也是 3.0 一致性模型的核心。

它要解决两个问题:分配 TimeTick(从 TSO 即 TimeStamp Oracle 时间戳分配器拿号)和维护可见性水位(LastConfirmedMessageID)。

TimeTick 有三个状态(timetick_and_txn.md):

  • Allocated:从 TSO 分配出来
  • Confirmed:Acker.Ack() 调用后推进
  • Synced:TimeTickSyncInspector 周期性把 confirmed 水位构造成 TimeTick 消息写回 WAL

最后一个状态有个隐藏的优化:当批内无真实消息时生成 non-persisted TimeTick,直接跳过 WAL 后端持久化。这对降低 TimeTick 的写入放大(一条 TimeTick 对应一条消息但没有实际数据)非常关键。

消费端的乱序恢复用 ReOrderByTimeTickBuffer 实现——维护一个最小堆,按 TimeTick 顺序 flush,无论物理写入顺序如何,消费者看到的消息都严格按 TimeTick 排序

LastConfirmedMessageID 推进必须同时满足两个条件

  1. 无并发写未完成(EndTimestamp < currentConfirmedTimeTick
  2. 事务已解决(committed、rolled back 或 expired)

这两个条件合起来就是 3.0 一致性模型的边界:从 LastConfirmedMessageID 读取保证所有后续消息的 TimeTick 严格更大

shard 层:Segment 分配和 13 种 Seal 策略

shard 拦截器是五层里文件最长的(383 行)。它维护一个 ops 表,把消息类型映射到处理函数:

代码语言:go
复制
ops = map[message.MessageType]interceptors.AppendInterceptorCall{
    CreateCollection:   handleCreateCollection,
    DropCollection:     handleDropCollection,
    Insert:             handleInsertMessage,
    Delete:             handleDeleteMessage,
    CreateSegment:      handleCreateSegment,
    Flush:              handleFlushSegment,
    ...
}

Insert 消息的处理逻辑是:检查 schema 版本、物化 function field、调用 shardManager.AssignSegment() 分配 segment、附加 SegmentAssignment 到消息头。

Seal(封口)策略有 13 种(shard/policy/seal_policy.go),对应不同的 segment 封口触发条件:

代码语言:markdown
复制
partition_not_found, partition_removed, collection_removed,
recover, fenced, force, capacity, binlog_number,
lifetime, idle, growing_bytes_hwm, node_memory, blocking_l0
五层拦截器链:redo → lock → replicate → timetick → shard
五层拦截器链:redo → lock → replicate → timetick → shard

设计哲学总结

为什么是这五层、这个顺序? 这是源码层面能看到的判断,但官方没有公开 RFC 解释设计动机(research-02-supplement.md 的信息缺口之一)。根据现有资料推断(标注为推断而非官方明示):

  1. redo 最外层——先处理重试,避免下游做无意义的工作
  2. lock 第二——锁是排他性的,要在时序分配前确定
  3. replicate 第三——CDC 复制需要看到原始消息(在分配 TimeTick 前确定复制路径)
  4. timetick 第四——分配 TimeTick 是核心,但要在 replicate 之后(replicate 拦截器处理 AlterReplicateConfig 时不能分配 TimeTick)
  5. shard 最后——segment 分配依赖 TimeTick

每一层只关心自己的语义,下游依赖上游的处理结果——这就是洋葱模型的精髓。

4. Broadcaster:跨 PChannel 原子广播

三层通道模型解决了并行度和隔离性问题,但 DDL 操作(CreateCollection 之类)需要原子地写入所有 PChannel——不能写完一半挂掉,也不能在某些 PChannel 写成功但其他 PChannel 还没写时让数据可见

3.0 的解法是 Broadcaster + Resource Key + CChannel TimeTick 三件套。Broadcaster 是 StreamingCoord 的子模块,专门负责跨通道广播。

六步广播流程

源码(broadcast_task.go)和官方文档(coordination/broadcaster.md)描述了完整的六步流程:

  1. Lock:按排序顺序获取 ResourceKey 锁(自动添加 SharedCluster)
  2. Persist:分配 BroadcastID,持久化 PENDING 任务到 catalog
  3. Append:broadcastScheduler 分发到 worker,调用 AppendMessages() 写入所有目标 PChannel
  4. FastAck:除非设置 AckSyncUp,否则直接从 append 结果自我确认所有 VChannel
  5. AckCallback:CChannel ACK 后回调,按 CChannel TimeTick 顺序执行(conflicting ResourceKey 任务)
  6. Tombstone & GC:回调完成后任务转入 TOMBSTONE,tombstoneScheduler 清理

FastAck 是一个关键优化——DDL 不需要等待消费者 ACK,从 append 结果就能自我确认。这省掉了消费者 ACK 那段 RTT,DDL 延迟比走消费者确认链路短不少。

Resource Key:锁的粒度

广播的锁不是按 PChannel 加的,是按 Resource Key 加的。Resource Key 体系定义在 messages.proto:772-791

代码语言:protobuf
复制
enum ResourceDomain {
    ResourceDomainCollectionName = 2;
    ResourceDomainDBName = 3;
    ResourceDomainPrivilege = 4;
    ResourceDomainSnapshotName = 5;
    ResourceDomainCluster = 127;
}

构造函数分了三种:

  • NewSharedCollectionNameResourceKey(db, coll)——集合级共享锁
  • NewExclusiveClusterResourceKey()——集群级排他锁
  • NewSharedSnapshotNameResourceKey(collID, name)——快照级共享锁

同一个集合的操作共享一个 CollectionName key——DDL 获取排他锁,DML 获取共享锁。

这把跨通道原子广播集合级并发控制两件事统一起来了。

CChannel TimeTick 的作用

为什么 CChannel 能保证执行顺序?因为每个 PChannel 都有唯一一个 CChannel(绑定关系永不改变),CChannel 上的 TimeTick 序列就是该 PChannel 的全局时序锚点。

DDL 任务在所有目标 PChannel 写完后,会等 CChannel ACK——CChannel ACK 的顺序就是该 PChannel 的 TimeTick 顺序。多个 conflicting ResourceKey 的 DDL 任务就靠这个顺序串起来执行。

Broadcaster 六步流程:Lock → Persist → Append → FastAck → AckCallback → Tombstone
Broadcaster 六步流程:Lock → Persist → Append → FastAck → AckCallback → Tombstone

这套机制的设计代价是StreamingCoord 是单点——挂了 DDL 整体停摆。但 StreamingCoord 只做调度,持久化状态交给 WAL 后端和 etcd,所以单点压力被压到调度这一步。

5. Woodpecker:自建 WAL 的 3 个理由 + 性能基准

3.0 运维层面变化不少,其中最显眼的,是把 Kafka/Pulsar 替换成了 Milvus 自家的 Woodpecker。这不是技术炫技,是被 2.x 的运维负担逼出来的。

Milvus 官方博客 we-replaced-kafka-pulsar-with-a-woodpecker-for-milvus.md 明确说了三个驱动因素:

① Operational Complexity(运维复杂度)

External dependencies like Kafka or Pulsar demand dedicated machines with multiple nodes and careful resource management.

Kafka 还得带一套 ZooKeeper(KRaft 模式后部分缓解),Pulsar 离不开 BookKeeper——多出来的是部署节点、运维学习曲线、配置出错点。Milvus 团队的反馈是:用户运维 Kafka/Pulsar 出问题的频率,远高于 Milvus 本身的问题。

② Architectural Constraints(架构约束)

Message queues like Kafka have inherent limitations on the number of supported topics. We developed VShard as a workaround for topic sharing across components, but this solution—while effectively addressing scaling needs—introduced significant architectural complexity.

Kafka 一个集群能支持的 topic 数量是有限的(默认配置下几千个就到瓶颈)。Milvus 集合多了 topic 不够用——2.x 搞了 VShard(Virtual Shard)来复用 topic,但复用 topic 又引入了新的架构复杂度

换句话说,VShard 是在 Kafka topic 数量限制下逼出来的 workaround,不是优雅设计。

③ Resource Inefficiency(资源浪费)

Storage for ephemeral signals (like Milvus's Timetick), which don't actually require long-term retention. However, these systems lack the flexibility to bypass persistence for such transient signals, leading to unnecessary I/O operations and storage usage.

TimeTick 这种临时信号根本不需要长期保留——但 Kafka/Pulsar 强制走持久化,每条 TimeTick 都要落盘。3.0 的 non-persisted TimeTick 优化(timetick 拦截器生成的、跳过 WAL 后端写入的 TimeTick)就是为这个问题设计的——但只有自建 WAL 才能做到。

Woodpecker 的 Zero-Disk 架构

Woodpecker 的核心创新是 Zero-Disk 架构

  • 所有日志数据存在云对象存储(S3、GCS、阿里 OSS)
  • 元数据通过 etcd 管理
  • 核心操作不依赖本地磁盘

这种架构带来的收益是计算和存储解耦——本地节点只负责缓存和写入协调,数据落到对象存储。本地节点挂了,重启后从对象存储恢复即可。

性能基准

官方博客给了硬数据:

系统

Kafka

Pulsar

WP MinIO

WP Local

WP S3

Throughput

129.96 MB/s

107 MB/s

71 MB/s

450 MB/s

750 MB/s

Latency

58 ms

35 ms

184 ms

1.8 ms

166 ms

关键数字

  • Local 文件系统:Woodpecker 比 Kafka 快 3.5×,比 Pulsar 快 4.2×
  • S3 模式:Woodpecker 比 Kafka 快 5.8×,比 Pulsar 快
  • 即使 MinIO(最弱配置)也能达到后端 65% 容量

Latency 也更低——Local 模式 1.8 ms 是单数字毫秒级,金融级场景够用。

Woodpecker vs Kafka/Pulsar 性能对比:吞吐提升 3.5-7×
Woodpecker vs Kafka/Pulsar 性能对比:吞吐提升 3.5-7×

两种部署模式

Woodpecker 提供了两种部署模式(按缓冲策略分):

  • MemoryBuffer 模式:临时把写入缓存到内存,定期刷到对象存储。适合批写优先的小规模部署,省钱
  • QuorumBuffer 模式:3 副本 quorum 写入(写成功两个就算成功),单数字毫秒级延迟。适合金融级场景,强一致 + 强可用

Milvus 3.0 的配置 woodpecker.client.quorum.quorumSelectStrategy.replicas: 3 默认是 QuorumBuffer 模式。

部署形态上还分两种(按运行时分类):

  • Embedded Mode:作为库内嵌到 Milvus 进程,依赖少
  • Service Mode:独立 LogStore 服务集群

Object Storage 兼容性

Woodpecker 依赖 S3 Conditional Write 语义(用条件写保证 idempotency)。官方文档列出了兼容性矩阵:

  • ✅ AWS S3、Azure Blob、MinIO(>=2024-12)、阿里 OSS、腾讯 COS、GCS、VAST Data
  • ❌ 华为 OBS(不支持 Conditional Write)

生产部署时要注意——如果你的对象存储不在白名单,Zero-Disk 架构跑不起来。

许可证细节

Woodpecker 的服务端组件(server/ 目录)是 AGPLv3 或 SSPLv1 双协议,其他组件是 Apache 2.0。如果 Milvus 集成 Woodpecker 服务端到分发包,会触发 AGPLv3/SSPLv1 传染性——这是社区版和商业版需要关注的法律细节。

6. 可插拔后端 + AlterWAL:运行时切换不停服

3.0 的 WAL 后端是可插拔的——支持四种实现:

后端

定位

适用场景

Woodpecker

Milvus 原生 WAL

新实例推荐,生产集群

Kafka

通用 MQ

已有 Kafka 基础设施

Pulsar

通用 MQ

已有 Pulsar 基础设施

RocksMQ

进程内 RocksDB 队列

Standalone 单节点

MQ 选择优先级(configs/milvus.yaml:171-173):

  • standalone:rocksmq > Pulsar > Kafka > Woodpecker
  • cluster:Pulsar > Kafka > Woodpecker

官方明确建议:新实例显式使用 Woodpecker——性能、运维、成本都更好。

AlterWAL 消息:运行时切换

更激进的设计是 AlterWAL 消息——可以在不停服的情况下运行时切换 WAL 后端。

源码实现(internal/streamingnode/server/wal/adaptor/opener.go)有两阶段切换:

  1. FLUSHING 阶段:flush 所有 Growing Segment,等待完成
  2. ADVANCE_CHECKPOINT 阶段:更新 VChannel 和 PChannel 的 checkpoint 到新 WAL 位置

判断后端选择的逻辑很巧妙——determineWALName() 从 checkpoint 的 MessageID.WALName() 推断当前应该用哪个后端。checkpoint 就是后端身份的真相源

3.0-beta 的升级约束(官方 message_storage_operator.md 文档):

When upgrading to Milvus v3.0-beta, you must maintain your current message queue choice. Switching between different message queue systems during the upgrade is not supported.

也就是说,3.0-beta 升级时不能切 MQ。但 AlterWAL 机制已经准备好,等未来版本放开。

这是个灰度迁移的关键设计——源码层面四条路径都还活着(research-01.md 第 1 节列出的 4 种数据流,其中 Replicated 路径说明旧 MQ 仍然存在)。

7. RecoveryStorage:可证明的故障恢复

故障恢复是分布式系统的命门。3.0 的 RecoveryStorage(internal/streamingnode/server/wal/recovery/recovery_storage_impl.go,682 行)有一个核心不变量

从任意 WAL 位置 + 对应的持久化状态,RecoveryStorage 可以向前重放 WAL,完全恢复一致的内存状态。

RecoverySnapshot 结构包含:

代码语言:go
复制
type RecoverySnapshot struct {
    VChannels         map[string]*streamingpb.VChannelMeta
    SegmentAssignments map[int64]*streamingpb.SegmentAssignmentMeta
    Checkpoint        *WALCheckpoint
    TxnBuffer         *utility.TxnBuffer
    AlterWALInfo      *AlterWALInfo
    SalvageCheckpoint *SalvageCheckpoint
}

恢复流程分两步:

  1. recoverRecoveryInfoFromMeta():从 catalog 并行加载 checkpoint、VChannel 元数据、Segment 分配
  2. recoverFromStream():从 checkpoint 到当前 WAL 位置构建 RecoveryStream,重放消息重建内存状态

Checkpoint 是恢复的锚点——SALVAGE_CHECKPOINT 用于跨 AlterWAL 边界的恢复(即使切换了后端,旧 checkpoint 仍然有效)。

这个设计的工程意义是:任意时刻重启 StreamingNode 都能恢复到一致状态,不需要人工干预。2.x 时代故障恢复涉及多个组件(msgstream 消费位点、etcd 元数据、DataNode 内存状态),每块都要单独处理。

事务系统

3.0 新增了事务系统(2.x 没有真正意义上的事务)。消息类型:BeginTxn=900CommitTxn=901RollbackTxn=902Txn=999

TxnManager 维护每个 VChannel 的 in-flight 事务计数,提供四个操作:

  • BeginNewTxn()——分配 TxnID + TimeTick,创建 TxnSession
  • FailTxnAtVChannel(vchannel)——中断某 VChannel 的所有事务(lock 拦截器调用)
  • RollbackAllInFlightTransactions()——force promote 时调用
  • GracefulClose()——等待所有事务完成

事务的生命周期有六种结局:Begin、Append、Commit、Rollback、Expire(TTL 超时)、Force-fail(排他操作时强制中断)。

Force-fail 机制让 DDL 操作可以立即中断 DML 事务,不需要等事务自然结束——这是把锁机制和事务机制打通的桥梁。

8. 必须指出的不完整

源码和官方文档看下来,3.0 streaming system 也有已知的不完整

  1. 端到端幂等性还在规划:milvus-io/milvus Issue #50000 提出要从 Client 端一路传递 idempotency_key 到 WAL。当前版本只到 at-least-once,exactly-once/effectively-once 是未来目标
  2. 3.0-beta 不能切 MQ:升级时必须保持原 MQ 类型,等未来版本放开
  3. 拦截器链设计动机没有公开 RFC:源码能看到五层实现,但官方没有解释为什么是这个顺序——本文的设计哲学总结是基于源码推断,不是官方明示
  4. 官方文档对外覆盖薄弱:内部 docs/agent_guides/streaming-system/ 有 19 个 markdown 文件,但 web-content 仓库的对外文档还是 2.6 时代的,3.0 streaming system 的变更在 release notes 里完全没提

这些不完整不影响 3.0 streaming system 的核心价值——但作为读者,应该知道当前的状态。

总结

Milvus 3.0 的流式架构变革可以浓缩成一句话:把所有变更写进同一条 WAL,让 TimeTick 提供全局可见性边界

围绕这个核心目标,3.0 做了几件大事:

  • 三层通道模型(PChannel/VChannel/CChannel)解决了并行度、隔离性、全局排序三个独立维度的问题
  • 五层拦截器链用洋葱模型把 redo/lock/replicate/timetick/shard 五个职责拆成可组合的层
  • Broadcaster + Resource Key + CChannel TimeTick解决了 DDL 跨 PChannel 的原子性问题
  • Woodpecker 替代 Kafka/Pulsar带来 3.5-7× 的吞吐提升,运维也更简单
  • AlterWAL 消息让 WAL 后端可以在运行时切换(虽然 3.0-beta 还不能)
  • RecoveryStorage 不变量保证任意时刻重启都能恢复到一致状态

推倒重来的代价写在源码里:写入路径全量迁移、拦截器基础设施从零搭、Recovery 系统从头写、事务系统从无到有、2.x 兼容代码(旧消息、msgstream 适配器)还得维护。但收益也是结构性的:统一真相源、清晰的一致性语义、可证明的故障恢复、可插拔的 WAL 后端。

3.0 streaming system 不是 2.x 的小修小补,是一次写入路径的彻底重构。理解 3.0,要从理解 WAL-first 开始;理解 WAL-first,要从理解为什么 2.x 的双轨制不能再用了开始。

说明:本文内容基于 Milvus 3.0 主源码(zilliztech/milvus-io/milvus,commit 时间戳 2026-07-10)和 docs/agent_guides/streaming-system/ 下的 19 篇内部架构文档分析整理而成,性能数据来自官方博客《We Replaced Kafka/Pulsar with a Woodpecker for Milvus》。文中的架构解读、性能数字、源码行号和配置参数均经过交叉验证,但实际生产环境的稳定性、性能特征和运维建议请以你的业务数据和环境测试结果为准。 如果你在真实生产环境跑过 Milvus 3.0 流式架构,欢迎在评论区分享你的经验。

好啦,谢谢你观看我的文章,如果喜欢可以点赞转发给需要的朋友,我们下一期再见!敬请期待!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. WAL-first 架构变革:从双轨到单轨
  • 2. 三层通道模型:物理并行、逻辑隔离、全局排序
    • PChannel:物理并行度的基本单元
    • VChannel:集合级隔离
    • CChannel:全局排序的锚点
    • 设计动机:解决 2.x 跨通道一致性问题
  • 3. 五层拦截器链:洋葱模型的设计哲学
    • redo 层:处理可重试错误
    • lock 层:PChannel 级和 VChannel 级双层锁
    • replicate 层:CDC 复制分支
    • timetick 层:最复杂的一层
    • shard 层:Segment 分配和 13 种 Seal 策略
    • 设计哲学总结
  • 4. Broadcaster:跨 PChannel 原子广播
    • 六步广播流程
    • Resource Key:锁的粒度
    • CChannel TimeTick 的作用
  • 5. Woodpecker:自建 WAL 的 3 个理由 + 性能基准
    • Woodpecker 的 Zero-Disk 架构
    • 性能基准
    • 两种部署模式
    • Object Storage 兼容性
    • 许可证细节
  • 6. 可插拔后端 + AlterWAL:运行时切换不停服
    • AlterWAL 消息:运行时切换
  • 7. RecoveryStorage:可证明的故障恢复
    • 事务系统
  • 8. 必须指出的不完整
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档