首页
/ Milvus 的 Channel 模型深度解析:PChannel、VChannel 与 CChannel 的分区与路由原理

Milvus 的 Channel 模型深度解析:PChannel、VChannel 与 CChannel 的分区与路由原理

2026-09-08 11:40:08作者:翟萌耘Ralph

Milvus 的流式系统以 WAL(Write-Ahead Log) 作为所有数据变更与元数据变更的唯一事实来源(single source of truth),而 WAL 在逻辑上被切分为三种 Channel:PChannel(物理通道)VChannel(虚拟通道)CChannel(控制通道)。本文基于仓库中的 channel/channel.md 展开,结合命名工具、PChannel 元数据与配置源码,讲解三种 Channel 各自的定位、命名规范、相互映射关系以及它们在负载均衡与故障转移中的角色,帮助你快速理解 Milvus 流式系统分区与路由的核心模型。

一、为什么需要三种 Channel:从单点 WAL 到可分区 WAL

在 Milvus 的流式系统架构中,WAL 并非一个单体的日志流。为了把写入压力分散到多个 StreamingNode 上,同时保持"每个集合的每个分片(shard)有清晰可路由的写入边界",WAL 被抽象为物理分区逻辑通道两个层次:

  • PChannel 代表物理上的分区,直接与底层 WAL Backend 的 topic/partition 一一对应,是系统横向扩展的粒度;
  • VChannel 是面向"集合 × 分片"的逻辑视图,让上层组件(Proxy、DataNode 等)不需要感知底层物理布局;
  • CChannel 是一个特殊的单例控制通道,为跨所有节点的集群级广播操作提供一个全局排序点。

这一模型的整体定位可以从流式系统知识库入口 streaming-system.md 中看到:"WAL spans multiple PChannels distributed across StreamingNodes, coordinated by StreamingCoord, and accessed by other components through a StreamingClient library."

二、PChannel(Physical Channel):WAL 分区的基本单元

2.1 核心定义

PChannel 是 WAL 分区的基本单元(fundamental unit of WAL partitioning)。每个 PChannel 与 WAL Backend 中的一个 topic/partition 建立 1:1 映射关系——也就是说,一条消息只会落在一个 PChannel 上,这个 PChannel 就是该消息所属的持久化存储单位。

在任意时刻,一个 PChannel 只会被分配给一个 StreamingNode。分配动作由 StreamingCoord 中的 Balancer 完成,并通过一个单调递增的 Term(任期号)对旧分配进行"围栏(fence)",防止上一个节点的陈旧写入继续生效。

这一点在源码 pkg/streaming/util/types/pchannel_info.go 中有清晰的体现:PChannelInfo 结构体包含 NameTermAccessMode 三个字段,其中 AccessMode 分为两种:

const (
    InitialTerm  int64      = -1
    AccessModeRW AccessMode = AccessMode(streamingpb.PChannelAccessMode_PCHANNEL_ACCESS_READWRITE) // 默认选项
    AccessModeRO AccessMode = AccessMode(streamingpb.PChannelAccessMode_PCHANNEL_ACCESS_READONLY)
)

从该文件注释可以看出 AccessMode 的语义:

  • AccessModeRW:WAL 实现处于读写模式,新 owner 上任时需要 fence 掉旧的 RW 实现,或等待旧的 RW 实现关闭
  • AccessModeRO:WAL 实现只读(例如主从复制场景中处于只读一端的节点),此时 append 操作会直接 panic。

这就是 Term 围栏机制在数据结构层面的落地:每次重新分配,Term 都会增加,客户端持有的"PChannel → 节点 + Term"快照如果过期,其写入就会被拒绝,从而保证同一个 PChannel 不会同时存在两个活跃写入者。

2.2 命名规则与配置

命名格式<prefix>_<index>

例如 by-dev-rootcoord-dml_0 表示:集群前缀为 by-dev、通道类型为 rootcoord 的 DML 通道、索引为 0。

PChannel 的总数在集群启动时固定,由配置项 rootCoord.dmlChannelNum 决定。查看 configs/milvus.yaml

rootCoord:
  dmlChannelNum: 16 # The number of DML-Channels to create at the root coord startup.

即默认创建 16 个 DML PChannel,这也是生产环境中常见的默认值——如果你看到 by-dev-rootcoord-dml_0by-dev-rootcoord-dml_15 共 16 个通道,即来源于此。

通道名的 prefix 部分由 msgChannel.chanNamePrefix 拼装而来。在 configs/milvus.yaml 中可看到:

# The complete channel name prefix is ${msgChannel.chanNamePrefix.cluster}-${msgChannel.chanNamePrefix.rootCoordDml}
rootCoordDml: rootcoord-dml

也就是说,by-dev-rootcoord-dml = ${cluster}(by-dev) + - + ${rootCoordDml}(rootcoord-dml)。前缀中还包含 rootCoordTimeTickrootcoord-timetick)与 rootCoordStatisticsrootcoord-statistics)等其他通道族。

2.3 物理通道的判定逻辑

命名规范不仅方便人类阅读,也为程序判断通道类型提供了依据。在 pkg/util/funcutil/func.go 中:

// IsPhysicalChannel checks if the channel is a physical channel
func IsPhysicalChannel(channel string) bool {
    i := strings.LastIndex(channel, "_")
    if i == -1 {
        return true
    }
    return !strings.Contains(channel[i+1:], "v")
}

判断规则是:取最后一个 _ 之后的后缀,若后缀中不包含字母 v,则判定为物理通道。这与 VChannel 的命名中固定含 v(版本/分片标识)的设计正好呼应。

三、VChannel(Virtual Channel):面向集合分片的逻辑通道

3.1 核心定义

VChannel 是作用域限定在"一个集合的一个分片(shard)"上的逻辑通道。它与 PChannel 的关键区别在于:

  • 一个 VChannel 只属于一个集合的一个分片;
  • 来自不同集合的多个 VChannel 可以共享同一个 PChannel(逻辑复用物理资源)。

VChannel 到 PChannel 的映射由 ChannelManager 通过负载均衡策略分配,这正是 channel_management.md 中描述的 AllocVirtualChannels() 流程:新 VChannel 会被优先分配到负载最低且 AvailableInReplication 的 PChannel 上。

VChannel 定义了以下三类操作的作用域

  • DML 消息路由(Insert/Delete 等写操作按分片哈希落到对应 VChannel);
  • 事务(transaction)timetick_and_txn.md 中的事务生命周期以 VChannel 为范围边界);
  • Segment 分配shard-management.md 中每个分片的 segment 归属)。

3.2 命名规则与双向解析

命名格式<pchannel_name>_<collectionID>v<shardIndex>

by-dev-rootcoord-dml_0_12345v0 为例,它表达了三层信息:

组成部分 取值 含义
PChannel 名 by-dev-rootcoord-dml_0 承载该 VChannel 的物理通道
CollectionID 12345 所属集合 ID
shardIndex 0 集合内的分片索引(第 0 个分片)

要从 VChannel 名中提取其所在的 PChannel,使用工具函数 funcutil.ToPhysicalChannel()。该函数的实现位于 pkg/util/funcutil/func.go

// ToPhysicalChannel get physical channel name from virtual channel name
func ToPhysicalChannel(vchannel string) string {
    if IsPhysicalChannel(vchannel) {
        return vchannel
    }
    index := strings.LastIndex(vchannel, "_")
    if index < 0 {
        return vchannel
    }
    return vchannel[:index]
}

即:取最后一个 _ 之前的部分就是 PChannel 名。func.go 中还有一组完整的配套函数:

  • GetVirtualChannel(pchannel, collectionID, idx):由 PChannel 名 + 集合 ID + 分片索引生成 VChannel 名,格式为 %s_%dv%d
  • ParseVChannel(vchannel)严格解析规范 VChannel 名,返回 (pchannel, collectionID, shardIndex, error)。它对每个组成部分做合法性校验——例如集合 ID 必须是非负十进制整数且不能带前导零、v 分隔符前后都不能为空,否则返回 merr 包装的错误;
  • GetCollectionIDFromVChannel(vChannelName):用正则 .*_(\d+)v\d+ 从 VChannel 名中提取集合 ID;
  • IsOnPhysicalChannel(channel, physicalChannel):判断某通道是否位于指定物理通道上(即 ToPhysicalChannel(channel) == physicalChannel)。

这些函数是 Proxy、DataNode、StreamingNode 等组件做通道路由、恢复与过滤时的公共基础,散落在 pkg/util/funcutil/ 中的同名工具库是它们的统一入口。

四、CChannel(Control Channel):集群级单例控制通道

4.1 核心定义

CChannel 是一种特殊的 VChannel,充当整个集群的单例控制通道。它的特殊性体现在两点:

  1. 永久绑定:集群初始化时被绑定到一个固定的 PChannel 上,此后永不改变(由 StreamingCoord 在启动阶段持久化该绑定关系,详见 channel_management.md);
  2. 全局排序点:为集群级广播操作提供一个单一的顺序锚点。

典型使用场景是 RBAC 变更等"影响所有节点、与具体集合无关"的操作:例如创建一个新角色或修改权限,它会影响整个集群而非某个集合的某个分片,因此不适合通过某个集合的普通 VChannel 传播,而要走 CChannel 这一统一广播路径。对应的消息语义可参考 message-semantic-rbac.md

4.2 命名规则

命名格式<pchannel_name>_vcchan

例如 by-dev-rootcoord-dml_0_vcchan,表示承载在 PChannel by-dev-rootcoord-dml_0 上的控制通道。

控制通道后缀常量定义在 pkg/util/funcutil/func.go 中:

ControlChannelSuffix = "vcchan" // is the suffix of the virtual control channel

配套函数:

// GetControlChannel returns the control channel name of the pchannel.
func GetControlChannel(pchannel string) string {
    return fmt.Sprintf("%s_%s", pchannel, ControlChannelSuffix)
}

// IsControlChannel checks if the channel is a control channel
func IsControlChannel(channel string) bool {
    return strings.HasSuffix(channel, ControlChannelSuffix)
}

