尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Milvus向量数据库存储链路全解析:从写入到查询的异步分层设计
向量数据库这几年从“新鲜玩意”变成了很多系统的标配组件Milvus 作为其中讨论度最高的一个几乎成了面试里绕不开的话题。但大多数人聊到它能说出来的无非是“存向量、建索引、做相似度检索”这几句一旦被追问“一条数据从写入到能被搜到中间到底经历了什么”就开始含糊其辞。我面过不少人也被人面过发现真正能把 Segment、Binlog、Collection、Partition 这些概念串成一条完整链路讲清楚的人其实不多。这篇内容就是想把这条链路彻底摊开讲透——从数据进来的那一刻起它被怎么接收、怎么缓冲、怎么落盘、怎么建索引、怎么被查询命中每一步背后的设计取舍是什么。适合正在准备向量数据库相关岗位面试的朋友也适合已经在用 Milvus 但只停留在调 API 层面、想往下再挖一层的开发者。看完之后你至少能做到别人问“为什么写入不是立刻可查”你能从内存缓冲和持久化的角度讲明白别人问“删除的数据去哪了”你不会只答一句“删了”。这些细节恰恰是区分“用过”和“懂”的分水岭。1. 先把整体链路在脑子里搭起来在钻进任何一个组件之前我习惯先把整条数据通路画成一个粗线条的流程这样后面每讲一个细节你都知道它挂在哪一环上。Milvus 的存储设计本质上是在解决一个矛盾写入要快查询也要快而这两件事对数据组织方式的要求是相反的。写入希望数据尽量待在内存里、顺序追加、少做随机操作查询希望数据已经排好序、建好索引、能快速定位。Milvus 的解法是把这两件事拆开用一条异步的、分层的流水线把写入和查询解耦。1.1 一条数据从进入到可查的完整旅程一条向量数据进入 Milvus 之后大致会经过这么几个阶段。客户端发起插入请求请求先到协调节点协调节点负责分配全局唯一的时间戳、把数据路由到对应的分片然后写进消息队列。写消息队列这一步是整条链路的起点它保证了数据的顺序性和可恢复性。接着负责消费的写入节点从消息队列里拉取数据在内存里组织成一个个小的数据块这些内存中的数据块就是后面要重点讲的 Segment 的雏形。当内存里的数据积累到一定量或者达到某个时间阈值写入节点会把这些数据刷到对象存储里形成持久化的段文件。与此同时这些段的元信息会被登记到元数据服务里。查询侧则根据元数据知道有哪些段可用再结合索引情况决定怎么检索。这条链路里有两个关键词贯穿始终异步和分层。异步意味着写入成功和可查询之间不是同一时刻发生的中间隔着消息队列和刷盘分层意味着数据在不同阶段有不同的物理形态内存里是一份刷盘后是另一份建索引后又是一份。理解了这两个词后面所有的“为什么”都能找到落脚点。1.2 为什么写入成功不等于立刻能查到这是面试里出现频率极高的一道题也是很多人第一次用 Milvus 时最困惑的地方明明插入接口返回成功了为什么紧接着查询却查不到答案就藏在上面那条链路里。插入接口返回成功只代表数据已经被可靠地写进了消息队列它还没有被消费、没有形成可查询的段、更没有建索引。从“进了队列”到“能被搜到”中间还有消费、刷盘、加载这几个步骤。这个设计不是偷懒而是刻意的取舍。如果每次写入都同步等待落盘和建索引写入吞吐会被彻底拖垮因为建索引是重操作。用异步换吞吐是几乎所有分布式存储系统的共同选择。所以你在实际使用中如果业务对“写后立即读”有强需求要么在应用层做补偿写完主动触发一次 flush 再查要么接受这个延迟窗口。面试时如果能主动点出“这是吞吐和实时性的权衡不是缺陷”印象分会高不少。2. Segment数据组织的最小单元Segment 是 Milvus 存储体系里最核心的概念没有之一。你可以把它理解成“一批数据的集合”但这批数据不是随便凑的它有明确的生长、封存、合并的生命周期。把 Segment 吃透等于把 Milvus 存储的半条命脉握在手里。2.1 Segment 的三种形态与生命周期Segment 并不是一成不变的它会随着数据量和时间推移在几种形态之间转换。刚写入时数据在内存里形成一个可增长段这个阶段它还在不断接收新数据体积小、变动频繁。当它增长到一定规模比如达到配置的行数上限或内存阈值就会被封存变成一个封存段不再接受新写入等待后续处理。封存之后数据会被刷到对象存储形成持久化的段文件此时它已经可以被查询但可能还没有索引走的是暴力扫描。再往后后台的合并任务会把多个小的封存段合并成更大的段减少段的数量提升查询效率。这个生命周期设计的关键在于“分级”。小段灵活但数量多查询时要遍历很多段大段查询效率高但合并成本大。系统在两者之间动态平衡既不让段无限膨胀导致合并困难也不让段太碎导致查询开销爆炸。实际调优时段的大小和合并策略是影响性能的重要旋钮后面会细说。2.2 内存段与持久化段的差异内存段和持久化段最直观的差异是“在不在内存里”但真正的差异在于它们承担的角色不同。内存段是写入的缓冲区它的存在是为了让写入不必每次都碰磁盘攒一批再统一处理。持久化段是查询的数据源它躺在对象存储里查询时按需加载。两者之间的转换点就是前面说的“刷盘”。这里有个容易被忽略的细节刷盘不是把内存段原封不动搬到磁盘而是把内存里的数据结构序列化成列式存储格式。为什么用列式因为向量检索和标量过滤往往只涉及部分字段列式存储能只读需要的列减少 IO。这个转换过程是有成本的所以刷盘频率不能太高否则写入性能受损也不能太低否则内存占用和故障恢复时间都会变长。这个平衡点是运维时要盯紧的参数。2.3 段合并到底在合并什么段合并Compaction这个词听起来抽象说白了就是把多个小文件整理成少数大文件。为什么要做这件事因为查询时要遍历所有相关的段段越多遍历开销越大元数据管理的负担也越重。合并之后段数量减少查询路径变短同时还能顺带清理掉被删除的数据。合并分两种一种是小段合并成大段纯粹为了减少数量另一种是带清理的合并把标记删除的数据真正物理删除。前者是常规操作后者通常在删除量积累到一定程度后触发。合并本身是后台异步进行的不阻塞读写但它会消耗 IO 和 CPU所以合并策略要结合业务负载来调。我见过一些场景写入量很大但合并配置保守结果段数量失控查询越来越慢最后不得不手动干预。这个坑后面会展开讲。3. Binlog被低估的可靠性基石很多人对 Binlog 的印象停留在“数据库的日志”觉得它跟查询性能没关系就懒得深究。但在 Milvus 里Binlog 是连接写入和持久化的关键一环理解它你才能回答“数据刷盘之前万一节点挂了怎么办”这类问题。3.1 Binlog 在 Milvus 里到底存了什么Milvus 的 Binlog 不是传统数据库那种记录“某行改了什么”的逻辑日志它更接近一种物理的、按列组织的变更记录。每次刷盘时内存段里的数据会按列拆开分别写成不同的 Binlog 文件——向量列一个文件标量列各自一个文件。这种按列拆分的方式和前面说的列式存储是一脉相承的目的都是让查询时能精准读取需要的列。除了数据本身Binlog 还承载了插入、删除这些操作的记录。删除在 Milvus 里不是立刻抹掉数据而是写一条删除标记查询时通过标记过滤掉。这些标记同样以 Binlog 的形式持久化。所以 Binlog 既是数据的载体也是操作的账本两者合在一起才能完整还原一个段的状态。3.2 从消息队列到 Binlog 的落盘路径数据从消息队列被消费后在内存里攒成段刷盘时写成 Binlog。这条路径上有几个关键点值得注意。第一刷盘是由写入节点触发的触发条件可以是段大小达到阈值也可以是时间到了。第二刷盘写的是对象存储不是本地磁盘这意味着它依赖对象存储的可用性和延迟。第三Binlog 写完之后元数据服务会登记这个段的信息查询侧才能感知到它的存在。这条路径的可靠性设计在于“先写日志再确认”。数据进消息队列就算可靠接收消费和刷盘即使中途失败也能从队列里重新消费不会丢数据。这就是为什么插入接口敢在数据还没落盘时就返回成功——它赌的是消息队列的持久性。理解这一点你就能明白 Milvus 的可靠性边界在哪里消息队列稳数据就稳。3.3 为什么 Binlog 采用列式组织列式组织的好处用一个生活化的类比就清楚了。假设你有一张表记录了很多人的姓名、年龄和照片。如果按行存查“所有人的年龄”要把每行的姓名和照片也读出来浪费如果按列存年龄单独一列读的时候只碰这一列干净利落。向量检索场景里查询往往只涉及向量列和少数标量过滤列列式存储能大幅减少无效 IO。这个选择和 Milvus 的整体定位是一致的它是为向量检索优化的系统不是通用关系库。通用库追求读写均衡向量库更偏向“写一次、查多次”的读密集型负载列式存储正好契合这个特点。代价是写入时的转换成本以及点查单条记录时不如行式方便但权衡下来对目标场景是划算的。4. 索引构建从原始段到可高速检索数据刷成 Binlog 只是“存下来了”要“查得快”还得靠索引。索引构建是 Milvus 里资源消耗最大的环节之一也是最容易出问题的地方。4.1 索引不是在写入时同步建的这一点必须反复强调索引构建是异步的和写入解耦。数据刷盘形成段之后会进入一个待建索引的队列由专门的索引节点消费并构建。构建完成后索引文件同样存到对象存储元数据更新查询时才会用上索引。在索引建好之前查询走的是暴力扫描能查到但慢。为什么不同步建因为建索引是 CPU 和内存密集型操作如果每次写入都同步建写入吞吐会断崖式下跌。异步建索引让写入和索引互不干扰写入只管快速落盘索引在后台慢慢追。这个设计的代价是“新数据有一段时间查得慢”但对大多数场景来说这个窗口可以接受。如果你的业务对新鲜数据的查询延迟极其敏感就得考虑调整刷盘和建索引的触发频率缩小这个窗口。4.2 不同索引类型对存储的影响Milvus 支持多种向量索引不同索引对存储的占用和构建开销差异很大。以常见的几类为例有的索引构建快、内存占用小但召回率略低有的索引为了高召回需要构建复杂的图结构内存和构建时间都成倍增加。选型时不能只看查询性能还要看存储成本和构建资源。索引类型构建开销内存占用查询特点适用场景扁平索引极低低暴力扫描精度最高但慢小数据集、精度要求极高倒排类索引中等中等聚类加速速度快中等规模、追求速度图类索引高高高召回、低延迟大规模、高召回要求这张表只是粗略对比实际选型还要结合数据规模、维度、查询 QPS 和硬件条件。我个人的经验是数据量在百万级以下扁平索引配合好的硬件往往够用不必一上来就上图类索引否则构建和内存成本会让你肉疼。4.3 索引与段的绑定关系索引是绑在段上的一个段一份索引。这意味着段合并之后原来的索引会失效需要对新合并出的大段重新建索引。这个细节很多人不知道但它解释了一个现象为什么大规模合并之后查询性能会短暂波动。因为合并产生的新段还没建好索引这段时间查询走的是扫描。理解这层绑定关系对运维很有帮助。比如你在做批量导入时可以刻意控制导入节奏避免触发大规模合并导致索引重建集中爆发。又比如你发现查询突然变慢可以查一下是不是刚发生了合并、索引还在重建。这些排查思路比干瞪眼强得多。5. 查询侧如何找到数据前面讲的都是“数据怎么存进去”现在换个视角看“查询怎么把数据捞出来”。这一侧的逻辑是把元数据、段、索引三者串起来。5.1 查询计划是怎么生成的一个查询请求进来协调节点先做的是“找目标”。它查元数据知道当前有哪些集合、哪些分区、哪些段再结合查询条件里的过滤字段排除掉不可能命中的段。比如你按某个标量字段过滤而某个段在这个字段上的取值范围完全不匹配这个段就直接跳过不用扫。这一步叫段裁剪是查询优化的第一道关卡。裁剪之后剩下的段按是否建好索引分类。有索引的走索引检索没索引的走暴力扫描。然后各个段的检索结果汇总、排序、取 TopK返回给客户端。整个过程是并行的多个段同时查最后归并。理解这个流程你就能明白为什么“段太多”会拖慢查询——不是单个段慢而是并行调度的开销和结果归并的成本上去了。5.2 删除标记在查询时怎么生效前面提到删除是写标记而不是物理删除那查询时这些标记怎么起作用答案是在结果归并阶段做过滤。每个段在检索时会同时读取该段的删除标记集合命中的结果如果落在标记集合里就被剔除。这个过滤是在内存里做的对用户透明。这个设计的好处是删除操作极快只写一条标记不用动原始数据。代价是删除的数据仍然占着存储空间直到合并时才真正清理。所以如果你的业务删除很频繁要关注合并策略否则存储会被“已删除但未清理”的数据撑大。这也是面试里可以主动展开的一个点能体现你对存储成本的理解。5.3 多段结果的归并与排序查询命中多个段时每个段各自返回一批候选结果这些结果需要在协调节点归并。归并的核心是按相似度分数排序取全局 TopK。听起来简单但段数量多的时候归并本身也是开销。这就是为什么前面反复强调要控制段数量——不仅影响单段检索还影响归并效率。归并还有一个细节不同段可能用了不同的索引类型比如老段用了某种索引新段还没建索引走扫描它们的分数计算方式要保证一致否则归并出来的排序会乱。Milvus 在分数归一化上做了处理保证跨段可比。这个点比较深面试时如果能提到“跨段分数一致性”会显得你确实钻进去过。6. 实操中绕不开的配置与调优理论讲完落到实操。这一节挑几个最影响实际表现的配置项讲清楚它们调什么、怎么调、调错了会怎样。6.1 段大小与刷盘阈值的取舍段大小和刷盘阈值是绑在一起的一对参数。段大小上限决定了内存段能攒多大刷盘阈值决定了多久刷一次。这两个值调大写入吞吐上去、段数量减少但内存占用增加、故障恢复变慢调小则相反。没有标准答案要看你的写入速率和内存预算。我的经验做法是先估算单条数据的平均大小乘以预期的每秒写入量算出每秒产生的数据量再结合可用内存反推一个合理的段大小。比如每秒写入 10MB希望内存段最多占 1GB那大概 100 秒刷一次段大小控制在 1GB 左右。这只是起点实际还要压测微调。调的时候一次只动一个参数观察段数量、查询延迟、内存曲线三个指标别一把梭。6.2 合并策略怎么配才不踩坑合并策略的核心是“什么时候触发合并、合并多大的段”。配得太激进合并频繁IO 和 CPU 被吃掉影响正常读写配得太保守段数量失控查询变慢。常见的坑是只关注写入性能把合并阈值设得很高结果运行一段时间后段数量爆炸。一个实用的思路是给段数量设一个软上限比如单个集合的段数量超过某个值就主动触发合并。同时监控合并任务的积压情况如果积压持续增长说明合并能力跟不上数据增长要么加资源要么调整策略。另外批量导入这种场景可以临时放宽合并策略导入完再收紧避免导入过程中合并和写入抢资源。6.3 索引构建资源怎么预留索引构建是资源大户尤其是图类索引构建时内存占用可能是数据本身的好几倍。如果索引节点和查询节点混部构建时可能把查询节点的资源挤占导致查询抖动。稳妥的做法是索引构建和查询服务在资源上做隔离或者至少给索引构建设资源上限。还有一个容易被忽略的点索引构建是分批的多个段可能同时触发构建。如果并发构建数不设限内存瞬间被打满节点可能被拖垮。所以要限制并发构建的数量让构建排队进行。这个参数在数据量大、段多的时候尤其重要我见过因为没限制并发导致节点频繁重启的案例。7. 常见问题与排查实录这一节整理几个实际运维中高频遇到的问题附上排查思路。这些问题在面试里也常被拿来考察“你有没有真正踩过坑”。7.1 写入成功但查询不到数据这是最高频的问题前面已经从原理上解释过。排查时按这个顺序走先确认数据是否真的进了消息队列看写入节点的消费延迟再确认是否已经刷盘看段的生成情况最后确认索引是否建好没建好也能查只是慢但如果连段都没加载就查不到。多数情况是刷盘还没发生等一会儿或者手动触发 flush 就能解决。注意手动 flush 是重操作生产环境不要频繁调用只在必要时用。频繁 flush 会产生大量小段反而拖慢查询。7.2 查询延迟突然升高延迟突增的排查要分场景。如果是刚做完批量导入或大规模合并大概率是索引重建导致的等索引建完会恢复。如果是持续升高看段数量是不是失控了段太多会导致查询要遍历和归并的段变多。还有一种可能是某个查询的过滤条件太宽段裁剪没起作用扫了全量数据。排查时先看监控里的段数量曲线和索引构建队列长度基本能定位大半。7.3 内存占用居高不下内存问题通常来自三处内存段攒得太大没刷盘、索引构建占用、查询时的缓存。先看刷盘是否正常如果写入速率高但刷盘跟不上内存段会持续膨胀。再看索引构建是否并发过多。最后看查询缓存配置是否过大。定位到具体来源后针对性调参别盲目加内存加内存只是推迟问题爆发。现象可能原因排查动作处理方式写入后查不到未刷盘或未加载查消费延迟、段生成等待或手动 flush查询延迟升高段过多或索引重建看段数量、索引队列调合并策略、等索引完成内存居高不下刷盘滞后或构建并发高看刷盘速率、构建并发调刷盘阈值、限并发存储持续增长删除未清理看删除标记量、合并情况触发带清理的合并7.4 段数量失控的应急处理段数量失控是慢性病爆发时查询会明显变慢。应急处理可以先手动触发一次合并把大量小段合成大段快速降低段数量。但这只是止血根治要回到合并策略的配置上把触发条件调得更积极一些。同时检查写入模式如果是持续的小批量高频写入本身就容易产生大量小段可以考虑在应用层做攒批减少写入次数。8. 面试里怎么把这条链路讲出彩最后聊点应试的东西。前面这些内容面试时不要一股脑倒出来要有节奏地展开。被问到存储相关的问题时先给一个粗线条的链路写入到队列、消费成段、刷盘成 Binlog、异步建索引、查询走裁剪加归并让对方知道你有全局观然后再根据追问往深处钻。几个能加分的表达技巧讲异步的时候主动点出“这是吞吐和实时性的权衡”讲删除的时候主动提“标记删除加合并清理是空间换时间的思路”讲索引的时候主动说“索引绑段合并后要重建这是性能波动的来源”。这些主动的“为什么”比被动回答问题更能体现深度。还有一点别把话说满。比如被问到某个参数的最优值不要给一个绝对数字而是说“取决于写入速率和内存预算我的经验是先估算再压测”。这种回答既专业又稳妥比背一个数字强。面试官想听的不是标准答案而是你的思考过程。我个人在实际操作中的体会是Milvus 这套存储设计最值得琢磨的地方是它把“写”和“查”用异步和分层彻底解耦每个组件只做一件事然后靠消息队列和元数据把它们串起来。你把这个解耦思想吃透不光 Milvus很多分布式存储系统的设计你都能一通百通。至于那些参数和配置都是在这个思想下的具体落地理解了思想参数调起来才有方向而不是瞎试。
RELATED

