后台治理

合并与转储策略

在介绍 Compact 之前, 我们先来了解 3 个重要的概念:

  • 读放大:读取数据时实际读取的数据量大于真正的数据量。例如 LSM-TREE 读取数据时需要扫描多个 SSTable
  • 写放大:写入数据时实际写入的数据量大于真正的数据量。例如在 LSM-TREE 树中写入时可能触发 Compact 操作,导致实际写入的数据量远大于该 key 的数据量
  • 空间放大:数据实际占用的磁盘空间比数据的真正大小更多。例如 SSTable 中存储的旧版数据都是无效的

LSM-TREE 通过顺序写入和后台合并来获得极高的写入吞吐能力,而 Compaction 策略决定了系统在写性能、读性能与资源消耗之间的核心权衡。 对于传统的 LSM-TREE,compaction 的策略主要有两种:Size-Tiered Compaction 和 Level compaction。

  • Tiering:先堆起来,攒多了再一起合并 (写优先),Tiering 就像文件先丢抽屉,满了再统一归档
  • Leveling:每一层都整理整齐,不允许乱放 (读优先),Leveling 就像每次都立刻放到分类清晰的书架上

图片

Tiered Compaction (Rocks DB)

同一层允许 SST 互相重叠,更多是把大小相近的 SST 批量合并成更大的 SST,不强求每层严格不重叠。

  • 优点:写放大低 (合并更顺势,不需要为了消除重叠而反复重写)
  • 缺点:读放大高 (查询可能要看更多 run)

图片

Size-Tiered Compaction Strategy (STCS) 的思路就是将大小相近的 sst merge 成一个新文件 memtable 逐步刷入到磁盘 sst,刚开始 sst 都是小文件,随着小文件越来越多,当数据量达到一定阈值时,STCS 策略会将这些小文件 compaction 成一个中等大小的新文件。同样的道理,当中等文件数量达到一定阈值,这些文件将被 compaction 成大文件,这种方式不断递归,会持续生成越来越大的文件。

核心原则:

  • 同一层允许存在多个 Key 范围重叠的 SST 文件
  • 文件先堆积,达到数量阈值再统一合并

假设每个 SST 能存 4 个 key

  1. 第一次刷盘:SST-A: [1 – 4]
  2. 第二次刷盘:SST-B: [5 – 8]
  3. 第三次刷盘,这次有更新 + 新数据,写入 3,6,9,10,生成 SST-C: [3 – 10]

此时 Level 0 结构:[1–4] [5–8] [3–10]

  • C 与 A 在 3–4 重叠
  • C 与 B 在 5–8 重叠

查询 key=6 时必须同时查多个文件,这就是读放大。Tiered 的合并方式是当某一层文件数量达到阈值就触发合并,输出一个大文件,最终变成 [1 – 10]。 因此其优点是写入快、写放大小,但是缺点是查询要扫描多个文件,读放大。

Leveling Compaction (Cassandra)

每一层的 SST 尽量互不重叠 (key range disjoint),一旦有重叠就要把上层 SST + 下层所有重叠 SST合并重写。

  • 优点:读放大低 (查一个 key/范围通常只要看很少文件)
  • 缺点:写放大高 (频繁重写大量数据)

图片

leveled 每层由多个 sstable 组成一个有序的的 run,sstables 之间互相也保持有序的关系,每层的数据 size 到达上限后与下一层的 run merge。这种方式将 level 的多个 run 降为一个,减小了读放大和空间放大,小 sstable 的方式提供了精细化任务拆分和控制的条件,控制任务大小也就是控制临时空间的大小。

核心原则:

  • 每一层的 SST 文件 Key 范围必须互不重叠
  • 全系统形成连续有序空间

同样的场景,发现重叠后立刻进行合并 [1–4] + [5–8] + [3–10] → [1–10],然后再次切分为 [1–5] [6–10],以此保证同层区间不重叠。 因此其优点是查询快,查询最多查一个文件,但是写放大大,后台 compaction 持续发生,IO 压力大。

维度 Tiered Leveling
同层是否重叠 允许 不允许
写入成本 极低 中高
查询成本 极低
写放大
IO 压力
延迟稳定性
优先目标 吞吐 响应

MARS3 的实现

