升级 Milvus 后搜索速度变慢的故障排除:WPS 团队的经验教训

  • Engineering
March 18, 2026
the WPS engineering team

本帖由金山办公软件公司的 WPS 工程团队撰写,他们在推荐系统中使用了 Milvus。在他们从 Milvus 2.2.16 升级到 2.5.16 的过程中,搜索延迟增加了 3 到 5 倍。这篇文章介绍了他们是如何调查并解决这个问题的,可能对社区中计划进行类似升级的其他用户有所帮助。

我们升级 Milvus 的原因

我们是 WPS 工程团队的一员,负责开发生产力软件,我们在在线推荐系统中使用 Milvus 作为实时相似性搜索背后的向量搜索引擎。我们的生产集群存储了数千万个向量,平均维度为 768。数据由 16 个查询节点提供,每个 pod 的配置限制为 16 个 CPU 内核和 48 GB 内存。

在运行 Milvus 2.2.16 时,我们遇到了一个已经影响到业务的严重稳定性问题。在高查询并发情况下,planparserv2.HandleCompare 可能会导致空指针异常,从而导致代理组件崩溃并频繁重启。这个错误在高并发场景下非常容易触发,直接影响了我们在线推荐服务的可用性。

以下是实际的代理错误日志和堆栈跟踪:

[2025/12/23 10:43:13.581 +00:00] [ERROR] [concurrency/pool_option.go:53] ["Conc pool panicked"]
[panic="runtime error: invalid memory address or nil pointer dereference"]
[stack="...
github.com/milvus-io/milvus/internal/parser/planparserv2.HandleCompare
  /go/src/github.com/milvus-io/milvus/internal/parser/planparserv2/utils.go:331
github.com/milvus-io/milvus/internal/parser/planparserv2.(*ParserVisitor).VisitEquality
  /go/src/github.com/milvus-io/milvus/internal/parser/planparserv2/parser_visitor.go:345
...
github.com/milvus-io/milvus/internal/proxy.(*queryTask).PreExecute
  /go/src/github.com/milvus-io/milvus/internal/proxy/task_query.go:271
github.com/milvus-io/milvus/internal/proxy.(*taskScheduler).processTask
  /go/src/github.com/milvus-io/milvus/internal/proxy/task_scheduler.go:455
..."]

panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x2f1a02a]

goroutine 989 [running]: github.com/milvus-io/milvus/internal/parser/planparserv2.HandleCompare(…) /go/src/github.com/milvus-io/milvus/internal/parser/planparserv2/utils.go:331 +0x2a github.com/milvus-io/milvus/internal/parser/planparserv2.(*ParserVisitor).VisitEquality(…) /go/src/github.com/milvus-io/milvus/internal/parser/planparserv2/parser_visitor.go:345 +0x7e5

堆栈跟踪显示的内容:恐慌发生在 Proxy 的查询预处理过程中,在queryTask.PreExecute 内。调用路径为

taskScheduler.processTaskqueryTask.PreExecuteplanparserv2.CreateRetrievePlanplanparserv2.HandleCompare

HandleCompare 尝试访问地址为0x8 的无效内存时发生了崩溃,触发了 SIGSEGV 并导致代理进程崩溃。

为了彻底消除这一稳定性风险,我们决定将集群从 Milvus 2.2.16 升级到 2.5.16。

升级前使用 milvus-backup 备份数据

在接触生产集群之前,我们使用官方的milvus-backup工具备份了所有数据。它支持同一集群内、跨集群和跨 Milvus 版本的备份和还原。

检查版本兼容性

milvus-backup 有两个跨版本还原的版本规则:

  1. 目标群集必须运行相同或更新的 Milvus 版本。2.2 版的备份可以加载到 2.5 版,反之则不行。

  2. 目标必须至少是 Milvus 2.4。不支持较旧的还原目标。

我们的路径(从 2.2.16 备份,加载到 2.5.16)符合这两条规则。

备份自 ↓ → 还原至2.42.52.6
2.2
2.3
2.4
2.5
2.6

Milvus 备份如何工作

Milvus 备份便于跨 Milvus 实例备份和恢复元数据、段和数据。它提供北向接口,如 CLI、API 和基于 gRPC 的 Go 模块,以便灵活操作备份和还原过程。

Milvus Backup 从源 Milvus 实例读取 Collections 元数据和片段,创建备份。然后,它会从源 Milvus 实例的根路径复制 Collections 数据,并将其保存到备份根路径。