相关推荐

轻量级H264解码库:纯C实现、无malloc、适配裸机与RTOS

轻量级H264解码库:纯C实现、无malloc、适配裸机与RTOS

简介:本资源是一套面向C# WinForm开发者的技术实践包,聚焦海思H264解码库在Windows平台的集成与调用,专为视频监控、嵌入式视频回放等实时解码场景提供可落地的C#互操作方案。资源共9个文件,涵盖2个核心PDF文档(含SDK开…

📅 2026/11/2 16:37:11
聚合搜索OneSearch:深度学习小白的资料检索自救指南

聚合搜索OneSearch:深度学习小白的资料检索自救指南

1. 学深度学习最不缺资料,缺的是一个把资料"捞起来"的入口先说个挺反直觉的事:我写深度学习小白笔记写到了第92篇,卡我时间最久的从来不是什么Transformer的attention公式、不是梯度消失的原理推导,而是"搜不到对的…

📅 2026/11/2 16:37:11
四层平台五层MVC:ASP.NET Core业务系统架构实践

四层平台五层MVC:ASP.NET Core业务系统架构实践

1. 四层平台的五层MVC:从概念到落地做.NET开发这些年,我最怕听到的不是“需求又变了”,而是“你先搭个框架”。因为框架本身没有标准答案,不同团队对同样一句话的理解可能差出十万八千里。前几天有个朋友问我,想用VS里…

