首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Milvus 删了 100 万条数据磁盘没缩?11 种 CompactionType 拆解删除回收机制

Milvus 删了 100 万条数据磁盘没缩?11 种 CompactionType 拆解删除回收机制

原创
作者头像
术哥
发布2026-06-21 12:46:46
发布2026-06-21 12:46:46
560
举报
文章被收录于专栏:运维有术运维有术

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

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

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

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

封面图
封面图

图 1:Milvus Compaction 的反差感——一个 API 背后藏着 7 套机制

如果你在 Milvus 里删过 100 万条向量,然后发现磁盘空间几乎没变,多半不是 bug。是数据真的还没从磁盘上抹掉。

Milvus 官方 2022 年的一篇 Blog 给过定义:compaction 是合并小段 + 清理逻辑删除的过程。听起来就一个动作。但翻一遍源码就会发现,这个动作在 pkg/proto/data_coord.proto 里被拆成了 11 个枚举值,背后挂着 6 套独立触发策略3 种优先级排序器多层互斥规则

对用户来说,调一个 compact() API 就完事了;对开发者来说,整套机制跑了 7 条互不相同的回收路径。

为什么非得拆成这样?这篇文章把源码翻开来看。

说明:本文基于 Milvus 源码(github.com/milvus-io/milvusdata_coord 包)和官方文档 v3.0.x 分析整理,所有结论均可追溯至源码引用或官方 PR/Release Notes。源码分析基于笔者本地仓库版本,尚未在生产环境中完成全场景验证。文中的配置模板和参数建议仅供参考,实际效果请以你的业务数据和环境测试结果为准。如果有实际使用经验,欢迎在评论区分享交流。

1. 11 个枚举值,编号 1 还消失了

源码位置:pkg/proto/data_coord.proto:670-684

代码语言:protobuf
复制
enum CompactionType {
  MergeCompaction = 2;
  MixCompaction = 3;
  SingleCompaction = 4;
  MinorCompaction = 5;
  MajorCompaction = 6;
  Level0DeleteCompaction = 7;
  ClusteringCompaction = 8;
  SortCompaction = 9;
  PartitionKeySortCompaction = 10;
  ClusteringPartitionKeySortCompaction = 11;
  BumpSchemaVersionCompaction = 12;
}

两个细节会先跳出来。

一个是编号 1 缺失01 都没有对应类型,明显是早期 UndefinedCompaction 被废弃后留下的演进痕迹。Milvus 的 protobuf 文件从不删旧枚举值,因为旧版本的元数据还在用。

另一个更有意思:1 到 6 号全是历史类型MergeCompactionSingleCompactionMinorCompactionMajorCompaction 在新版的触发器代码里已经找不到调用方了,源码注释里还能看到一行 todo: migrate to compaction_trigger_v2

真正活着的只有 5 种

  • Level0DeleteCompaction(L0 删除回收)
  • MixCompaction(小段合并 + 删除清理)
  • SortCompaction(单段排序重写)
  • ClusteringCompaction(基于聚类键重新分布)
  • BumpSchemaVersionCompaction(schema 演进时的段重写)

这里就有第一个反常识:为什么不直接做成一个 compaction + 一堆配置开关?答案藏在每种类型背后的数据病里 - 它们差得太远,硬塞到一个执行路径里,触发条件写不清楚,优先级也调不明白。

2. 6 个独立 policy:每种类型对应一种数据病

新版触发器(compaction_trigger_v2.go)注册了 6 个独立的 policy,每个挂着自己的 ticker 和触发间隔:

Policy

Ticker

触发间隔

输出 CompactionType

l0CompactionPolicy

L0Ticker

L0CompactionTriggerInterval

Level0DeleteCompaction

singleCompactionPolicy

SingleTicker

MixCompactionTriggerInterval

MixCompaction + SortCompaction

clusteringCompactionPolicy

ClusteringTicker

ClusteringCompactionTriggerInterval

ClusteringCompaction

