
🚩 2026 年「术哥无界」系列实战文档 X 篇原创计划 第 184 篇,Milvus 最佳实战「2026」系列第 25 篇
大家好,欢迎来到 术哥无界 | ShugeX | 运维有术。
我是术哥,一名专注于 AI 编程、AI 智能体、Agent Skills、MCP、云原生、AIOps、Milvus 向量数据库的技术实践者与开源布道者!
Talk is cheap, let's explore。无界探索,有术而行。

Milvus 3.0 正式版在 2026 年 7 月 29 日发布,距离 5 月 9 日的 3.0-beta 只隔了不到三个月。LF AI & Data 官方博客把它称为项目历史上一次大规模架构更新。这话不是营销话术,从源码和官方文档里,能数出至少五条同时断裂的架构线。
如果只记住一个判断,那就是:2.x 到 3.0 是断点升级,不是平滑升级。官方用三个不支持把话说死了:不支持从 release candidate 版本直升、升级过程中不允许切换消息队列、写入 3.0 之后无法靠镜像回滚(官方 v3.0.0 升级指南明确,v3.0 写入后仅回滚镜像无法读取数据)。
这一篇是 Milvus 3.0 系列的第 8 篇。前七篇拆过 Storage V3 的 manifest 表格式、Streaming System 的 WAL 架构这些新地基,这篇从生产升级的角度收个束:断裂在哪、官方用什么兼容设计兜底、评估 3.0 时该看什么。
先看断裂面,再看兼容设计。2.x 时代的五个核心假设,在 3.0 里全部换了:
任何一条单拎出来都够一次大版本升级,3.0 一次全上。官方升级文档给过明确的 beta 期路径:2.5.x → 2.5.16(启用 mixCoordinator)→ 3.0-beta;GA 之后更新为 2.6.20 → 3.0.0(Operator 1.3.0)。
这里有个被官方牺牲掉的版本:2.6.0-rc1。Helm chart 明确写 v2.6.0-rc1 与 v2.6.x 不兼容、不能从 release candidate 直升。rc1 用户要保数据只能走社区迁移方案,官方不提供升级工具。rc1 是架构切换的中间态,官方直接把它牺牲了,用它试错,然后锁死路径。
时间线说明一下:beta(2026-05-09)到 GA(2026-07-29)只隔两个多月,官方对断裂面应该是有把握的。但对生产用户来说,关键问题从来不是新功能好不好,而是存量数据能不能平滑过渡。下面几节就是官方为这个问题写的答案。

