当前位置: 首页 > article >正文

Milvus 存储设计揭秘:从数据写入到 Segment 管理的全链路解析

90d8be5a415b5a1a77176228967f6893.png

ef6beeb84449ffa4a234929cdc6151bd.jpeg

作为一款云原生向量数据库,Milvus 的高效查询性能有赖于其独特的存储架构设计。然而,在实际使用过程中,许多社区用户常常会遇到以下问题:

为什么频繁调用 flush 后,查询速度会变慢?

数据删除后,磁盘空间为何依旧无法及时释放?

查询延迟有时为何忽高忽低?

这些现象的背后,通常都与 Milvus 的核心存储单元——Segment 的处理机制密切相关。Segment 是 Milvus 数据持久化的最小单位,其生命周期管理直接影响系统性能和资源利用率。本文将深入剖析 Milvus 从数据写入到 Segment 落盘、合并以及建立索引的完整流程,帮助开发者更好地理解系统行为并有效规避常见误区。

01

数据写入:Insert 请求如何拆解为 Segment?

当 Milvus 服务启动后,会在消息队列(Pulsar/Kafka)中创建若干用于接收数据的 “管道”,其数量根据 milvus.yaml 中 rootCoord.dmlChannelNum 进行配置。对于 Pulsar 或 Kafka,每个“管道”即对应一个 topic。

(1)Insert 请求与数据分片

a. insert 请求由 proxy 接收。proxy 会根据每条数据的 primary key 值计算出 hash,并将该 hash 对 collection 的 shards_num 取模。根据这个结果,数据被划分到相应的 shard 中。

b. 当 shards_num 大于 1 时,insert 数据最多会被分为 shards_num 份,分配到不同的 shard。

(2)数据传输

a. 每个 shard 产生的数据通过某个管道进行传输,多个 shard 也可共享同一条管道,以实现负载均衡。

b. data node 订阅各自对应的 shard 数据,并尽量将不同的 shard 分配到多个 data node 上。如果集群中只有一个 data node,则会由该节点订阅所有 shard 的数据。

(3)Growing Segment

a. 在 data node 端,每个partition中的每个 shard 都会对应一个 growing segment。data node 为每个 growing segment 维护一个缓存,用于存放尚未落盘的数据。

简而言之,proxy 会按照 shards_num 对数据进行分片,并将其发送到消息管道 ,然后data node从消息管道中异步接收数据。

下图展示了在 shards_num=2 的情况下,数据经由管道传输至 data node 的流程示意图:

3e0e1bf06aa8291947ff8084ff2f07ab.png

02

数据落盘:Growing Segment 如何持久化?

在 data node 中,来自各个 shard 的数据会被累积在对应 growing segment 的缓存里。该缓存的最大容量由 milvus.yaml 中 dataNode.segment.insertBufSize 控制,默认值为 16MB。一旦缓存中数据超过此阈值,data node 就会将这部分数据写入 S3/MinIO,形成一个 Chunk。Chunk 指的是 segment 中的一小段数据,并且每个字段的数据也会被分别写入独立的文件。因此,每个 Chunk 通常包含多个文件。

此外,dataNode.segment.syncPeriod(默认 600 秒)定义了数据在缓存中允许停留的最长时间。如果在 10 分钟内缓存数据尚未达到 insertBufSize 的上限,data node 同样会将缓存写入 S3/MinIO

将 segment 数据拆分为多个 Chunk 进行持久化,主要是为了:

(1)减少 data node 内存占用,避免growing segment数据在内存中堆积过多;

(2)当系统发生故障后重启时,无需从消息队列重新拉取growing segment的全部数据,已持久化的数据只需从S3/MinIO读取。

d8aec2a5ba27583f122547ee4eaf5079.png

当某个 growing segment 累计写入量达到一定阈值后,data node 会将其转换为 sealed segment,并新建一个新的 growing segment 来继续接收 shard 数据。这个阈值由 dataCoord.segment.maxSize(默认 1024MB)和 dataCoord.segment.sealProportion(默认 0.12)共同决定,也就是说,当 growing segment 大小达到 1024MB × 0.12(约 122MB)时,就会被转为 sealed segment。

在此过程中,如果用户通过 Milvus SDK 调用 flush 接口,则会强制将指定 Collection 的所有 growing segment 缓存落盘并转为 sealed segment,无论它们已写入多少数据。因此,频繁调用 flush() 容易产生大量体量较小的 sealed segment,进而影响查询性能。

下图展示了 growing segment 落盘和转换为 sealed segment 的示意流程:

bc890877b9c91f35b9028ff3360ea647.png