storageVersionUpgradePolicy

StorageVersionTicker

MixCompactionTriggerInterval

MixCompaction

bumpSchemaVersionPolicy

BumpSchemaVersionTicker

BumpSchemaVersionCompactionTriggerInterval

BumpSchemaVersionCompaction

forceMergeCompactionPolicy

仅手动触发

MixCompaction

每个 policy 都实现 Enable() / Trigger() / Name() 三个方法,独立判断自己负责的那种数据病是否到了该治的时候。

6 个 policy 与对应数据病
6 个 policy 与对应数据病

图 2:6 套 policy 各管一种数据病——删除累积、小段碎片、聚类失衡、schema 落后、存储版本旧、用户手动合并

关键路由表在 trigger_v2.go:67

代码语言:go
复制
case TriggerTypeLevelZeroViewChange, TriggerTypeLevelZeroViewIDLE, TriggerTypeLevelZeroViewManual:
    return datapb.CompactionType_Level0DeleteCompaction
case TriggerTypeSegmentSizeViewChange, TriggerTypeSingle, TriggerTypeForceMerge:
    return datapb.CompactionType_MixCompaction
case TriggerTypeClustering:
    return datapb.CompactionType_ClusteringCompaction
case TriggerTypeSort:
    return datapb.CompactionType_SortCompaction
case TriggerTypeStorageVersionUpgrade:
    return datapb.CompactionType_MixCompaction  // 共用 mix 执行路径
case TriggerTypeBumpSchemaVersion:
    return datapb.CompactionType_BumpSchemaVersionCompaction

注意一个细节:StorageVersionUpgrade 没有自己的执行路径,直接复用 MixCompaction 的执行器。Milvus 团队的设计原则很清楚 - 触发条件要拆细,执行能复用就复用

每种类型背后是一种性质完全不同的数据病:

  • L0 Delete:删除日志一直堆在 L0 段里,不回收就会拖垮查询
  • Mix:写入产生的小段太多,加载和查询都要扫一堆段
  • Sort:段内的向量分布乱,影响统计任务效率
  • Clustering:实体在段之间分布不均,必须基于标量字段重新聚类
  • StorageVersionUpgrade:存储格式升级后旧段要重写到新版本(比如 V3 的 manifest LOB 存储)
  • BumpSchemaVersion:collection 改了 schema(新增字段、删除字段),旧段要重写才能让 schema 真正生效

把这六种病塞进一个 compaction,意味着每次触发都得检查六套条件,互斥规则会爆炸。拆开之后,每个 policy 独立判断,独立调度。

3. L0 Delete:那条删除回收专线

6 种类型里,最值得拆开看的是 Level0DeleteCompaction。它是专门负责把删除从磁盘上抹掉的机制,其他类型本质上都是在不同维度上重写 segment。

要理解 L0 为什么特殊,先得理解 Milvus 的段分级:

  • L0 段:只存 deltalog(删除日志),不存 binlog(数据)
  • L1 段:普通数据段
  • L2 段:经过 sort compaction 排序后的段

删除请求到来时,删除记录先写到当前 channel 的 L0 段,查询时再 apply 到 L1/L2。这种设计的好处是写入路径不被删除阻塞 - 删除只是往 L0 里追加一条日志,极快。代价是 L0 会持续累积,必须定期回收。

回收这件事,看似简单:把 L0 里的删除日志合并到对应的 L1/L2 段里,然后丢掉 L0。实际上 Milvus 给了它整整 7 处特殊待遇,下面挑 4 个最硬核的工程取舍来讲。

优先级:永远排在最前面

源码位置:compaction_queue.go。三种 prioritizer:

代码语言:go
复制
LevelPrioritizer:    L0Delete(1) < Mix/BumpSchema(10) < Clustering(100) < 其他(1000)
MixFirstPrioritizer: Mix/BumpSchema(1) < L0Delete(10) < Clustering(100) < 其他(1000)
DefaultPrioritizer:  按 PlanID 排序(FIFO)