MARS3 采用 Tiered compaction 的方式,简而言之,在 VIN+TS 的典型时序负载下,数据持续写入会导致新 run 与下层多个 run 的 key space 广泛重叠,使 Leveled Compaction 很难维持层内不重叠,进而频繁触发大范围重写,写放大显著且在 MPP 多实例场景被进一步放大。

假设现在有 3 个设备 (VIN=A,B,C),每个设备都在持续上报最新值。

现在系统里已经有一层 L1 文件,每个文件覆盖一个 VIN 的一个时间段:

  • L1 里已有:
    • A 的历史:A:[0 ~ 999]、A:[1000 ~ 1999]
    • B 的历史:B:[0 ~ 999]、B:[1000 ~ 1999]
    • C 的历史:C:[0 ~ 999]、C:[1000 ~ 1999]

这时新数据来了:

  • A 又上报了一些点:TS = 1500 ~ 1700
  • B 又上报了一些点:TS = 1600 ~ 1800
  • C 又上报了一些点:TS = 1400 ~ 1650

这些新写入通常会先形成一个新的 run (比如在 L0 或上层),我们叫它 NewRun。NewRun 里面同时包含 A/B/C 的最新时间片,它覆盖的键范围大概是:

  • A:[1500 ~ 1700]
  • B:[1600 ~ 1800]
  • C:[1400 ~ 1650]

现在它会和 L1 哪些文件重叠?

  • A 的新数据会重叠 A:[1000 ~ 1999]
  • B 的新数据会重叠 B:[1000 ~ 1999]
  • C 的新数据会重叠 C:[1000 ~ 1999]

也就是说一个 NewRun 会同时重叠 L1 的多个文件(每个 VIN 都重叠一个)。

如果设备更多,比如 10,000 个 VIN 同时写,那么一个 NewRun 可能会重叠 成百上千个 L1 文件(取决于 L1 的切分方式)。而 Leveled 的要求是:L1 里文件要尽量不重叠。所以当 NewRun 要合并进 L1 时,Leveled 必须做这件事:把 NewRun + 所有与它重叠的 L1 文件读出来,重新合并排序,再写回成新的 L1 文件,保证写回后依然不重叠。这一步的代价是即便这次新写入只有很少数据 (比如 1GB),只要它碰到了很多 VIN 的历史段,它就会拖进来很多历史文件一起重写 (比如 10GB、50GB、100GB),造成验证的写放大。而 Size-Tiered 不强求下层不重叠,它更多是把同尺寸的 run 合并变大,不会为了消除重叠而每次拖进来一堆下层文件重写,所以在这个场景下,相对来说写放大的影响会更小一些。

与此同时,MARS3 的 RUN 为列存结构,需要足够大的物理连续性以保障扫描吞吐与压缩效率,这与 Leveled 常用的小 SST 粒度相冲突。Size-Tiered Compaction 则更契合该负载:它以合并同尺寸 run 为主,显著降低写放大,同时在时间推进的数据流下自然形成按时间聚集的 run 组织,使基于时间条件的过滤与跳读更有效。 为了控制读放大,我们为每一层限制了读放大系数,每一层的读放大超过这个系数以后就会触发向下层的 compaction:

图片

level_size_amplifier 用于指定 Level 尺寸的放大系数:

Level 尺寸的放大系数。Level 触发合并操作的阈值,计算方式为:rowstore_size * (level_size_amplifier ^ (level -1))。其值越大,读速越慢,写速越快。可以根据具体场景信息 (写多读少/读多写少、压缩率等) 来决定具体值。注意:确保每个 Level 的 run 数量不要过多,否则会影响查询性能,甚至阻止新数据插入

level_size_amplifier 的本质是控制每一层比上一层能大多少倍,就好比越往上是缓冲区,越往下是长期存储区 因此不难理解,amplifier 更大,每层能堆更久,run 更可能变多,导致查询检查更多对象 (读放大),但 compaction 触发少 (写更快),amplifier 更小,更早触发下沉整理,run 更少更整洁,读更快,但是 compaction 更频繁。rowstore_sizelevel_size_amplifier 两者合在一起,形成一个可控的下沉节奏,用来在 Tiered 模型下约束 run 堆积,从而限制读放大,同时尽量保留写吞吐优势。更多细节参照可观测性