要从备份中还原,Milvus Backup 会根据备份中的 Collections 元数据和段信息,在目标 Milvus 实例中创建一个新的 Collections。然后将备份数据从备份根路径复制到目标实例的根路径。

运行备份

我们准备了一个专用配置文件configs/backup.yaml 。主要字段如下所示,敏感值已删除:

milvus:
  address: 1.1.1.1  # Source Milvus address
  port: 19530  # Source Milvus port
  user: root  # Source Milvus username (must have backup permissions)
  password: <PASS> # Source Milvus user password

etcd: endpoints: “2.2.2.1:2379,2.2.2.2:2379,2.2.2.3:2379” # Endpoints of the etcd cluster connected to Milvus rootPath: “by-dev” # Prefix of Milvus metadata in etcd. If not modified, the default is by-dev. It is recommended to check etcd before proceeding.

minio: # Source Milvus object storage bucket configuration storageType: “aliyun” # support storage type: local, minio, s3, aws, gcp, ali(aliyun), azure, tc(tencent), gcpnative address: ks3-cn-beijing-internal.ksyuncs.com # Address of MinIO/S3 port: 443 # Port of MinIO/S3 accessKeyID: object storage AK>
secretAccessKey: object storage SK> useSSL: true bucketName: rootPath: “file” # Root directory prefix under the source object storage bucket where the current Milvus data is stored. If Milvus is installed using Helm Chart, the default prefix is file. It is recommended to log in to the object storage and verify before proceeding.

# Object storage bucket configuration for storing backup data backupStorageType: “aliyun” # support storage type: local, minio, s3, aws, gcp, ali(aliyun), azure, tc(tencent) backupAddress: ks3-cn-beijing-internal.ksyuncs.com # Address of MinIO/S3 backupPort: 443 # Port of MinIO/S3 backupAccessKeyID: backupSecretAccessKey: backupBucketName: backupRootPath: “backup” # Root path to store backup data. Backup data will be stored in backupBucketName/backupRootPath backupUseSSL: true # Access MinIO/S3 with SSL crossStorage: “true” # Must be set to true when performing cross-storage backup

然后运行此命令:

# Create a backup using milvus-backup
./milvus-backup create --config configs/backup.yaml -n backup_v2216

milvus-backup 支持热备份,因此通常对在线流量影响不大。在非高峰时段运行更安全,可避免资源争用。

验证备份

备份完成后,我们验证了备份的完整性和可用性。我们主要检查备份中的 Collections 和网段数量是否与源群集中的相匹配。

./milvus-backup list --config configs/backup.yaml
# View backup details and confirm the number of Collections and Segments
./milvus-backup get --config configs/backup.yaml -n backup_v2216

它们匹配,因此我们继续进行升级。

使用 Helm 图表升级

由于跳转了三个主要版本(2.2 → 2.5)和数千万向量,就地升级风险太大。因此,我们建立了一个新的集群,并将数据迁移到其中。旧群集保持在线,以便进行回滚。

部署新集群

我们使用 Helm 部署了新的 Milvus 2.5.16 集群:

# Add the Milvus Helm repository
: helm repo add milvus https://zilliztech.github.io/milvus-helm/
helm repo update  
# Check the Helm chart version corresponding to the target Milvus version
: helm search repo milvus/milvus -l | grep 2.5.16
milvus/milvus        4.2.58               2.5.16                    Milvus is an open-source vector database built ...

# Deploy the new version cluster (with mmap disabled) helm install milvus-v25 milvus/milvus
–namespace milvus-new
–values values-v25.yaml
–version 4.2.58
–wait

关键配置更改 (values-v25.yaml)

为了公平地进行性能比较,我们尽可能保持新集群与旧集群的相似性。我们只更改了几个与该工作负载相关的设置:

  • 禁用 Mmap(mmap.enabled: false):我们的推荐工作负载对延迟很敏感。如果启用 Mmap,一些数据可能会在需要时从磁盘读取,这会增加磁盘 I/O 延迟并导致延迟峰值。我们将其关闭,这样数据就会完全保留在内存中,查询延迟也会更稳定。

  • 查询节点数:保持16 个,与旧集群相同

  • 资源限制:每个 Pod 仍有16 个 CPU 内核,与旧集群相同