compaction 顺手转格式,四层保护加两个兜底压住风险
存储格式换代,业界常规做法是两种:停机迁移(备份、导出、导入、切流量),或者写批量迁移脚本在后台跑。Milvus 两个都没选,选了第三条路:让 compaction 干这个活。
系列前篇已经拆过 storageVersionUpgradePolicy 的 targetVersion 逻辑和三层保护机制,不重复。这里补全全景:这个迁移策略默认是开着的,StorageVersionCompactionEnabled 默认 true(2.6.10 引入),3.0.0 又加了 StorageFormatCompactionEnabled(默认 false)做更细粒度的格式对齐。
真正的总开关是 useLoonFFI(默认 false)。打开后,新写入的 segment 在 CreateNewGrowingSegment 里直接生成 V3 格式(同时生成 manifest path);存量 V2 segment 不立即动,等 compaction 触发时,storageVersionUpgradePolicy 发现 storage_version 不等于 targetVersion,顺手把它转成 V3。
storageVersionUpgradePolicy 选 segment 的条件也写得很克制:healthy、flushed、非 compacting、非 importing、非 L0、未受保护、storage_version 不等于 targetVersion。正在写入的、正在合并的、正在导入的 segment 一律不碰,迁移只挑状态稳定的下手。这套选择条件保证格式迁移不会跟正常写入流程抢资源,也不会在 segment 还没落稳的时候就动它。
这个选择的收益是结构性的:
但顺手是有代价的,所以官方加了四层保护:
第一层,TEXT 拒绝降级。目标版本低于 V3 且集合有 TEXT 字段,整个集合跳过,不做静默提升。TEXT 需要 V3 manifest 的 LOB 存储支持,降级会丢数据。
第二层,速率限制。每 120 秒最多跑 3 个版本升级 compaction(rateLimitTokens=3、rateLimitInterval=120s),防止后台迁移把集群 I/O 吃满。
第三层,QueryNode 最小版本门控。集群最小 QueryNode 版本低于 2.6.9(sessionVersionRequirement)时不启动迁移,避免新格式 segment 被旧节点读到。
第四层,External Collection 豁免。external collection 数据不在 Milvus 存储里,跳过版本迁移。
除了这四层,还有两个容易被忽略的 fail-closed 设计。
一个是受保护快照门控。isCollectionCompactionBlocked 会检查集合有没有未加载的 protected snapshot RefIndex,如果有,升级 compaction 保守跳过,日志写着 skip storage version compaction for collection due to unloaded protected snapshot RefIndex。意思是加载失败时宁可不动,也不在引用索引没就绪的状态下迁移格式。
另一个藏在 compaction 参数生成里(GenerateJSONParams):即使 useLoonFFI 被关掉,只要集合含 TEXT 字段,也会强制把目标版本提到 V3。注释说得很直白:TEXT fields require at least V3 manifest storage for LOB support. This is a safety net。
一句话总结这节:格式迁移的成本被分摊进 compaction,而风险被四层保护加两个兜底压住。这是 3.0 兼容设计里很完整的一条线。
Streaming System 把写入路径整个重写了,但旧世界的消息不能不管。消息版本分三档(pkg/streaming/util/message/version.go):
VersionOld Version = 0 // streamingnode 之前的旧消息,2.6 保留,3.0 移除
VersionV1 Version = 1 // 编解码仍依赖 msgstream
VersionV2 Version = 2 // 完全不依赖 msgstream注释本身就是设计意图:V0 是明确要淘汰的东西,但代码里仍然留了一个完整适配层。V1 这个档位的信息量也不小:它的编解码仍依赖 msgstream,说明 msgstream 这套库没有整体删除,而是降级成了编解码器,继续服务过渡期。
旧消息适配在 internal/streamingnode/server/wal/adaptor/old_version_message.go。newOldVersionImmutableMessage 把 V0 消息转成 V1,交给 streaming service 消费。代码注释挺坦诚:转换会损失一些性能,但旧消息量总是很小,可以接受。一共覆盖 8 种消息类型:CreateCollection、DropCollection、Insert、Delete、TimeTick、CreatePartition、DropPartition、Import。
真正的难点不在转换本身,而在反推 vchannel。V0 时代的消息没有 vchannel 概念,适配器得从 pchannel 和消息数据里猜:CreateCollection 从 PhysicalChannelNames/VirtualChannelNames 匹配,Drop/Partition/Import 通过 VChannelTempStorage 反查。
消费侧也留了适配器。QueryNode 里还活着一批旧 msgstream 消费者,delegatorMsgstreamAdaptor 把 streaming 的 WAL Scanner 包装成旧 msgstream.MsgStream 接口:Seek() 把 MsgPosition(WALName + MsgID)转成 streaming 的 DeliverPolicyStartFrom,再加 DeliverFilter 只消费 Insert、Delete、SchemaChange、AlterCollection、ManualFlush 这几类消息。有意思的是生产者侧方法全部 panic("should never be called"),它只做消费适配。
落到工程上,意思很明白:架构推倒重来不等于删除所有旧入口。真实迁移是层层适配,让新旧世界在同一个进程里共存,而不是一刀切。V0 消息注释里写着 3.0 移除,但适配代码还在,这就是兼容设计的典型姿态:该淘汰的淘汰,该留的后门留。