通过 Params.DataCoordCfg.CompactionTaskPrioritizer 切换(level / mix / 默认)。默认就是 LevelPrioritizer,L0 永远排在最前面。官方 configure_datacoord.md 文档里也写得明白:level 模式下 L0 compactions first, then mix compactions, then clustering compactions

理由很直接:删除不回收,deltalog 会一直累积,bloom filter 膨胀,查询延迟跟着飙升,写入也会被阻塞。Mix 和 Clustering 慢一点只是查询慢一点,L0 慢了是连锁灾难。

Fast Finish:找不到目标段就直接丢

这是 PR #47154 引入的快路径,源码在 compaction_task_l0.go:100

代码语言:go
复制
// 如果 plan 里只有 L0 段(没有可选的 L1/L2 目标段),直接走 fast finish
if len(plan.SegmentBinlogs) == len(t.GetTaskProto().GetInputSegments()) {
    log.Info("l0CompactionTask fast finish: no target segments, directly marking L0 segments as dropped")
    // 保存空输出,把 L0 段标记为 dropped,跳过 DataNode 调用
    if err = t.saveSegmentMeta([]*datapb.CompactionSegment{}); err != nil { ... }
    // 直接进入 meta_saved 状态
    if err = t.updateAndSaveTaskMeta(setState(datapb.CompactionTaskState_meta_saved)); err != nil { ... }
    return
}

触发场景很具体:L0 段的删除日志影响的所有 L1/L2 段都已经被 drop 了(比如 collection 删了大量段)。这种情况下,不需要把删除 apply 到任何目标段,直接丢弃 L0 即可。省了一次 DataNode 调用,避免了无效 IO。