Compaction scheduler

图片

  1. Compaction scheduler:数据库启动的时候启动一个常驻的辅助进程,当 rowstore 达到 rowstore_size 的时候会切换到一个新的 rowstore 上面,此时会向 scheduler 进程发送信号,并通过共享内存告知此次的 compaction 请求,包括 relation id 以及相应的 level,scheduler 进程负责注册新的 worker 来做 compaction。 Compaction scheduler 主要负责:
  • 启停 compaction worker
  • 管理复用 compaction worker
  • 确定 compaction 任务的优先级
  • 正确响应数据库停机请求
  1. Compaction worker:负责具体的 compaction 请求,同时检测到 level 的读放大超过限制以后,通知 scheduler 进程新的 compaction 请求,目前掣肘于进程模型,Compaction worker 的数量上限在代码中写死了 16 个。

  2. Compaction prober:负责主动探测 compaction 任务,比如:

  • 一些定时compaction任务
  • 被中断的compaction任务 大体流程是:
    1. Insert Backend 持续写入 RowStore
    2. 当某个 RowStore 达到阈值,就会切换到一个新的 RowStore
    3. 向 scheduler 发送 SIGUSR1,同时在共享内存里写入 compaction 请求信息:(relid, level)
    4. scheduler 被唤醒,读取共享内存里的请求:
    • 排队 / 去重 / 排优先级
    • 选择新拉起 worker或复用空闲 worker
    1. worker 开始执行 compaction

Compaction 优先级

对于 compaction,如果有多个表都需要做 compaction,但是总共就 16 个 worker,因此需要有一定的优先级,保证“最需要”的表能够及时进行 compact。 简而言之,compaction 会考虑比如 Level 的层次,越低级别越高;考虑是否是 Eager 还是 Lazy (手动和自动);compaction 的类型等等。

Autoprobe

Autoprobe 的原理是,定时探测所有 Mars3 表的事务年龄和大小,以年龄除以大小为 score,autoprobe 可以对历史 Level 进行压缩。选出其中 score 最高的 N 个level触发 flush/compaction 任务。通过函数 matrixts_internal.mars3_autoprobe_candidates() 可以查看 autoprobe 的候选 level 及其 score。

adw=# select * from matrixts_internal.mars3_autoprobe_candidates();                                                                                                                    
 segid | datname |   relname   | level | nruns |    age     |   bytes   |         score                                                                                                
-------+---------+-------------+-------+-------+------------+-----------+-----------------------                                                                                       
     0 | adw     | t_w0_nosort |     1 |    13 |     616005 |  49888440 |  0.012347650076851471                                                                                        
     0 | adw     | t_w0_nosort |     0 |     1 | 2147483647 |  25034752 |     85.78010467209741                                                                                        
     0 | adw     | t_w3b_3key  |     0 |     1 | 2147483647 |  25034752 |     85.78010467209741                                                                                        
     0 | adw     | t_w1_1key   |     1 |     7 |     329693 |  23821378 |  0.013840215288972788 
...

因为 autoprobe 总是选择 score 高的 level,为了防止由于 compaction 失败导致 autoprobe 不能处理其他 score 更低 level。增加了 autoprobe 的黑名单机制,同一个 compaction 失败 3 次后进入黑名单,不再继续重试:

  • 通过函数 mars3_autoprobe_blacklist() 可以查看黑名单;
  • 通过函数 mars3_autoprobe_blacklist_remove(regclass, level) 可以删除当前数据库指定的level的黑名单;
  • 通过函数 mars3_autoprobe_blacklist_clear() 可以删除所有黑名单;

GUC 说明:

  1. guc mars3.autoprobe_period 控制多长时间探测一次,单位是秒,大于 0 为打开,0 为关闭,默认为关闭,可以先设置 10 分钟探测一次,再根据工作负载调整
  2. guc mars3.autoprobe_workers 这个控制一次探测选出几个 level,默认为 2,注意这个占用的是普通 compaction 的 worker 数,并不是启动新的 worker
  3. guc mars3.autoprobe_retry 控制重试次数,值为 2 则一共尝试 3 次;
  4. guc mars3.autoprobe_blacklist_size 控制黑名单大小;