如果 Collection 存在多个 partition,则每个 partition 的数据也会被相同数量的 shard 分割。下图示例展示了多个 Collection 共用 2 条管道,其中某个 Collection 有 2 个 partition 时的数据分发过程:

884738102344bb492764eedf9434e6f8.png

数据在 S3/MinIO 中的存储路径由 milvus.yaml 中 minio.bucketName(默认值 a-bucket)以及 minio.rootPath(默认值 files)共同决定。segment 数据的完整路径格式为:

[minio.bucketName]/[minio.rootPath]/insert_log/[collection ID]/[partition ID]/[segment ID]

在 segment 路径下,Milvus 还会根据每个字段的 ID 创建对应子目录来存放该字段的各个 Chunk 文件。下面是某个 segment 在 MinIO 中的存储结构示例,可见 455457303288873052 表示 collection ID,455457303288873053 表示 partition ID,455457303289273082 表示 segment ID,而 0/1/100/101/102 则分别代表该 Collection 的各个字段 ID。该 segment 仅包含一个 Chunk,因此每个字段目录下只有一个数据文件:

72f28a4741ebc54639024ac95c6c2d42.png

03

Segment 合并优化:Compaction 的三种场景

在持续执行 insert 请求时,新数据不断流入,sealed segment 的数量也会随之增加。如果同样规模的数据被拆分成过多小尺寸(如小于 100MB)的 segment,系统的元数据管理和查询都会受到影响。为了优化这一点,data node 会通过 compaction 将若干较小的 sealed segment 合并成更大的 sealed segment。理想状态下,合并后形成的 segment 大小会尽量接近 dataCoord.segment.maxSize(默认 1GB)。

不过,小文件合并仅仅是 compaction 任务的一部分。compaction 还包括其他需求场景,主要分为以下三种:

(1)小文件合并(系统自动)

a.触发条件:存在多个体积较小的 sealed segment,其总大小接近 1GB。

b.优化效果:减少元数据开销,提升批量查询的性能。

(2)删除数据清理(系统自动)

a.触发条件:segment 中的被删除数据占比 ≥ dataCoord.compaction.single.ratio.threshold(默认 20%)。

b.优化效果:释放存储空间,并减少无效数据的重复扫描。

3.按聚类键(Clustering Key)重组(手动触发)

a.使用场景:面向特定查询模式(例如地域或时间范围检索)优化数据分布。

b.调用方式:通过 SDK 调用 compact(),并按照指定的 Clustering Key 对 Segment 进行重组。

04

索引构建:临时索引 vs 持久化索引

对于每个 growing segment,query node 会在内存中为其建立临时索引,这些临时索引并不会持久化。同理,当 query node 加载未建立索引的 sealed segment 时,也会创建临时索引。关于临时索引的相关配置,可在 milvus.yaml 中通过 queryNode.segcore.interimIndex 进行调整。

当 data coordinator 监测到新的 sealed segment 生成后,会指示 index node 为其构建并持久化索引。然而,如果该 sealed segment 的数据量小于 indexCoord.segment.minSegmentNumRowsToEnableIndex(默认 1024 行),index node 将不会为其创建索引。

所有索引数据都被保存在以下路径:

 
 
[minio.bucketName]/[minio.rootPath]/index_files

下图展示了在 MinIO 中某个 sealed segment 的索引存储结构。路径中的 455457303289273598 代表了 index node 的任务 ID,用于唯一标识该索引文件;其中的 1 则是索引版本号(index version)。还可见 partition ID(455457303288873053)和 segment ID(455457303289273082)的信息,这有助于排障和定位。而最底层名为 IVF_FLAT 的目录中,才是真正的索引文件:

6a0907a7a2c4aa47deddeb2fb9780892.png

05

Segment 加载:Query Node 如何管理数据?

当用户调用 load_collection 时:

(1)Query node 会从 S3/MinIO 加载该 Collection 的所有 sealed segment,并订阅相应的 shard 流数据。

(2)系统力求将多个 shard 分配给不同的 query node 以实现负载均衡;如果只有一个 query node,则该节点订阅全部 shard。

(3)对于每个 shard,query node 会同样在内存中维护一个对应的 growing segment,保证与 data node 上的数据保持一致。如果growing segment的部分数据已经以chunk的形式持久化在S3/MinIO中,query node会从S3/MinIO读取这些chunk,未持久化的数据则从Pulsar/Kafka中订阅,从而在内存中组成完整的growing segment。由于query node从Pulsar/Kafka中订阅growing segment的数据是异步行为,当有新的数据插入时,这些新数据对于查询请求不是即时可见,因此用户可在查询请求中设置 consistency level 以控制新数据的可见性。