该淘汰的淘汰,该留的后门留
读源码的人会发现:3.0 的 WAL 后端是可插拔的(Woodpecker/Kafka/Pulsar/RocksMQ 四种实现),而且 AlterWAL 机制已经实现了两阶段切换(FLUSHING → ADVANCE_CHECKPOINT,系列前篇讲过细节)。
那升级的时候顺手把 MQ 换成 Woodpecker,行不行?
官方文档在升级指南和 message_storage_operator 里至少 3 处强调同一件事:升级到 3.0-beta 时必须保持当前的 message queue 选择,升级过程中切换 MQ 系统不支持,支持切换要等未来版本。
这里有个很容易被忽略的区分:机制存在不等于官方支持。
AlterWAL 在源码里是真实存在的可用能力,但 3.0-beta 的升级路径没有开放它。原因官方没说,我的判断是:AlterWAL 切换涉及 checkpoint 语义、消费位点迁移、两阶段状态机,这类能力还没经过足够多的生产验证,官方宁可把升级矩阵锁死,也不愿意在升级这种高危操作里再叠加一个高危操作。
对生产评估来说,结论很简单:看官方升级路径,别被源码里的能力清单带着跑。AlterWAL 是未来能力的信号,不是现在可以这么干的许可。
索引格式升级和存储格式升级有个关键区别:它没有 compaction 渐进迁移可依赖,走的是显式版本路由。
build 侧,ScalarIndexCreator::Upload() 检查 SCALAR_INDEX_ENGINE_VERSION 配置,大于等于 3 调 UploadV3(),否则走老 Upload()。load 侧同理,SealedIndexTranslator 检查 scalar_index_engine_version,大于等于 3 且非 vector 调 LoadV3()。
// build 侧逻辑示意(非完整源码)
if config.SCALAR_INDEX_ENGINE_VERSION >= 3 {
return uploader.UploadV3(index)
}
return uploader.Upload(index) // 老格式路径仍保留版本决策集中在 Go control plane(pkg/common/common.go):MinimalScalarIndexEngineVersion=0、CurrentScalarIndexEngineVersion=4、MaximumScalarIndexEngineVersion=4。这里有个有意思的演进:设计文档 2026 年 2 月写作时 current 还是 2,现在源码已经到 4 了,V3 格式已默认启用,版本 4 支持 JSON path index 多类型(STL_SORT/BITMAP/HYBRID),on-disk 格式和 v3 相同。
为什么不做运行时 sniff(读文件头魔数自动判断格式)?从设计上看,核心原因是版本决策要集中在控制面,数据面保持无状态。build/load 节点只负责执行,不负责判断这个格式认不认识;版本判断是集群级配置,QueryNode/DataNode 通过 sessionutil 的 WithScalarIndexEngineVersion 上报自己支持的版本范围(minimal/current/maximum),控制面据此决定让谁干活。
这个设计的直接收益是回滚成本低:把 CurrentScalarIndexEngineVersion 设回 2,V2 的 Upload/Load 方法还在,不用改代码。代价是版本号成了需要集群配合的配置项,QueryNode 版本不齐时,可能出现新索引建了但旧节点读不了的情况。