autoprobe 关了的影响就是没有写入的表,就不会进行 compact 了;有写入的表还是会不断 merge 的

可观测性

ps aux | grep postgres | grep 'compact worker' :此命令用于验证所有工作进程是否处于运行状态。检查是否存在未处于运行状态但已启动较长时间且未退出的工作进程,这些系统运行异常的迹象。 其次,在数据库日志中也会有 compact 相关活动信息,创建一张 t1 表并不断插入数据:

postgres=# create table t1(id int,info text)                                                                                                                                                                                                                
using mars3 with(mars3options='prefer_load_mode=single,rowstore_size=64');                                                                                                                                                                                  
NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause -- Using column named 'id' as the Greenplum Database data distribution key for this table.                                                                                                              
HINT:  The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.                                                                                                  
CREATE TABLE  

新开窗口使用 matrixts_internal.mars3_level_stats 不断观察各个 Level 的状态

图片

可以看到,当 L0 写满 64MB (rowstore_size) 之后,L1 短暂出现过 32 KB,然后直接往 L2 进行写。后续也是直接写入 L2,那么为什么会这样,为什么不是先直接写入 L1?

图片

在 MARS3 中,有 adjust level 的逻辑 —— GetDesiredLevel

  • 根据一个 Run 的大小 (TotalSize) 推算它应该属于哪个层级 (desired_level)
  • 如果现在不在那个层级 (cur_level),就加锁,把这个 Run 从当前层挪到目标层

根据上面代码流程

  • run_size ≈ rowstore_size = 64MB
  • total_amp = run_size / rowstoresize = 64MB / 64MB = 1.0
  • desired_level = log(1.0) / log(8) = 0 / log(8) = 0

因此,这个 run 的理论相对层号是 0,其大小正好等于 rowstore_size 这一档

if (desired_level < 0)
{
    ret = desired_level < -0.5 ? 1 : 2;
}
else
{
    ret = Min(2 + round(desired_level), MAXLEVELS - 1);
}

因为我们算出来 desired_level = 0,走 else:

  • round(0) = 0
  • ret = 2 + 0 = 2 (再做一次 Min 上限保护)

所以最终结果:GetDesiredLevel() 返回 2,也就是说:这个 64MB 的 run,按新版本规则应放到 L2。

设计原理

MARS3 的 tiered 模型里,稳定的几何规律不是靠每层总量瞬间严格满足,而是靠 compaction 让 run 的目标大小档位逐步趋近 amp 倍增长。触发 compaction 的核心阈值是:

  • 旧版本 (< REL_V1):threshold(level)=rowstoresize × amp^level
  • 新版本 (>= REL_V1):threshold(level)=rowstoresize × amp^(level - 1)

这里 bytes_threshold 用于触发一次 compaction / pick 输入集合规模预算,Compact 会 pick N 个 run;这 N 个 run 的 total_size 大概是 bytes_threshold。 所以按照这个计算方式:

  • Level 1:64 MB
  • Level 2:512 MB
  • Level 3:4 GB
  • Level 4:32 GB
  • ...

同时加上 Min(MAX_RUN_SIZE, …) 的限制,如果算出来超过了 MAX_RUN_SIZE,就按 MAX_RUN_SIZE 来 (防止 job 或输出 run 过大)。

设计动机

  1. 动机 A:让几何增长模型稳定,不被列存压缩率扰乱:
  • rowstore_size 是行存粒度
  • L1+ 是列存压缩态,字节数会变化很大,因此如果不做起算点平移,用同一套公式硬套每层,很容易出现:L1 因压缩变小 → 触发节奏失真 → 读放大/堆积失控
  • 新版本用 level-1,并在 run 归类上用 +2,就是在把模型基准对齐到更稳定的层上,从工程上讲就是:别用最容易漂的那层当尺子
  1. 动机 B:把 compaction 任务规模做成指数增长,但用上限兜底
  • 指数增长:层越深一次合并处理的数据就应该越大,否则会频繁扰动深层数据 (写放大、IO 抖动)
  • MAX_RUN_SIZE:防止某次 compaction 任务或输出 run 过大,把系统拖死,可以表级调整
  1. 动机 C:把读放大控制变成可调整项,level_size_amplifier 越大:
  • 阈值增长越快 → compaction 更少 → 写更顺
  • 但 run 更可能堆积 → 读放大风险更大