📅 2026/11/2 16:37:11
MORE NEWS

更多资讯

📰

老旧货梯智能化改造:协议转换与声明式梯控架构设计

1. 老旧货梯智能化改造的行业痛点 在工业物联网领域,设备改造最棘手的往往不是全新系统的搭建,而是对存量老旧设备的智能化升级。以货梯为例,全国制造业园区中超过60%的货梯设备使用年限超过15年,这些设备普遍存在三大典型问题&am…

📰

四边形不等式优化DP:原理与实现详解

1. 四边形不等式优化DP概述 动态规划(DP)是算法竞赛中的核心技巧,而四边形不等式优化则是提升DP效率的重要方法。这种优化技术利用决策单调性,将原本O(n)的复杂度降低到O(nlogn),在处理大规模数据时优势显著。 四边形不等式优化的核心思想是…

📰

Codex + Jev 组合实战:类型安全与 Skill 机制配置指南

1. 这套组合到底在解决什么问题 先把话说在前头:Codex 本身是个能力很强的代码智能体,但它的默认配置和默认模型,在真实项目里经常会出现“能跑但不够顺手”的情况。尤其是当你需要它处理复杂类型推导、跨文件重构、或者需要它理解一套自定义…

📰

Vue Router滚动行为控制与优化实践

1. 为什么需要路由跳转后的滚动控制?在单页应用(SPA)开发中,Vue Router 的路由跳转不会像传统页面跳转那样自动重置滚动位置。这会导致两个常见问题:从长页面跳转到新页面时,新页面会继承前一个页面的滚动位…

📰

ABAP协变性(Covariance)解析与Java对比实践

1. 为什么ABAP开发者需要关注协变性(Covariance)在面向对象编程领域,协变性(Covariance)是一个经常被讨论但容易被忽视的概念。作为一名有十年ABAP开发经验的工程师,我发现很多同行对这个问题存在误解——要么完全忽视,要么简单套用Java等语言…

📰

ZXing扫码优化实战:从40%失败率降至5%的图像预处理全链路

简介:本资源是一套基于ZXing开源库实现二维码与条形码扫描功能的完整Android项目实践方案,面向Android开发初学者及中级工程师,解决移动应用中快速集成扫码能力的核心需求。压缩包共1874个文件,涵盖254个JSON配置与接口定义、158个…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