版本决策集中在控制面,数据面保持无状态
前几节讲的是数据格式怎么迁,这节讲的是已有集合怎么用上新功能。3.0 在这方面有个容易被低估的设计:schema evolution。
场景很具体:一个只存 dense 向量的集合,想做混合检索,给现有字段加 BM25 全文检索。2.x 的做法是重建集合、重灌数据,成本高到很多人直接放弃。
3.0 支持 add function field:通过 AlterCollectionSchema RPC 动态加 function field,把现有字段喂给 BM25 函数;支持可选回填(do_physical_backfill),也支持 lazy evaluation(函数输出按需计算,不用一次性把全量数据算完)。schema 变更先写 WAL 再更新内存状态,每次变更递增 schema version。约束也清楚:一次只能加一个 function field,所有 segment 的 schema version 必须先对齐。
drop 侧也有完整设计(feat/drop-collection-field 分支已落地实现)。DropRequest 支持 drop field、drop function、detach function 三种动作,其中三个细节很见功力:
配套的还有 BumpSchemaVersionCompaction:schema 版本变更后,重写 segment 使其匹配最新 schema,compaction inspector 每 30 分钟轮询一次。
约束是:不能 drop PK、partition key、clustering key、最后一个 vector 字段,被 function 引用的字段也不能 drop。这些约束不是拍脑袋,每个都是数据完整性的硬边界:PK 没了主键就没了,function 引用的字段被 drop 会让函数失去输入,partition key 被删会影响既有数据的路由。
这组设计的核心信息是:3.0 不只是让你新建集合用新功能,还让存量集合能进化。dense-only 集合加 BM25 不用重建,删字段不用全量重灌。这两个能力直接决定了存量用户升级后的改造工作量。
把前几节放一起看,Milvus 3.0 的兼容设计反复出现五个模式:
对应到五个断裂面,可以画成一张表:
断裂面 | 兼容设计 | 关键门控 |
|---|---|---|
存储格式 V2→V3 | compaction 渐进迁移 | useLoonFFI + 四层保护 |
写入路径 → WAL | 旧消息适配 + msgstream 适配器 | 消息 V0/V1/V2 |
索引格式 V2→V3 | 显式版本路由 | scalar_index_engine_version |
SDK / proto v2→v3 | 无,必须同步升级 | 官方无兼容层 |
部署形态重构 | 锁死官方升级路径 | mixCoord / StreamingNode |
注意表里第四行:SDK/proto 这条断裂线没有兼容设计,官方也没有提供过渡层,它是五条线里需要用户主动、同步、完整配合的一条。其他断裂都有系统侧兜底,这条没有,因为协议不可互换这件事本身没法兜。
这套模式的价值在于可评估性:每个断裂面都配了版本字段、迁移路径、保守开关三件套,升级风险是可见的、分步的,不是一次性赌博。
但也别把兼容设计当成免费午餐。社区信号显示流式化的可用性成本是真实存在的:官方 benchmark 团队自己报过 StreamingNode rebalance 时 503(channel distribution is not serviceable,GitHub issue #48903),真实用户报过迁移后 collection load 卡在 50%(issue #43455)。这两个 issue 说明同一件事:新架构的边界情况,官方和用户都还在摸索。
回到开头的判断:2.x 到 3.0 是断点升级。断点是五条架构线同时换,兼容设计是官方给每条线分别配了版本字段、迁移路径和保守开关。
生产评估 3.0,我认为值得问的三个问题不是新功能有什么,而是:
官方文档没细说的部分(存量 2.x segment 在 3.0 中被读取和 compact 的逐字说明、GA 后社区的采纳情况),当前官方资料或源码中未发现相关依据,这里不做推断。
断裂是确定的,兼容是分层的。官方把升级路径锁得很紧,不允许切 MQ、不允许 rc 直升、不允许回滚,这种保守既是限制,也是对生产用户负责。对大多数 2.x 用户来说,3.0 值得评估,但值得按官方给的路径一步步走,而不是被源码里的能力清单带着跑。
说明:本文内容基于 Milvus 3.0 源码(milvus-io/milvus)与官方 v3.0.0 升级文档分析整理而成,源码分析基于笔者本地仓库版本,尚未在生产环境中完成全场景验证。文中的版本号、参数配置和迁移建议仅供参考,实际效果请以你的业务数据和环境测试结果为准。如果有实际升级经验,欢迎在评论区分享交流。
好啦,谢谢你观看我的文章,如果喜欢可以点赞转发给需要的朋友,我们下一期再见!敬请期待!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。