Milvus 2.6.16 的 release notes 也把这个修复正式收编了:Improved L0 compaction to fast-finish when no matching L1/L2 segments are found(PR #49376)。

Self-heal:手动开关救场僵尸 L0

这个最有故事感。PR #48907 的标题是 fix: self-heal compaction segment positions and add L0 force-select bypass。姊妹 PR #48910 的官方描述把问题讲得很直白:

Compaction inherits StartPosition/DmlPosition from source segments via getMinPosition without recalculating from actual data. The import position bug (PR #47276) wrote wrong timestamps on imported segments, and these wrong positions persist and propagate through compaction. L0 compaction then misses L1/L2 segments due to StartPosition mismatches, causing zombie L0 segments and silent delete loss.

翻译一下:import 写错了时间戳,导致 L0 永远找不到对应的 L1/L2 段,删除日志变成僵尸段。后果是用户以为删了,其实没删 - silent delete loss,静默删除丢失。

修复策略是一个运维开关 - LevelZeroCompactionForceSelectAll。开启后强制让所有 L1/L2 段都参与 L0 compaction,绕过位置校验。源码在 compaction_l0_view.go:20

代码语言:go
复制
// resolveLatestDeletePos 在 LevelZeroCompactionForceSelectAll 启用时,
// 返回 MaxUint64 时间戳,让所有 L1/L2 段都通过 startPos < taskPos 过滤
// ——用于从 import position bug 引起的错误 StartPosition 元数据中恢复
func resolveLatestDeletePos(latestL0DmlPos *msgpb.MsgPosition) *msgpb.MsgPosition {
    if paramtable.Get().DataCoordCfg.LevelZeroCompactionForceSelectAll.GetAsBool() {
        return &msgpb.MsgPosition{
            ChannelName: channel,
            Timestamp:   math.MaxUint64,
        }
    }
    return latestL0DmlPos
}

平时关闭,出问题的时候手动开起来做一次性修复。注意这里还有一个被顺便修掉的潜伏 bug:mix/clustering compaction 里 DmlPosition 用的是 getMinPosition,但 DmlPosition 表示的是当前实体时间戳,应该用 max。官方在 PR 描述里明确承认了这是个 latent bug。

版本兼容:升级时不杀旧 L0 任务

源码在 compaction_task_meta.go:96-101

代码语言:go
复制
// Compatibility handling: for milvus ≤v2.4, since compaction task has no PreAllocatedSegmentIDs field,
// here we just mark the task as failed and wait for the compaction trigger to generate a new one.
//
// NOTE:
// - Only compaction tasks that require pre-allocated segment IDs should be marked as failed
// - Level0DeleteCompaction tasks never use PreAllocatedSegmentIDs and must be ignored here,
//   otherwise unfinished L0 delete compaction tasks created before upgrade will be
//   incorrectly marked as failed on reload.
if !isCompactionTaskFinished(task) &&
    task.PreAllocatedSegmentIDs == nil &&
    task.GetType() != datapb.CompactionType_Level0DeleteCompaction {
    task.State = datapb.CompactionTaskState_failed
    task.FailReason = "PreAllocatedSegmentIDs is nil"
}

读这段注释能感觉到一种很谨慎的工程态度:升级前未完成的 clustering/mix 任务可以标 failed 重来,但 L0 任务不行 - 因为 L0 管的是删除数据,中断一次就可能丢日志。所以宁可让它继续跑完,也不能在升级时被杀掉。

这种取舍贯穿了整个 L0 设计:宁可多跑一次也不能丢

4. Snapshot 豁免:删除回收不能停

如果说前 4 处特殊待遇是在讲 L0 怎么被保护,那 snapshot 豁免讲的是 L0 不能被别人挡住

Milvus 3.0 引入了 snapshot 功能(PR #47669、#48227、#49052)。一旦某个 segment 被 snapshot 引用,它就不能被 compaction 改动 - 否则备份就会失效。这套保护接口有两个:

代码语言:go
复制
// 段级保护:被 snapshot 引用的段禁止 compaction
func (m *meta) isSegmentCompactionProtected(segmentID int64) bool

// collection 级阻塞:保护 snapshot 的 RefIndex 未加载完时,整个 collection 禁止 compaction
// fail-closed 设计
func (m *meta) isCollectionCompactionBlocked(collectionID int64) bool

所有 v2 policy 在筛选段时都会调用这两个接口 - 源码里能 grep 到 19 处调用。

但 L0 是个例外。源码位置 meta.go:2930,函数 GetCompactableSegmentGroupByCollection 的注释写得非常直接:

代码语言:go
复制
// GetCompactableSegmentGroupByCollection returns sealed segments grouped by collection.
// This consumed exclusively by the L0 compaction policy, which only acts on L0 segments.
// Snapshot compaction protection targets L1/L2 segments referenced by snapshots, so it must
// NOT filter segments here: doing so would prevent L0 delete-log compaction and cause
// delta log accumulation, query latency spikes, and write stalls on collections with
// active snapshots.

取舍逻辑是这样的:如果 L0 也被 snapshot 保护,那么有活跃 snapshot 的 collection 上,所有删除日志都不能回收。后果链官方注释写得清清楚楚:delta log 累积 → bloom filter 膨胀 → 查询延迟激增 → 写入停滞

所以最终设计是:snapshot 期间允许 L0 回收(只动 L0 段,不动被引用的 L1/L2)。L0 段本身不存数据,只存删除日志,丢掉它不影响 snapshot 的数据完整性。

到这里,L0 的 7 处特殊待遇全齐了:

  1. 优先级排在最前(LevelPrioritizer 中 L0=1,Mix=10)
  2. 独立触发间隔(L0CompactionTriggerInterval 与 Mix 分开)
  3. 独立互斥规则(L0 与所有其它类型在 channel 级互斥)
  4. 快路径(fast finish 跳过 DataNode 调用)
  5. 自愈开关(force-select bypass 应急修复)
  6. 版本兼容(升级时不杀旧 L0 任务)
  7. Snapshot 豁免(L0 段不被 snapshot 保护)
L0 删除回收专线
L0 删除回收专线

图 3:L0 删除回收专线——L0→L1/L2 流程 + 7 处特殊待遇

一句话总结:L0 不是普通 compaction,是系统的垃圾清运车,一旦停摆,整个系统会出问题。

你在用 Milvus 的时候如果碰过这种症状 - 删了一大堆数据,磁盘没掉,查询反而变慢 - 很可能就是 L0 这条专线的某个环节卡住了。欢迎在评论区聊聊你遇到过的场景。

5. 调度与互斥:谁能并发、谁要排队

讲完 L0 的特殊性,再来看整个调度层是怎么把 7 套机制装进一个执行池的。源码位置 compaction_inspector.go(763 行)。

整体结构是三队列 + 状态机:

代码语言:markdown
复制
queueTasks (优先级堆)   ──►   executingTasks (map)   ──►   cleaningTasks (map)
       ↑                          │                            │
       │                          ▼                            ▼
   Enqueue()              Process() 状态推进               Clean() 清理
                          checkCompaction()              cleanFailedTasks()

任务状态机也很清楚:pipelining → executing → meta_saved → completed → cleaned,失败可以走 failed / timeout 分支。

每种类型的最大执行时长是不一样的,源码里直接写死:

代码语言:go
复制
var maxCompactionTaskExecutionDuration = map[datapb.CompactionType]time.Duration{
    MixCompaction:               30 * time.Minute,
    Level0DeleteCompaction:      30 * time.Minute,
    ClusteringCompaction:        60 * time.Minute,
    SortCompaction:              20 * time.Minute,
    BumpSchemaVersionCompaction: 30 * time.Minute,
}

Clustering 给了一个小时,因为它要做完整的全局重分布。Sort 只给 20 分钟,单段排序不应该这么久。超过时长不会直接 fail,只是打 RatedWarn 告警,留出超时容忍空间。

调度循环维护 4 个互斥集合:

  • l0ChannelExcludes:L0 任务占用的 channel
  • mixChannelExcludes + mixLabelExcludes:Mix/Sort/BumpSchema 占用
  • clusterChannelExcludes + clusterLabelExcludes:Clustering 占用

互斥规则有三条:

  • L0 与 Mix/Sort/BumpSchema/Clustering 在 channel 级别互斥(L0 会写 delta 到 channel 任意段,必须独占)
  • Mix/Sort/BumpSchema 与 Clustering 在 label 级别互斥(partitionID + channel)
  • 同类任务在 channel 级别互斥

被互斥的任务会被放回 excluded 列表,循环结束后重新 enqueue。这里的 channel 级互斥约束最硬,因为 L0 的工作模式是往一个 channel 里所有相关段写 delta,必须独占整个 channel 才能维持一致性。

调度器与互斥规则
调度器与互斥规则

图 4:调度器架构——3 种 prioritizer、5×5 互斥矩阵、差异化执行时长

6. 用户看得见的两个出口:ForceMerge 和 Clustering

讲完后台机制,回到用户视角。对用户来说,真正能感知到的只有两个 compaction 入口:ForceMergeClustering。其余几种都是系统自动跑。

ForceMerge:带拓扑感知的 MixCompaction

ForceMerge 是 2025 年 12 月 PR #45556 引入的,标题就叫 feat: Add force merge。官方文档(milvus.io/docs/force-merge.md)的定义是:

Force Merge is designed to consolidate small and fragmented segments into fewer and larger ones to improve query performance and storage efficiency.

但翻开源码会发现,ForceMerge 本质上就是 MixCompaction 的一种变体,只是多了拓扑感知的目标大小计算。源码在 compaction_view_forcemerge.go

代码语言:go
复制
// QueryNode 内存约束:用全局最小内存
qnMaxSafeSize := float64(lo.Min(lo.Values(v.topology.QueryNodeMemory))) / querynodeMemoryFactor

// DataNode 内存约束:段必须能放进最小的 DataNode
dnMaxSafeSize := float64(lo.Min(lo.Values(v.topology.DataNodeMemory))) / datanodeMemoryFactor

maxSafeSize := min(qnMaxSafeSize, dnMaxSafeSize)

设计动机很现实:合并出来的段不能比最小的 QueryNode/DataNode 内存还大,否则加载不进去。Standalone 非 pooling 模式下 QueryNode 和 DataNode 共机,maxSafeSize 还要折半。

Issue #46706 里一位用户的 bug 报告也侧面证实了这个判断 - 用户原话是:ForceMerge (MixCompaction) successfully completes and merges 3 segments into 1。用户层面看到的 ForceMerge,在元数据里就是 MixCompaction。

还有一个细节很有意思 - 并行加载优化

代码语言:go
复制
perShardParallelism := queryNodeCount / (numReplicas * numShards)
if perShardParallelism > 1 && targetCount < perShardParallelism {
    // 提升 segment 数到 perShardParallelism,加快加载速度
    targetCount = desiredCount
    maxSafeSize = totalSize / float64(targetCount)
}

段数太少时,加载新 collection 的并行度受限于段数。ForceMerge 会主动增加段数到 perShardParallelism,让每个 querynode 都能并行加载一段。这跟合并成更少更大的段的目标是矛盾的 - 但加载速度比段大小更重要的时候,就反过来增加段数

Clustering:官方 benchmark 25 倍 QPS

Clustering 是另一个用户可见的出口,在 Milvus 2.5 正式以 Beta 形式进入用户文档。设计动机官方文档说得明白:

Milvus stores incoming entities in segments within a collection and seals a segment when it is full. As a result, entities are arbitrarily distributed across segments. This distribution requires Milvus to search multiple segments to find the nearest neighbors.

解决方案是基于 scalar field(聚类键)的值重新分布 segment,生成 PartitionStats(segment 与聚类键值的映射),查询时用聚类键值剪枝无关 segment。

官方 benchmark 数据:20M 条 LAION 768 维数据集,按 key 字段聚类后

  • 无过滤:17.75 QPS
  • key==1000 精确过滤:431.41 QPS(25 倍提升

剪枝比例越高,QPS 越高。但 Clustering 当前有一个临时约束,源码里有明确注释:

代码语言:go
复制
// todo: remove this check after support partial clustering compaction
func (policy *clusteringCompactionPolicy) checkAllL2SegmentsContains(...)

partition+channel 下的所有 L2 段必须全部参与 clustering,否则 skip。这是临时限制,未来会支持 partial clustering。

7. 演进时间线和踩坑史

把整个 compaction 子系统的演进拼起来看,能看出 Milvus 团队的工程节奏。

Compaction 演进时间线
Compaction 演进时间线

图 5:双时间线对照——架构演进(拆分 policy)与 L0 修复史(踩坑与补救)

架构演进

时间

PR

内容

2024-Q2

#37190

拆分 L0 和 Mix 的触发器间隔(L0 独立)

2024-Q3

#39217

L0 policy 引入 active collections 机制

2025-Q1

#42562

Sort stats task 独立为 SortCompaction

2025-Q2

#45556

新增 ForceMerge(target size compaction)

2025-Q2

#46990

新增 StorageVersionUpgrade policy

2025-Q3

#48808

新增 BumpSchemaVersionCompaction

L0 关键修复(这一串最有意思):

时间

PR

问题

2024-Q4

#40960

删除数据丢失(duplicate binlogID)

2025-Q2

#46436

设置 latestDeletePos 边界(L0 与 L1 选择的关键修复)

2025-Q2

#47154

Fast finish(L0 命中 0 个 L1/L2 时直接 drop)

2025-Q4

#48907

Self-heal(修复 import position bug 引起的僵尸 L0 和静默删除丢失)

2026-Q2

#47214 / #49122

deltalog max count 默认值从 30 提升到 1000(缓解高删除负载下的积压)

末尾一条值得单独说一下。Milvus 2.6.16(2026-05-14)的 release notes 明确写了:deltalog max count 默认值从 30 提升到 1000,目的是缓解高删除负载下的 compaction 积压。这从侧面说明,之前的默认值(30)对高删除场景明显不够,社区应该踩过不少坑。

整个时间线还藏着 3 处可见的渐进式迁移痕迹

  1. trigger v1 与 v2 并存:v1 仍处理 MixCompaction 的常规触发,v2 处理新增类型。v2 的 singleCompactionPolicy 注释里写着 support l2 single segment only for now, todo: move l1 single compaction here
  2. SingleCompactionPolicy 只支持 L2:同样是上面那条注释
  3. ClusteringCompaction 全量约束todo: remove this check after support partial clustering compaction

这些 todo 注释是先做能用的版本、再渐进优化的典型工程实践。也意味着当前架构仍在演进中 - L1 的 single compaction 迁移、partial clustering 支持都是后续会动的地方。

总结:对用户 1 个,对开发者 7 套

回到开头那个问题 - 删了 100 万条数据,磁盘为什么没缩。

答案藏在 Milvus 的整体设计取舍里:写入路径不被删除阻塞(往 L0 追加日志极快),删除回收是另一条独立的路径(L0 Delete Compaction 周期性触发)。两次 compaction 之间的窗口期,删除日志会一直堆在 L0 段里,磁盘空间自然不会立刻释放。

这件事在官方文档里其实没有专门文档说明 - 调研报告里那张覆盖度表格很能说明问题:

类型

官方文档覆盖度

ClusteringCompaction

完整(user guide + benchmark)

L0 DeleteCompaction

极低(仅在 PR/Issue 中可见)

MixCompaction

极低

SortCompaction

极低

BumpSchemaVersionCompaction

极低

StorageVersionUpgrade

极低

ForceMerge

完整

官方文档只覆盖了用户可感知的 Clustering 和 ForceMerge,其余 5 种类型的实现细节完全藏在源码和 PR 里

所以才会有那个反差:对用户来说只有一个 compact() API 和一个 Clustering Compaction,对开发者来说却跑着 7 套独立机制,每套对应一种不同的数据病,每套有自己的触发条件、优先级、互斥规则。

Milvus 把这件事拆得这么细,本质是因为向量库的 segment 一旦写入就被索引、被多副本加载、被 snapshot 引用,任何合并动作都伴随着复杂的并发约束。一个大而全的 compaction 既写不清楚触发条件,也调不清楚优先级。

拆细的代价是复杂度暴涨,好处是每条路径可以独立优化、独立修复。L0 那一长串 bug 修复史(#40960 → #46436 → #47154 → #48907)能看得出来 - 如果 L0 和 Mix 共用一条执行路径,这些 bug 的排查难度会指数级上升。

如果你在做向量库选型,或者运维 Milvus 时遇到过删除不释放、查询莫名变慢的问题,源码里那 7 套机制大概率能给你答案。建议收藏,下次遇到类似的磁盘异常先翻 L0 这条专线。

好啦,谢谢你观看我的文章,如果喜欢可以点赞转发给需要的朋友,我们下一期再见!敬请期待!

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 11 个枚举值,编号 1 还消失了
  • 2. 6 个独立 policy:每种类型对应一种数据病
  • 3. L0 Delete:那条删除回收专线
    • 优先级:永远排在最前面
    • Fast Finish:找不到目标段就直接丢
    • Self-heal:手动开关救场僵尸 L0
    • 版本兼容:升级时不杀旧 L0 任务
  • 4. Snapshot 豁免:删除回收不能停
  • 5. 调度与互斥:谁能并发、谁要排队
  • 6. 用户看得见的两个出口:ForceMerge 和 Clustering
    • ForceMerge:带拓扑感知的 MixCompaction
    • Clustering:官方 benchmark 25 倍 QPS
  • 7. 演进时间线和踩坑史
  • 总结:对用户 1 个,对开发者 7 套
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档