首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Milvus 3.0 删完空间没回来?空间是排队释放的:先压缩,再 GC,最后才轮到配置

Milvus 3.0 删完空间没回来?空间是排队释放的:先压缩,再 GC,最后才轮到配置

原创
作者头像
术哥
发布于 2026-10-20 22:35:16
发布于 2026-10-20 22:35:16
650
举报
文章被收录于专栏:运维有术运维有术

🚩 2026 年「术哥无界」系列实战文档 X 篇原创计划 第 210 篇,Milvus 最佳实战「2026」系列第 35 篇

大家好,欢迎来到 术哥无界 | ShugeX | 运维有术。

我是术哥,一名专注于 AI 编程、AI 智能体、Agent Skills、MCP、云原生、AIOps、Milvus 向量数据库的技术实践者与开源布道者!

Talk is cheap, let's explore。无界探索,有术而行。

封面
封面

如果你用 Milvus 按主键删过数据,很可能撞见过这样一对组合:删除请求返回成功,delete_count 也给了一串数字——但刚删完,那条数据还能搜到;半小时后看磁盘,空间一分没动。

先分清两个问题:一是"删了还能查到",二是"空间没回来"。前者是查询侧的事,后者是存储侧的事。它们背后是同一条链——删除从返回成功到真正离开磁盘,中间经过的每个环节,各有各的时机。这篇就沿着这条链走一遍,两个现象会在各自的环节上对上号。

01 调用返回成功时,实际发生了什么

墓碑声明
墓碑声明

从 proxy 这一侧看,delete 请求只做一件事:把你要删的主键和时间戳打包成一条删除消息,追加进 WAL(预写日志)。消息写进去,请求就返回成功。数据文件、索引、段,什么都没碰。

消息长什么样决定了它的性质。proxy 会把主键按哈希路由到各个虚拟通道,每个主键带上一个时间戳,打包上限受 quotaAndLimits.limits.maxDeleteSize 约束,默认 16 MiB。这条配置的官方注释原文:“maximum protobuf size of each Proxy-side materialized delete message, in bytes, for both delete and upsert tombstones; -1 means no limit…”——删除消息在官方配置语义里就叫 tombstone,墓碑。

墓碑不是尸体:它只是“这里应该有个东西消失”的声明,消失本身另有其时,排在链条后面。

还有一个容易被当成确认回执的数字:delete_count。简单删除路径(表达式是 pk == a 或 pk in [a, b])返回的就是表达式里主键的个数;复杂条件删除返回的是查询命中的主键数。

删一个数据里根本不存在的主键,delete_count 照样计数:它统计的是你提交了多少张墓碑,跟系统确认删掉了多少条数据没有关系。

后面的机制能解释为什么这两者注定对不上:真正“删掉一条”的证据要到压实阶段才出现,而 compactor 里有一个专门的指标叫 MissingDeleteCount,统计的就是“墓碑找不到对应行”——悬空的墓碑多到需要单独计数。

02 复合条件删除,是先查一遍再删

这是我走查源码时第一个被绊住的地方。

proxy 解析删除表达式后分两条路:主键等值和主键枚举走轻量路径,直接打包墓碑;其余所有条件表达式(按某个标量字段过滤、按时间范围清理)走的是 complexDelete,它的做法是把删除条件序列化成一个查询计划,发给查询节点完整执行一遍,把命中的主键流式查回来,再对这些主键写墓碑。

proxy 的代码注释甚至专门提醒:复杂计划序列化后可能达到几十 MiB,为了不在每个并发分片回调里重复这份内存开销,要先整体序列化一次再分发。

在大集合上用一个宽泛的过滤条件做批量清理,代价不是一次元数据操作,而是一次覆盖命中范围的查询,同时这个动作还占用删除限流配额。

评估一次清理操作的代价,先看表达式会走哪条路。

03 查询为什么看不到已删数据

位图门
位图门

墓碑进了 WAL 之后,查询节点的流水线把它消费下来,按时间顺序分批写进每个数据段内部的一个内存结构。这个结构(segcore 里的 DeletedRecord)的形态非常直白:一个按时间戳排序的 (时间戳, 行号) 跳表,加一个位图。查询执行时,位图被“或”进结果过滤:位图里置 1 的行号,直接从候选结果里挡掉。数据本体还在段里躺着,索引也照常为它服务,只是最后一步结果组装时被位图拦下。

这也解释了删除请求里那个时间戳的用途:查询时间戳不是你手动传的时间参数,而是按这条查询请求的一致性级别解析出的 GuaranteeTs 水位;墓碑的时间戳早于这个水位才生效,晚于它的照常可见。查询侧的“删了还能查到”几乎都是水位还没推进到删除那一刻,那套可见性机制第 04 篇展开过,但判断不依赖它:等几秒重查即可。

有一个时序保护的细节顺带值得知道:删除时间戳不晚于插入时间戳的墓碑会被直接丢弃。这是给同主键先删后写的并发场景留的护栏。