a35475d32291261bcca4f32c8d322b7e.png

(4)对于已构建好持久化索引的 sealed segment,query node 会从 S3/MinIO 中加载其索引文件。若某个 sealed segment 尚无索引或是 growing segment,query node 则会在内存中创建临时索引(详见第四节)。

当 compaction 任务将多个 sealed segment 合并为新的 sealed segment 后,query node 只有在新 segment 的索引构建完成后,才会加载其索引,并将旧 segment 的索引与数据从内存中清理掉,这个过程被称为 hand-off。

06

常见问题和 Tips

基于以上对 Milvus 存储与 Segment 处理原理的介绍,以下列出一些常见的注意事项与实用建议,帮助大家更好地使用 Milvus 提升系统性能:

(1)避免频繁调用 flush()

a.每次调用 flush() ,有可能会在每个 partition 的每个 shard 上都产生一个 sealed segment,频繁调用极易产生大量碎片化的小 segment。

b.建议:优先依赖自动落盘机制,只有当用户在插入全量数据后想确认数据全部落盘时(主要用于性能测试时避免用到 temp index),才有必要手动调用一次 flush 操作。

(2)监控 Segment 数量

a.可通过 Milvus 的 GUI 工具 attu 查看各 Segment 的分布状况,小 segment 较多时可能会影响查询性能,无需做额外操作,系统的compaction 机制会在必要时进行合并。

b.如果将 segment_size(默认 1GB)调大,可减少 segment 数量,提升索引查询效率,但同时要注意对系统负载均衡和内存的影响。

c.shards_num 的值直接影响 growing segment 数量,可根据数据量设置合理的 shards_num 来打散写入热点。一般来说,百万行级别数据量建议设置 shards_num=1,千万行级别数据量建议设置 shards_num=2,亿级别以上数据量建议设置 shards_num=4 或 8。

d.partition 的数量也直接影响 growing segment 的数量,此外大量的 partition 也会消耗系统资源导致系统性能下降,因此需要根据业务需求设置合理的 partition 数量。

(3)合理规划 Clustering Key

针对常见的范围查询、批量数据删除等业务场景设计合适的 Clustering Key,可进一步提升 compaction 效率并优化查询表现。

通过理解 Segment 的生命周期管理,开发者可以更高效地设计数据写入策略、调优系统参数,让 Milvus 在复杂场景下依然保持稳定的高性能表现。

如对以上案例感兴趣,或有更多实战经验或疑问,想对Milvus做进一步了解,欢迎扫描文末二维码交流进步。

作者介绍

fe5da1c3ce31f2bbd78c7d9d58e5dfc1.jpeg

张粲宇

Zilliz 高级产品经理

推荐阅读

659d0be2f07af30409c0038132bc9339.png

d8074db65b69dcbfc3a48f1721b0f0c6.png9373d153e5a470f371ce17759980f7c6.png


http://www.kler.cn/a/538023.html

相关文章:

  • Swift的方法派发机制
  • 从零到一:我的元宵灯谜小程序诞生记
  • QTreeView和QTableView单元格添加超链接
  • Render上后端部署Springboot + 前端Vue 问题及解决方案汇总
  • LIMO:上海交大的工作 “少即是多” LLM 推理
  • IDEA编写SpringBoot项目时使用Lombok报错“找不到符号”的原因和解决
  • 深入浅出Log4j2:从入门到实战应用指南
  • C语言基础系列【6】流程控制
  • 快速建立私有化知识库(私有化训练DeepSeek,通过ollama方式)
  • python 使用OpenAI Whisper进行显卡推理语音翻译
  • 探秘树莓集团海南战略:文创领军者的市场破局之路
  • 【Go语言快速上手】第二部分:Go语言进阶
  • opencv打开摄像头出现读取帧错误问题
  • 原子核链式反应与曼哈顿计划
  • 【docker】Failed to allocate manager object, freezing:兼容兼容 cgroup v1 和 v2
  • Django+simpleui实现文件上传预览功能
  • Unity-Mirror网络框架-从入门到精通之Discovery示例
  • LabVIEW污水生化处理在线监测
  • 【Pandas】pandas Series var
  • 线程状态:
  • ##__VA_ARGS__有什么作用
  • Java并发篇
  • Deepseek得两种访问方式与本地部署
  • 【0403】Postgres内核 检查(procArray )给定 db 是否有其他 backend process 正在运行
  • 车机音频参数下发流程
  • H2模拟mysql的存储过程