所以 amp 越大,读越慢写越快。

验证

还是以上方的 t1 表为例,观察 L2 何时进行 Pick,按照刚刚的计算模型,L2 大约在 512 MB 的时候就会触发 compaction:

                                    Thu 29 Jan 2026 02:42:07 PM CST (every 1s)                                                                                                                                                                              

 segid | level | total_nruns | visible_nruns | invisible_nruns | object_nruns | object_visible_nruns | level_size                                                                                                                                           
-------+-------+-------------+---------------+-----------------+--------------+----------------------+------------                                                                                                                                          
     3 |     0 |           1 |             1 |               0 |            0 |                    0 | 64 MB                                                                                                                                                
     3 |     1 |           0 |             0 |               0 |            0 |                    0 | 0 bytes                                                                                                                                              
     3 |     2 |          15 |            15 |               0 |            0 |                    0 | 533 MB                                                                                                                                               
     3 |     3 |           0 |             0 |               0 |            0 |                    0 | 0 bytes                                                                                                                                              
(4 rows)                                                                                                                                                                                                                                                    

                                    Thu 29 Jan 2026 02:42:08 PM CST (every 1s)                                                                                                                                                                              

 segid | level | total_nruns | visible_nruns | invisible_nruns | object_nruns | object_visible_nruns | level_size                                                                                                                                           
-------+-------+-------------+---------------+-----------------+--------------+----------------------+------------                                                                                                                                          
     3 |     0 |           2 |             2 |               0 |            0 |                    0 | 69 MB                                                                                                                                                
     3 |     1 |           0 |             0 |               0 |            0 |                    0 | 0 bytes                                                                                                                                              
     3 |     2 |          15 |            15 |               0 |            0 |                    0 | 533 MB                                                                                                                                               
     3 |     3 |           0 |             0 |               0 |            0 |                    0 | 0 bytes                                                                                                                                              
(4 rows) 

                                    Thu 29 Jan 2026 02:42:09 PM CST (every 1s)                                                                                                                                                                              

 segid | level | total_nruns | visible_nruns | invisible_nruns | object_nruns | object_visible_nruns | level_size                                                                                                                                           
-------+-------+-------------+---------------+-----------------+--------------+----------------------+------------                                                                                                                                          
     3 |     0 |           1 |             1 |               0 |            0 |                    0 | 16 MB                                                                                                                                                
     3 |     1 |           0 |             0 |               0 |            0 |                    0 | 0 bytes                                                                                                                                              
     3 |     2 |          16 |            16 |               0 |            0 |                    0 | 577 MB                                                                                                                                               
     3 |     3 |           1 |             0 |               1 |            0 |                    0 | 32 kB                                                                                                                                                
(4 rows)                                                                                                                                                                                                                                                    

                                    Thu 29 Jan 2026 02:42:10 PM CST (every 1s)                                                                                                                                                                              

 segid | level | total_nruns | visible_nruns | invisible_nruns | object_nruns | object_visible_nruns | level_size                                                                                                                                           
-------+-------+-------------+---------------+-----------------+--------------+----------------------+------------                                                                                                                                          
     3 |     0 |           1 |             1 |               0 |            0 |                    0 | 16 MB                                                                                                                                                
     3 |     1 |           0 |             0 |               0 |            0 |                    0 | 0 bytes                                                                                                                                              
     3 |     2 |          16 |            16 |               0 |            0 |                    0 | 626 MB                                                                                                                                               
     3 |     3 |           1 |             0 |               1 |            0 |                    0 | 32 kB                                                                                                                                                
(4 rows)

...
...

                                    Thu 29 Jan 2026 02:42:26 PM CST (every 1s)                                                                                                                                                                              

 segid | level | total_nruns | visible_nruns | invisible_nruns | object_nruns | object_visible_nruns | level_size                                                                                                                                           