04 墓碑自己也会变成“数据”

如果说前三环都在预期内,这一环是我没料到的:删除记录不会一直躺在日志里,它会被物化成一个正式的 segment。

WAL 里的删除流按批物化成 Level 标记为 L0 的段,源码默认阈值是单批最多 500,000 行或 32 MiB。L0 段是数据段体系里的一等公民:参与故障恢复(恢复时它的最早位置还会成为删除检查点)、按分区和通道组织、有完整的生命周期。它在存储上和普通 segment 的唯一区别是内容:里面装的全是墓碑。

这就是“这个集合我只删不写,容量怎么还在涨”的答案:涨的是删除记录自己。

按墓碑的形态估算(时间戳加主键),一条墓碑在消息层面只有几十字节,但乘上高频删除的业务,再算上 WAL 保留和物化副本,这笔账就不再是零。

05 压实:被删的行在这里才真正消失

两步压实
两步压实

物理消除分两步。

第一步是 L0 compaction:后台每 10 秒轮询一次(默认配置),把积攒的 L0 段拿出来,用各个已封存段的布隆过滤器把墓碑路由到对应的数据段,为每个段重写一份删除日志。注意这一步仍然没有碰数据本体,它只是把散落的墓碑按“该找谁”归档。

真正把行从存储里抹掉的是第二步 mix compaction:每 60 秒触发一次(默认配置),合并段时先读出该段全部墓碑组成删除集合,然后逐行扫描数据,命中删除集合的行不写进新文件,顺带清掉超过集合 TTL 的过期数据。到这里,L0 压实和 mix 压实这两道钟都走完了,空间仍然一分没回来。这属于正常走时:留给空间回归的时钟在下一环的 GC 那里,还没开始走。

被删的行没有被当场擦掉,它们留在旧文件里。旧文件整体标记为 dropped,从此不再被任何新版本引用。

到这里,那条数据在逻辑上已经彻底消失,查询永远不会再看到它。但在磁盘上,它的字节还完好地躺在那个 dropped 文件里。

06 最后一环:GC 的两道时钟

GC 时钟
GC 时钟

dropped 文件由数据协调节点的垃圾回收清理,而 GC 刻意地慢。两道默认容忍窗:dropped 段的文件要等 3 小时(dataCoord.gc.dropTolerance 默认 10800 秒)才会被清;元数据里查不到记录的游离文件要等 24 小时(missingTolerance 默认 86400 秒)。容忍窗的存在是防御性的:防止误删还在创建中或还在被引用的文件,代价是删除后的空间回归以小时计。

还有一道不在配置里的坎:社区有一个仍然开放的 issue(#49585)指出,数据协调节点的四个 GC 子任务在同一个 goroutine 里顺序执行,当 dropped 段积压很大时,段回收会饿死索引回收,生产环境观察到约一半(55%)的索引文件成了段-索引孤儿——所属段已被回收,索引文件本身还留在存储里。

如果你的集群经历过大规模删除,“空间迟迟不回来”还多了一个与配置无关的成因:GC 在排队。

删完数据半小时看磁盘没动静,在这套机制里是时钟还没走到,不需要干预;过了容忍窗以天计仍不回归,再回头查 GC 排队和段积压。

07 分环排障:现象落在哪一环

把前面四个环节折成一张判断表,遇到“删除出了问题”时先定位环节:

  • 删了还能查到:可见性环。水位是查询按自己的一致性级别定出的一个时间戳,删除时间更早的墓碑只有在这个水位推进过删除时刻之后才生效——刚删完就查到,多半是水位还没走到,隔几秒重查一次就能分辨“没生效”和“没删掉”(完整机制见第 04 篇)。若重查后仍然长期可见,检查该主键是否真的提交过墓碑,注意 delete_count 不回答这个问题——它就是 §01 里那个只数提交了多少条墓碑声明的计数器。
  • 查不到了,但空间不降:压实与回收环。按顺序核对:集合的自动压实开关是否打开(默认开)、删除发生是否还不满三道默认时钟(L0 压实 10 秒、mix 压实 60 秒、GC 3 小时)、dropped 段是否大量积压(GC 排队问题)。
  • 只删不写,容量却持续上涨:删除记录自己在占用存储,确认 L0 段的物化和压实是否正常发生,而不是去数据里找“没删掉”的东西。

代价边界就是这张表的另一面:删除轻时是一条几十字节的墓碑消息,重时是一次先查后删的查询加一份持续增长的写入负载,外加以小时计的空间时钟。给数据清理排期时,把这四个环节连各自的默认时钟一起算进去,别只盯着 delete 的返回值。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 01 调用返回成功时,实际发生了什么
  • 02 复合条件删除,是先查一遍再删
  • 03 查询为什么看不到已删数据
  • 04 墓碑自己也会变成“数据”
  • 05 压实:被删的行在这里才真正消失
  • 06 最后一环:GC 的两道时钟
  • 07 分环排障:现象落在哪一环
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档