主要版本升级提示:

  • 构建新的群集,而不是就地升级。这样可以避免元数据兼容性风险,并保持干净的回滚路径。

  • 迁移前验证备份。一旦数据采用了新版本的格式,就无法轻易返回。

  • 在切换过程中保持两个群集的运行。逐步转移流量,只有在完全验证后才停用旧群集。

使用 Milvus 升级后迁移数据--备份恢复

我们使用milvus-backup restore 将备份加载到新集群中。在 milvus-backup 的术语中,"还原 "指的是 "将备份数据加载到目标集群"。目标必须运行相同或更新的 Milvus 版本,因此,尽管名称相同,但还原总是将数据向前移动。

运行还原

还原配置文件configs/restore.yaml 必须指向新群集及其存储设置。主要字段如下所示:

# Restore target Milvus connection information
milvus:
  address: 1.1.1.1  # Milvus address
  port: 19530  # Milvus port
  user: root  # Milvus username (must have restore permissions)
  password: <PASS> # Milvus user password  
  etcd:
    endpoints: "2.2.2.1:2379,2.2.2.2:2379,2.2.2.3:2379" # Endpoints of the etcd cluster connected to the target Milvus
    rootPath: "by-dev"  # Prefix of Milvus metadata in etcd. If not modified, the default is by-dev. It is recommended to check etcd before proceeding.

minio: # Target Milvus object storage bucket configuration storageType: “aliyun” # support storage type: local, minio, s3, aws, gcp, ali(aliyun), azure, tc(tencent), gcpnative address: ks3-cn-beijing-internal.ksyuncs.com # Address of MinIO/S3 port: 443 # Port of MinIO/S3 accessKeyID:
secretAccessKey: useSSL: true bucketName: ” rootPath: “file” # Root directory prefix under the object storage bucket where the current Milvus data is stored. If Milvus is installed using Helm Chart, the default prefix is file. It is recommended to log in to the object storage and verify before proceeding.

# Object storage bucket configuration for storing backup data backupStorageType: “aliyun” # support storage type: local, minio, s3, aws, gcp, ali(aliyun), azure, tc(tencent) backupAddress: ks3-cn-beijing-internal.ksyuncs.com # Address of MinIO/S3 backupPort: 443 # Port of MinIO/S3 backupAccessKeyID: backupSecretAccessKey: backupBucketName: backupRootPath: “backup” # Root path to store backup data. Backup data will be stored in backupBucketName/backupRootPath backupUseSSL: true # Access MinIO/S3 with SSL crossStorage: “true” # Must be set to true when performing cross-storage backup

然后我们运行

./milvus-backup restore --config configs/restore.yaml -n backup_v2216 --rebuild_index

restore.yaml 需要新群集的 Milvus 和 MinIO 连接信息,这样还原的数据就会写入新群集的存储中。

还原后的检查

还原完成后,我们检查了四项内容,以确保迁移正确无误:

  • Schema 模式。新集群中的 Collections Schema 必须与旧的完全一致,包括字段定义和向量维度。

  • 总行数。我们比较了新旧群集中的实体总数,以确保没有数据丢失。

  • 索引状态。我们确认所有索引都已完成构建,且其状态设置为Finished

  • 查询结果。我们在两个群集上运行了相同的查询,并比较了返回的 ID 和距离分数,以确保结果一致。

流量逐步转移和延迟惊喜

我们分阶段将生产流量转移到新集群:

阶段流量份额持续时间我们观察到的情况
第一阶段5%24 小时P99 查询延迟、错误率和结果准确性
第 2 阶段25%48 小时P99/P95 查询延迟、QPS、CPU 使用率
第 3 阶段50%48 小时端到端指标、资源使用情况
第 4 阶段100%持续监控整体指标稳定性

我们一直保持旧群集运行,以便即时回滚。

在回滚过程中,我们发现了问题:新 v2.5.16 集群的搜索延迟比旧 v2.2.16 集群高出 3-5 倍。

查找搜索延迟的原因

第 1 步:检查总体 CPU 使用率

我们从每个组件的 CPU 使用率入手,查看群集是否缺乏资源。

组件CPU 使用率(内核)分析
查询节点10.1上限为 16 个内核,因此使用率约为 63%。未完全使用
代理0.21非常低
混合计算0.11非常低
数据节点0.14非常低
索引节点0.02非常低

这表明 QueryNode 仍有足够的 CPU 可用。因此,速度变慢并不是由于 CPU 整体不足造成的。