-------+-------+-------------+---------------+-----------------+--------------+----------------------+------------                                                                                                                                          
     3 |     0 |           1 |             1 |               0 |            0 |                    0 | 32 MB                                                                                                                                                
     3 |     1 |           0 |             0 |               0 |            0 |                    0 | 0 bytes                                                                                                                                              
     3 |     2 |          16 |            16 |               0 |            0 |                    0 | 626 MB                                                                                                                                               
     3 |     3 |           1 |             0 |               1 |            0 |                    0 | 467 MB                                                                                                                                               
(4 rows) 

...
...

                                    Thu 29 Jan 2026 02:43:58 PM CST (every 1s)                                                                                                                                                                              

 segid | level | total_nruns | visible_nruns | invisible_nruns | object_nruns | object_visible_nruns | level_size                                                                                                                                           
-------+-------+-------------+---------------+-----------------+--------------+----------------------+------------                                                                                                                                          
     3 |     0 |           1 |             1 |               0 |            0 |                    0 | 32 MB                                                                                                                                                
     3 |     1 |           0 |             0 |               0 |            0 |                    0 | 0 bytes                                                                                                                                              
     3 |     2 |           1 |             1 |               0 |            0 |                    0 | 36 MB                                                                                                                                                
     3 |     3 |           1 |             1 |               0 |            0 |                    0 | 531 MB                                                                                                                                               
(4 rows)                                                                                                                                                                                                                                                    

                                    Thu 29 Jan 2026 02:43:59 PM CST (every 1s)                                                                                                                                                                              

 segid | level | total_nruns | visible_nruns | invisible_nruns | object_nruns | object_visible_nruns | level_size                                                                                                                                           
-------+-------+-------------+---------------+-----------------+--------------+----------------------+------------                                                                                                                                          
     3 |     0 |           1 |             1 |               0 |            0 |                    0 | 32 MB                                                                                                                                                
     3 |     1 |           0 |             0 |               0 |            0 |                    0 | 0 bytes                                                                                                                                              
     3 |     2 |           1 |             1 |               0 |            0 |                    0 | 36 MB                                                                                                                                                
     3 |     3 |           1 |             1 |               0 |            0 |                    0 | 531 MB                                                                                                                                               
(4 rows)  

结合日志和现象,我们可以观察到: 14:42:07 ~ 14:42:08

  1. L0 行存 RowStore 正好到一个 run 的切换点
  2. L2 已经堆了 15 个列存 run,总计 533MB
    L0: 1 run, 64MB
    L2: 15 runs, 533MB
    L1: 0
    L3: 0

    14:42:09 ~ 14:42:10

  3. L0 从 69MB 回到 16MB:说明刚刚发生了一次 flush/切换,新 run 重新开始累积
  4. L2 run 从 15→16:新 flush 出来的一份列存 run 被放进 L2
  5. L3 出现一个不可见 (invisible) 且很小 (32KB) 的 run:猜测是 compaction 进行中常见的占位/元数据/临时产物状态
    L0: 回到 16MB (新 run 开始累计)
    L2: 16 runs, 577 → 626MB (又新增了一个 run)
    L3: 1 run, invisible=1, 32KB

    14:42:26

  6. L2 的 compaction 还在跑
  7. L3 的输出 run 已经写出来大体积,但仍处于 不可见,表明输出的 run 还没 commit,或需要等元数据更新
    L2: 16 runs, 626MB
    L3: invisible 32KB → 467MB(仍不可见)
    14:43:58 ~ 14:43:59 (收尾)
    L2: 1 run, 36MB
    L3: 1 run, 531MB (可见)
    L0: 32MB (新的 RowStore run 正在累计)

    这说明 compaction 完成并提交:

  • L2 的 16 个 run 被合并吃掉了绝大多数,只剩下一个小 run (36MB),通常是 compaction 期间新产生/来不及被本轮合并选中的 run,或是阈值/边界导致的剩余
  • L3 最终只剩 1 个可见大 run (531MB),这就是本轮 L2→L3 的 compaction 输出成果
  • L0 继续写入,稳定运行

图片

值得注意的是,L0 的 bytes_threshold 为 0,是 flush 下去的,没啥意义 RunLife-Flushed,用于代码调试,追踪一个 Run 的生命周期,包括:

  • RunLife-Created:创建一个 RUN
  • RunLife-Flushed:读并刷一个 RUN
  • RunLife-Recycled:回收复用一个 RUN

图片

返回上一章节:存储引擎原理