资讯详情

Milvus存储链路深度解析:Segment、Binlog与Compaction的因果与调优

📅 2026/11/2 19:56:02 | 华诺云谱 👁 阅读
Milvus存储链路深度解析:Segment、Binlog与Compaction的因果与调优
向量数据库这几年从“新鲜玩意”变成了很多系统的标配组件但大部分人对它的认知还停留在“建个集合、写几条向量、搜一下”的层面。真到了线上出问题——查询突然变慢、数据写入后查不到、磁盘空间莫名暴涨、扩容之后性能反而下降——就开始抓瞎。这些现象背后几乎都指向同一个东西数据在存储层到底是怎么组织的。Milvus 作为这一领域里架构比较完整的一套系统它的存储链路从 Segment 到 Binlog 的设计恰好是理解“向量数据怎么落地”的最佳样本。这篇内容就是把这套流程掰开揉碎讲清楚数据写进来之后经历了什么、为什么要有这么多层、每一层解决什么问题、哪些参数值得调、哪些坑我实际踩过。适合已经用过 Milvus 但对其内部机制一知半解的同学也适合准备系统设计类面试、想把存储链路讲明白的读者。读完你至少能做到两件事面试时能把 Segment、Binlog、 compaction 这条线讲出因果而不是背名词线上遇到写入或查询的异常时知道该往哪一层去查。1. 先搞清楚 Milvus 存储链路要解决的核心矛盾在讲具体组件之前得先明白这套架构是被什么“逼”出来的。向量检索有个很别扭的特性写入和查询对数据组织方式的要求是相反的。写入希望越快越好最好是追加写、顺序落盘查询希望数据越紧凑越好最好是全局有序、一次扫描就能出结果。这两个诉求天然打架任何存储系统都得在中间做权衡Milvus 的选择是“分层异步”用多级结构把矛盾拆开消化。1.1 为什么不能像传统数据库那样一行行存传统关系型数据库按行存储一条记录的所有字段挨在一起点查很快。但向量检索是“批量相似度计算”你要拿一个查询向量去和成千上万条向量算距离如果向量散落在磁盘各处光是寻道就把时间耗光了。所以向量数据必须按列连续存放一批向量拼成一个大块计算时能整块读进内存做 SIMD 加速。这就是列式存储思路在向量场景的延伸也是后面所有 Segment、Binlog 设计的出发点——一切为了“批量连续读”。但列式存储又带来新问题单条写入会变成“往大块里插一条”代价很高。于是系统必须先把写入缓冲起来攒够一批再合并成大块。这个“攒”的过程就是 MemTable 和 Growing Segment 存在的意义。你把这个逻辑想通了后面看到的各种“段”就不会觉得是凭空冒出来的名词。1.2 写入放大与查询放大的跷跷板任何存储系统都在两个放大之间走钢丝。写入放大指你写一条数据底层实际写了多少份查询放大指你查一次底层实际扫了多少无用数据。Milvus 的整个存储链路本质就是在动态调节这两个放大系数。刚写入的数据放在内存里的小段Growing Segment查询时也要扫它但因为它小查询放大可控等它攒大、落盘、合并成 Sealed Segment查询效率高了但合并过程本身产生写入放大。系统通过后台的 compaction 不断把小段合成大段就是在用“后台的写入放大”换“前台的查询效率”。理解了这个跷跷板你调参时就不会盲目——想让写入快就容忍更多小段想让查询快就得接受后台合并的 IO 开销。这个权衡贯穿全文后面每一节我都会回到这个视角。1.3 一套存储链路要回答的四个问题把矛盾拆开之后Milvus 的存储设计其实要回答四个具体问题这也是我理解这套架构的骨架问题对应机制关键组件数据写进来先放哪内存缓冲 预写日志MemTable / WAL缓冲满了怎么落盘刷盘成不可变段Sealed Segment落盘后怎么保证可恢复操作日志持久化Binlog段太多怎么收敛后台合并Compaction这四个问题串起来就是一条完整链路。下面我按“数据从进入到能被高效查询”的顺序逐层拆。注意我不会按教科书顺序平铺而是按“你实际会遇到的现象”来组织这样更容易建立直觉。2. Segment向量数据落地的基本单位Segment 是 Milvus 存储里最核心的概念没有之一。你可以把它理解成“一批向量的集装箱”——数据不会一条条散着存而是打包成段。但段分两种状态这个区分是很多人第一次看文档会忽略、但实际最关键的点。2.1 Growing Segment 与 Sealed Segment 的本质区别Growing Segment 是“还在长个儿”的段数据先写进内存同时记一份日志保证不丢。它可读可写查询时会和已落盘的段一起被扫描。Sealed Segment 是“封箱”的段一旦封存就不可变只读。为什么非要不可变因为不可变才能做索引、才能被多个查询安全共享、才能放心地做缓存。可变的数据结构没法高效建索引这是数据库领域的通用经验。我见过有人问“为什么不能直接往 Sealed Segment 里追加”答案就在这一旦允许追加索引就得频繁重建查询时还得处理并发读写复杂度爆炸。用“封箱”换“简单和高效”是这套设计的核心取舍。实际使用中你会看到集合的段数量随着写入增长然后被后台慢慢合并这个动态过程就是两种段在互相转化。2.2 一个 Segment 内部到底装了什么一个 Sealed Segment 不是只存向量。它内部按列组织通常包含几类数据向量列本身、主键列、时间戳列、以及各种标量字段列。每列单独存成一个文件这就是前面说的列式布局。查询时如果只做向量检索不带标量过滤就只读向量列带了过滤条件再按需读标量列。这种“按需读列”的能力是列存相对行存的一大优势能显著降低查询放大。这里有个实操细节值得说标量字段的列文件会配合索引比如倒排、位图过滤时先走索引定位到候选行再回向量列算距离。所以如果你的过滤条件字段没建索引查询会退化成全段扫描慢得离谱。这一点在调优时经常被忽略——大家盯着向量索引参数却忘了标量过滤这条路径。2.3 段大小对性能的实际影响段不是越大越好也不是越小越好。段太小段数量爆炸每次查询要打开一堆文件、合并一堆结果元数据开销和调度开销都上来了段太大单次 compaction 耗时长、内存占用高而且查询时的并行度上不去。实践中段大小通常和内存、并行度挂钩经验值是在“能放进内存做批量计算”和“数量不至于太多”之间取平衡。我个人的体会是段大小要和你的查询并发量一起考虑。高并发场景下段数量多一点、每个段小一点反而有利于多线程并行扫描低并发、追求单次查询延迟的场景段大一点、数量少一点更划算。这个没有标准答案得压测。后面讲 compaction 时会再展开这个权衡。3. Binlog不只是日志是数据的持久化载体很多人一听 Binlog 就联想到“数据库的二进制日志”以为只是个辅助的恢复工具。在 Milvus 里Binlog 的地位比这高得多——它是数据真正落盘后的存储形式Segment 的每一列数据最终都以 Binlog 文件的形式存在对象存储或本地盘上。理解这一点你对“数据在哪”的认知才不会错位。3.1 Binlog 的三种类型与各自职责Milvus 的 Binlog 大致分三类职责清晰Insert Binlog记录插入的向量和标量数据是数据的主体。Delete Binlog记录删除操作。注意删除在向量库里通常是“标记删除”不是物理立刻抹掉因为物理删除会破坏不可变段的结构。Delta Binlog / 统计类 Binlog记录段级别的元信息变化比如哪些行被删了、段的状态变更等。这个分类的意义在于插入和删除走不同的持久化路径。插入是追加写效率高删除是打标记等 compaction 时再真正清理。如果你不理解这点就会困惑“为什么删了数据磁盘没变小”——因为物理回收要等合并这是设计使然不是 bug。3.2 为什么 Binlog 要按列拆文件前面说 Segment 内部按列存落到 Binlog 就是“一列一个文件”。这么做的好处很直接查询时按需加载减少 IO。但代价是文件数量多元数据管理压力大。所以系统会维护一份“段到 Binlog 文件”的映射清单查询时先查清单再定位文件。这份清单本身的持久化和一致性就是元数据服务要保证的事。实操中如果你发现对象存储上的文件数量异常多、小文件扎堆往往就是段太碎、Binlog 文件太散导致的。这时候要么调大刷盘阈值让段大一点要么靠 compaction 收敛。小文件问题在对象存储上尤其要命因为每次列目录、每次打开文件都有额外延迟。3.3 从写入到 Binlog 落盘的完整时序把时序走一遍链路就清楚了。一条数据写入时先进内存缓冲MemTable同时写预写日志保证崩溃可恢复缓冲攒到阈值触发刷盘把内存里的数据按列写成 Binlog 文件写完 Binlog这个段就从 Growing 转为 Sealed元数据服务更新段状态之后查询就能看到这个段。整个过程是异步的写入方拿到“成功”响应时数据可能还在内存里但因为有预写日志不会丢。注意这里的“成功”语义是“已持久化到日志”不是“已进入可查询的 Sealed 段”。如果你写完立刻用强一致读去查可能查不到刚写的数据这是正常的最终一致行为。需要强一致时得用对应的读一致性级别别把“写成功”和“立即可查”混为一谈。4. Compaction段收敛背后的取舍艺术段会越写越多查询要扫的段越来越多性能必然下降。Compaction 就是后台的“收纳工”把小段合并成大段、清理被标记删除的数据、回收空间。但这个过程不是免费的它消耗 IO 和 CPU还会和前台查询抢资源。怎么合、什么时候合是这套系统里最考验调优功力的地方。4.1 合并策略什么时候该合、合到什么程度常见的合并思路有两种一种是把多个小段合成一个大段类似 LSM 树的层级合并另一种是专门清理删除标记的合并。前者解决“段太多”后者解决“删除没回收”。实际系统里两者往往结合按段的数量、大小分布、删除比例来决定触发时机。我踩过的一个坑是合并阈值设得太激进后台一直在合并前台查询被 IO 拖慢。后来把触发条件放宽让段多攒一会儿再合前台反而稳了。这说明合并不是越勤越好得看你的负载类型。写多读少的场景可以懒一点读多写少的场景要勤一点保证查询时段数量可控。4.2 合并期间的查询一致性怎么保证合并过程中旧段还在被查询引用新段正在生成。系统不能让查询读到“半成品”所以通常的做法是新段完全生成并持久化后才原子地切换元数据指向新段旧段等没有查询引用后再回收。这个“写时复制原子切换”的模式是保证一致性的关键。理解这点对排查问题很有用。如果你在合并期间看到查询结果短暂波动或者磁盘占用先涨后降新旧段短暂共存都是正常现象。真正要警惕的是合并卡住不动——那往往是元数据服务或存储层出了问题得去看后台任务的状态。4.3 删除数据后磁盘不降反升的真相这是被问得最多的现象之一。原因就是前面说的删除是打标记物理数据还在旧段里合并时要把旧段读出来、剔除被删的行、写成新段这个过程中新旧段同时存在磁盘占用会短暂上升合并完旧段回收后才下降。所以“删了数据磁盘先涨后降”是正常链路不是异常。如果你的场景对空间回收很敏感可以关注删除比例触发的合并策略让系统在删除积累到一定比例时优先做清理型合并。但别指望“删完立刻降”那和不可变段的设计是冲突的。5. 把链路串起来一次写入与一次查询的完整旅程前面分层讲完这一节把镜头拉远用两个完整流程把知识点串成线。我建议你读这部分时对照前面的组件在脑子里画一遍数据流向比死记概念有效得多。5.1 一次写入从进入到可查的全过程写入请求进来先做校验和路由找到对应的集合和分区数据进内存缓冲同时追加预写日志缓冲达到阈值或超时触发刷盘按列写成 BinlogBinlog 持久化成功后段状态从 Growing 切到 Sealed元数据更新后台 compaction 视情况把小段合并查询侧通过元数据拿到最新的段列表开始扫描。整条链路里预写日志是可靠性底线Binlog 是持久化载体元数据是调度中枢三者缺一不可。这里有个容易忽略的点刷盘是批量触发的不是每条都刷。所以写入吞吐高的时候你会看到段在短时间内快速生成又快速被封存然后 compaction 忙起来。这个节奏是正常的别看到段数量短时飙升就慌。5.2 一次查询如何决定扫哪些段查询进来先解析向量和过滤条件元数据服务返回当前所有可查的段包括 Growing 和 Sealed查询调度器把段分给多个执行线程并行扫描每个段内部先走标量索引过滤再算向量距离结果汇总、排序、返回。这里的关键是段级并行——段越多理论上并行度越高但调度和汇总开销也越大。所以段数量要控制在合理区间这就是前面反复强调段大小和合并策略的原因。如果你的查询突然变慢排查顺序建议是先看段数量是不是暴涨了再看标量过滤字段有没有索引最后看是不是 compaction 在抢 IO。这个顺序能覆盖大部分常见问题比盲目调向量索引参数有效。6. 实操中真正值得调的参数与避坑清单理论讲完落到能动手的层面。这一节列的都是我在实际使用中反复验证过的点不是文档照搬。6.1 刷盘与段大小相关的关键配置刷盘阈值决定了 Growing Segment 多大时落盘。设太小段碎、文件多、合并压力大设太大内存占用高、崩溃恢复慢要重放更多日志。经验做法是结合可用内存和写入速率来定保证单个 Growing Segment 不会把内存吃穿同时又不至于频繁刷盘。这个值没有万能数字得按你的数据量和内存规格压测。另一个是段的目标大小。它影响合并的粒度和查询并行度。我一般会先按默认值跑观察段数量分布和查询延迟再微调。别一上来就大改参数先建立基线再针对性调整这是调优的基本纪律。6.2 索引与存储的联动注意事项向量索引不是独立存在的它依附于 Sealed Segment。所以索引构建的时机、索引文件的大小都和段的大小强相关。段太大建索引慢、内存峰值高段太小索引文件碎、加载开销大。标量索引同理过滤字段没索引会拖垮查询。还有一个常被忽略的点索引和原始向量是分开存的查询时可能既要读索引又要读原始数据做重排。所以存储层的 IO 能力直接影响查询延迟别只盯着索引参数底层盘的吞吐也得跟上。6.3 我踩过的三个典型坑第一个坑以为写完就能立刻查到结果用了默认一致性级别读到了旧数据排查半天以为是丢数据。后来搞清一致性级别和段可见性的关系才明白这是语义问题不是 bug。第二个坑删除大量数据后磁盘不降慌了以为删除失效。实际是等 compaction 回收观察一段时间后正常下降。第三个坑扩容后性能反而下降原因是新节点上段分布不均查询调度没打散。后来靠 rebalance 和调整段分布解决。这三个坑的共同点是都是对存储链路理解不到位导致的误判而不是系统真有问题。把链路吃透这类“假故障”能省掉大量排查时间。7. 面试视角怎么把这条链路讲出因果而不是背名词最后聊点实用的。很多人面试被问“Milvus 存储怎么设计的”就开始背 Segment、Binlog、Compaction 的定义面试官一听就知道是背的。真正加分的讲法是讲因果链先说写入和查询的矛盾再说分层怎么化解矛盾然后说每一层为什么必须存在最后说代价是什么、怎么权衡。比如你可以这样组织向量检索要求批量连续读所以数据要按列打包成段但写入是流式的所以先缓冲再刷盘刷盘后段不可变才能建索引、才能安全共享段多了查询放大所以要有后台合并合并有写入放大和 IO 争抢的代价所以要调策略。这条线讲下来每个组件都是“被问题逼出来的”而不是“设计者拍脑袋加的”。面试官要的就是这种把设计还原成决策过程的能力。再补一句个人体会能把代价讲清楚比能把功能讲清楚更能体现深度。任何设计都有取舍只讲好处不讲代价反而显得没真正用过。你如果能说出“合并阈值调激进的副作用”“删除不立刻回收空间的原因”基本就能让面试官相信你是真上手过的。这套链路我前后梳理过好几遍每次线上遇到怪现象回头再看都能对上号。存储这东西概念看着多但底层逻辑就那么几条不可变换效率、异步换吞吐、合并换查询、日志换可靠。把这四条记住再复杂的现象也能找到落点。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