第二步:检查查询节点的平衡情况

CPU 总量看起来没有问题,但单个 QueryNode pod 存在明显的不平衡

查询节点 podCPU 使用率(上次)CPU 使用情况(最大值)
查询节点 pod-18.38%9.91%
querynode-pod-25.34%6.85%
喹啉模式-POD-34.37%6.73%
喹啉模式-模块-44.26%5.89%
喹啉-pod-53.39%4.82%
喹啉-pod-63.97%4.56%
喹啉-pod-72.65%4.46%
喹啉-pod-82.01%3.84%
喹啉-pod-93.68%3.69%

Pod-1 使用的 CPU 几乎是 pod-8 的 5 倍。这就是问题所在,因为 Milvus 会将查询分散到所有 QueryNodes,并等待速度最慢的节点完成查询。几个超负荷的 pod 拖累了每一次搜索。

第 3 步:比较分段分布

负载不均通常表明数据分布不均,因此我们比较了新旧集群的数据段布局。

v2.2.16 版数据段布局(共 13 个数据段)

行数范围段数状态
740,000 ~ 745,00012密封
533,6301密封

旧的集群相当均匀。它只有 13 个区段,其中大部分区段约有740 000 行

v2.5.16 版数据段布局(共 21 个数据段)

行数范围段数状态
680,000 ~ 685,0004密封
560,000 ~ 682,5505密封
421,575 ~ 481,8004密封
358,575 ~ 399,7254密封
379,650 ~ 461,7254密封

新的集群看起来很不一样。它有 21 个分段(多了 60%),分段大小各不相同:有些分段有 ~685k 行,其他分段只有 350k。还原后的数据分散不均。

根本原因

我们将问题追溯到最初的还原命令:

./milvus-backup restore --config configs/restore.yaml -n backup_v2216 \
  --rebuild_index \
  --use_v2_restore \
  --drop_exist_collection \
  --drop_exist_index

--use_v2_restore 标志启用了数据段合并还原模式,该模式将多个数据段合并为一个还原任务。该模式的设计目的是在有许多小片段时加快还原速度。

但在我们的跨版本还原(2.2 → 2.5)中,v2 逻辑重建的网段与原始群集不同:它将大网段分割成大小不均的小网段。一旦加载,一些查询节点就会被比其他节点更多的数据卡住。

这从三个方面损害了性能:

  • 热节点:拥有较大或较多数据段的查询节点不得不做更多的工作

  • 最慢节点效应:分布式查询延迟取决于最慢的节点

  • 更多合并开销:更多数据段也意味着合并结果时需要更多工作

修复

我们删除了--use_v2_restore ,并恢复使用默认逻辑:

./milvus-backup restore --config configs/restore.yaml -n backup_v2216

我们首先清理了新集群中的不良数据,然后运行默认还原。分段分布恢复平衡,搜索延迟恢复正常,问题也就解决了。

下次我们会采取的不同措施

在这个案例中,我们花了太长时间才发现真正的问题:数据段分布不均衡。如果能这样做,就能更快地解决问题。

改进网段监控

Milvus 没有在标准的 Grafana 仪表盘中公开每个 Collections 的段计数、行分布或大小分布。我们不得不通过Attu和 etcd 手动挖掘,速度很慢。

如果能增加

  • Grafana 中的段分布仪表盘,显示每个查询节点加载了多少段,以及它们的行数和大小

  • 当节点间的分段行数偏差超过阈值时触发失衡警报

  • 迁移比较视图,以便用户在升级后比较新旧集群的数据段分布情况

使用标准迁移清单

我们检查了行计数,认为没有问题。但这还不够。完整的迁移后验证还应该包括

  • Schema 一致性。字段定义和向量维度是否匹配?

  • 分段数。网段数量是否发生了巨大变化?

  • 分段平衡。各分段的行数是否合理均衡?

  • 索引状态。所有索引都是finished 吗?

  • 延迟基准。P50、P95 和 P99 查询延迟是否与旧群集相似?

  • 负载平衡。QueryNode 的 CPU 使用量是否平均分配给各个 pod?

添加自动检查

您可以使用 PyMilvus 编写此验证脚本,以便在生产中出现不平衡之前及时发现:

from pymilvus import connections, utility, Collection  
def check_segment_balance(collection_name: str):
    """Check Segment distribution balance"""
    collection = Collection(collection_name)
    segments = utility.get_query_segment_info(collection_name)
    # Group statistics by QueryNode
    node_stats = {}
    for seg in segments:
        node_id = seg.nodeID
        if node_id not in node_stats:
            node_stats[node_id] = {"count": 0, "rows": 0}
        node_stats[node_id]["count"] += 1
        node_stats[node_id]["rows"] += seg.num_rows
    # Calculate balance
    row_counts = [v["rows"] for v in node_stats.values()]
    avg_rows = sum(row_counts) / len(row_counts)
    max_deviation = max(abs(r - avg_rows) / avg_rows for r in row_counts)
    print(f"Number of nodes: {len(node_stats)}")
    print(f"Average row count: {avg_rows:.0f}")
    print(f"Maximum deviation: {max_deviation:.2%}")
    if max_deviation > 0.2:  # Raise a warning if deviation exceeds 20%
        print("⚠️ Warning: Segment distribution is unbalanced and may affect query performance!")
    for node_id, stats in sorted(node_stats.items()):
        print(f"  Node {node_id}: {stats['count']} segments, {stats['rows']} rows")

# Usage example connections.connect(host=“localhost”, port=“19530”) check_segment_balance(“your_collection_name”)

更好地使用现有工具

一些工具已经支持分段级诊断:

  • Birdwatcher:可直接读取 Etcd 元数据并显示网段布局和通道分配

  • Milvus Web UI(v2.5+):可让您直观地检查网段信息

  • Grafana + Prometheus:可用于为实时集群监控构建自定义仪表盘

对 Milvus 社区的建议

对 Milvus 做一些改动会让这类故障排除变得更容易:

  1. 更清楚地解释参数兼容性 milvus-backup 文档应清楚地解释--use_v2_restore 等选项在跨版本还原过程中的行为,以及它们可能带来的风险。

  2. 在还原后添加更好的检查在 restore 完成,如果工具能自动打印段分布的摘要,将会很有帮助。

  3. 公开与平衡相关的指标Prometheus指标应包括网段平衡信息,以便用户直接监控。

  4. 支持查询计划分析类似于 MySQLEXPLAIN ,Milvus 将受益于一个能显示查询如何执行并帮助定位性能问题的工具。

结论

总结

阶段工具/方法关键点
备份milvus-backup 创建支持热备份,但必须仔细检查备份
升级使用 Helm 构建新集群禁用 Mmap 以减少 I/O 抖动,并保持与旧群集相同的资源设置
迁移Milvus-backup 还原小心使用 --use_v2_restore。在跨版本还原中,除非明确了解,否则不要使用非默认逻辑
灰色推出逐步转移流量分阶段转移流量:5% → 25% → 50% → 100%,并保留旧群集以备回滚
故障排除Grafana + 网段分析不要只看 CPU 和内存。还要检查分段平衡和数据分布
修复删除坏数据并重新还原删除错误标志,用默认逻辑还原,性能恢复正常

迁移数据时,不仅要考虑数据是否存在、是否准确。您还需要注意数据是如何 分布的

分段数和分段大小决定了 Milvus 如何在各节点间均匀地分配查询工作。如果分段不平衡,少数节点就会承担大部分工作,而每次搜索都会为此付出代价。跨版本升级会带来额外的风险,因为还原过程可能会以不同于原始群集的方式重建数据段。像--use_v2_restore 这样的标记会使数据变得支离破碎,而单靠行数是无法显示出来的。

因此,在跨版本迁移中,最安全的方法是坚持使用默认还原设置,除非有特殊原因。此外,监控范围不应局限于 CPU 和内存;您需要深入了解底层数据布局,尤其是段分布和平衡,以便更早地发现问题。

来自 Milvus 团队的说明

我们要感谢 WPS 工程团队与 Milvus 社区分享这一经验。这样的文章很有价值,因为它们记录了真实的生产经验教训,并使之对面临类似问题的其他人员有所帮助。

如果您的团队有值得分享的技术经验、故障排除故事或实践经验,我们很乐意听取您的意见。请加入我们的Slack 频道并联系我们。

如果您正在解决自己的难题,这些社区频道也是与 Milvus 工程师和其他用户联系的好地方。您还可以通过Milvus Office Hours预约一对一服务,以获得备份和恢复、跨版本升级和查询性能方面的帮助。

    Try Managed Milvus for Free

    Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.

    Get Started

    Like the article? Spread the word

    扩展阅读