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

作为一款云原生向量数据库,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 的流程示意图:
2.6-1.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读取。
2.6-2.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 的示意流程:
2.6-3.png
如果 Collection 存在多个 partition,则每个 partition 的数据也会被相同数量的 shard 分割。下图示例展示了多个 Collection 共用 2 条管道,其中某个 Collection 有 2 个 partition 时的数据分发过程:
2.6-4.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,因此每个字段目录下只有一个数据文件:
2.6-5.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 的目录中,才是真正的索引文件:
2.6-6.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 以控制新数据的可见性。
2.6-7.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 在复杂场景下依然保持稳定的高性能表现。
技术干货
一次解决三大成本问题,升级后的 Zilliz Cloud 如何造福 AIGC 开发者?
对于应用开发而言,成本问题向来是企业和开发者关注的重点,更迭迅速、变化莫测的 AIGC 时代更是如此。这里的成本既指软件开发成本,也包括硬件成本、维护成本。Zilliz Cloud 可以一次性解决这三大问题,帮助开发者降低开发成本、优化硬件成本、减少维护成本。
2023-7-6技术干货
Zilliz Cloud 明星级功能详解|解锁多组织与角色管理功能,让你的权限管理更简单!
Zilliz Cloud 云服务是一套高效、高度可扩展的向量检索解决方案。近期,我们发布了 Zilliz Cloud 新版本,在 Zilliz Cloud 向量数据库中增添了许多新功能。其中,用户呼声最高的新功能便是组织与角色的功能,它可以极大简化团队及权限管理流程。
2023-6-28技术干货
我决定给 ChatGPT 做个缓存层 >>> Hello GPTCache
我们从自己的开源项目 Milvus 和一顿没有任何目的午饭中分别获得了灵感,做出了 OSSChat、GPTCache。在这个过程中,我们也在不断接受「从 0 到 1」的考验。作为茫茫 AI 领域开发者和探索者中的一员,我很愿意与诸位分享这背后的故事、逻辑和设计思考,希望大家能避坑避雷、有所收获。
2023-4-14