注意一个容易混淆的点:IsControlChannelstrings.HasSuffix(channel, "vcchan") 判定。由于后缀 vcchan 不包含下划线后的数字与 v,该判定与 IsPhysicalChannelToPhysicalChannel 的解析逻辑可以组合使用而不冲突——控制通道在逻辑上是 VChannel 家族的一员,但承载在固定的物理通道上。

五、三种 Channel 判定与转换速查表

基于 channel.md 的命名规范和 pkg/util/funcutil/func.go 的实现,整理成可直接查阅的表:

问题 方法 说明
这是物理通道吗? IsPhysicalChannel(name) 最后一个 _ 后缀不含 v
这是控制通道吗? IsControlChannel(name) vcchan 结尾
从 VChannel 名提取 PChannel ToPhysicalChannel(vchannel) 取最后一个 _ 之前的部分
VChannel 是否在指定 PChannel 上 IsOnPhysicalChannel(channel, p) 等价于 ToPhysicalChannel(channel) == p
生成 VChannel 名 GetVirtualChannel(p, collID, idx) 格式 p_collIDvidx
严格解析 VChannel 名 ParseVChannel(vchannel) 返回 (p, collID, idx),校验非法输入
提取 VChannel 中的集合 ID GetCollectionIDFromVChannel(name) 正则提取
生成 CChannel 名 GetControlChannel(pchannel) PChannel 名拼接 vcchan

这些工具函数构成了所有上层组件处理 Channel 名时的公共规范,避免各模块各自实现解析逻辑导致的不一致。

六、从源码看 Channel 的"一生":元数据与状态迁移

理解了三类 Channel 的定义后,我们可以从源码再往下挖一层,看 Channel 元数据在 StreamingCoord 中是如何被建模与维护的。

6.1 PChannel 元数据结构

internal/streamingcoord/server/balancer/channel/pchannel.go 中定义了 PChannelMeta,它持有底层 protobuf 元数据并提供只读访问接口。从代码注释与结构看:

  • 新建的 PChannel 默认 Term1 开始(区别于 types.InitialTerm = -1),初始状态为 UNINITIALIZED,且默认 availableInReplication = true
  • CurrentServerID() 返回当前被分配的节点 ID,若未分配则返回 -1;
  • AssignHistories() 保留该通道的历史分配记录(每次分配时的 Term 与节点),供审计与故障排查使用;
  • AvailableInReplication() 决定该通道是否可用于 VChannel 分配与 DDL 广播:未配置复制(或未加入复制集群)时所有 PChannel 均可用;一旦加入复制,只有当前集群 ReplicateConfiguration.pchannels 中列出的通道可用

关于 Channel 分配的状态机与两阶段确认(AssignPChannelsAssignPChannelsDone)的完整流程,可进一步阅读 channel_management.md

6.2 PChannel 的来源:静态配置 + 动态扩展

PChannel 集合并非完全静态。从 internal/streamingcoord/server/balancer/ 的模块划分看:

  • balancer/policy_registry.go:Balancer 主逻辑与可插拔的负载均衡策略注册;
  • channel/ 子目录:ChannelManagerPChannelMeta 等通道管理与元数据实现;
  • channel_provider.go:PChannel 的提供方,负责把配置初始化与运行时动态添加(如 AddPChannels())统一成 Balancer 可见的通道集合。

换句话说:启动时由 rootCoord.dmlChannelNum 生成固定数量的 PChannel,运行过程中系统也可以按需动态扩展(新通道同样受 Replicate 配置的 gating 约束)。

6.3 VChannel 与 CChannel 的分工再梳理

把三类 Channel 放到消息写入路径上理解会更加直观:

  • DML 消息Client → Proxy → StreamingClient.Append → StreamingNode → WAL Backend,其中 Append 需要首先解析出目标 VChannel 所属的 PChannel(ToPhysicalChannel),再按 PChannel 的当前分配找到持有该通道的 StreamingNode;
  • DDL/DCL(如 RBAC):走 StreamingClient.Broadcast → StreamingCoord.Broadcaster → 所有相关 PChannel(原子广播),其中与集合无关的集群级广播经由 CChannel 提供的全局排序点统一排序;
  • 关于 Broadcast 如何在多个 PChannel 间做资源锁定与 ACK 追踪,可参考 broadcaster.md

七、相关代码与文档指引

如果你希望进一步深入,以下是围绕 Channel 模型的核心代码与文档入口:

结语

PChannel、VChannel 与 CChannel 构成了 Milvus 流式系统分区与路由的三层骨架:PChannel 面向物理存储与横向扩展,VChannel 面向集合分片的逻辑隔离与负载均衡,CChannel 则为集群级控制面提供稳定的全局排序点。理解三者的命名规范(尤其是 _index_collectionIDvshardIndex_vcchan 三种形态)和 funcutil 中配套的解析函数,是阅读 Proxy、StreamingNode、StreamingCoord 各模块源码以及排查通道路由问题时的第一步。建议在阅读 streaming-system.md 架构总览后,结合本文与 channel_management.md 一起阅读,即可对 Milvus 的流式写入路径形成完整认知。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391