多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Pebble 点墓碑密度压缩启发式(Tombstone Density Compaction Heuristic)设计解析

Pebble 点墓碑密度压缩启发式(Tombstone Density Compaction Heuristic)设计解析 KV存储嵌入式数据库【免费下载链接】pebbleRocksDB/LevelDB inspired key-value database in Go项目地址https://gitcode.com/gh_mirrors/pe/pebble点击查看免费下载导读本文基于 Pebble 的官方设计文档 20240701_tombstone_density_heuristic.mdRFC状态为 done as of 2024-08-15系统讲解 Pebble 为应对“点墓碑point tombstone高密度堆积导致读性能劣化”而引入的全新压缩类型tombstone density compaction。文章将带你掌握墓碑密集数据块的定义与三个核心配置项、从 SSTable 写入期检测到表统计再到压缩选择器的完整执行链路、以及与 move/elision-only/read-triggered 等既有压缩类型的优先级关系并结合options.go、compaction_picker.go、sstable/rowblk_writer.go、compaction_test.go等源码给出可验证的实现依据。读完本文你将理解该启发式如何在不依赖“空间回收收益”的情况下仅凭“墓碑在键空间中的密度”就主动触发压缩从而保护读路径的迭代性能。背景为什么“挤在一起的墓碑”会拖慢读性能Pebble 原有的大部分压缩启发式都基于 SSTable 文件大小例如各层容量分数、CompactionScore等。这类启发式存在一个共同盲区当大量点墓碑在键空间中彼此紧邻地堆积时对这些墓碑之后的键执行读取会变得极其缓慢而基于文件大小的启发式却对此无感知。根据 RFC 引述的 issue 分析未压缩的点墓碑主要通过两条途径劣化读性能CPU 开销执行SeekGE/SeekLT、Next/Prev等顺序迭代操作时迭代器需要逐一“跳过”墓碑才能到达第一个存活键。墓碑簇越长单次 seek/next 付出的 CPU 周期越多。I/O 开销墓碑占据的数据块仍需从磁盘加载由于墓碑挤压了存活键在块中的占比读取存活键往往需要加载更多数据块产生额外的 I/O。RFC 特别强调必须在墓碑被读取之前就主动削减其密度——因为现实工作负载中存在大量“墓碑写入后很久才触发读取”的场景如队列式负载先删除后读。等到读请求真正到来再处理性能损失已经发生。六类诱发墓碑堆积的典型因素RFC 归纳了导致现有启发式失效、墓碑持续堆积的具体场景大库才明显墓碑堆积通常只发生在大型存储 10 GB上且需要 LSM 的多层multiple levels都被填充之后才会显现。LSM “过于规整”当层与层之间的尺寸比恰好处于合理值、各层压缩分数compaction scores都被计算为 1 时任何常规压缩都不会被调度——尽管墓碑堆积已经存在。高层墓碑 低层多文件当高层L0–L3存在高密度点墓碑、且这些墓碑横跨底层L4–L6大量 SSTable 时读性能恶化尤其严重。热写区间与热读区间相邻最常见的场景是一个键范围被高频写入/删除而相邻键范围被高频读取。RocksDB 在其“用 RocksDB 实现队列服务”的文档中描述过此类负载CockroachDB 的 KV liveness 区间就多次踩中该问题——raft log 被夹在用于过期租约expiration-based leases的高频读键之间节点每隔几秒 gossip 一次 liveness反复写删 raft log 消息使 raft log 跨越大量 SSTable用墓碑填满缓存把高频读的租约过期键挤出缓存。小 KV 值更易堆积KV 对越小单个 SSTable 能容纳的键越多。此时“空间回收潜力”类启发式几乎失效——墓碑虽小、占空间不大但填满了键空间。顺序迭代场景专属SeekGE/SeekLT与Next/Prev需要连续跨过多个键一旦Next落入墓碑簇必须先遍历完所有墓碑才能到达存活键。核心设计墓碑密集数据块Tombstone-Dense Data Block新启发式引入的核心概念是tombstone-dense data block。一个数据块只要满足以下任一条件即被判定为“墓碑密集”数量阈值块内点墓碑数量 ≥N。默认N 100由新选项options.Experimental.NumDeletionsThreshold控制。尺寸占比阈值块内点墓碑的未压缩尺寸 / 数据块未压缩尺寸 ≥Y。例如默认Y 0.5时一个 4KB 的数据块若含至少 2KB 的点墓碑即判定为密集。该阈值由options.Experimental.DeletionSizeRatioThreshold控制。两条判据分工明确数量阈值防 CPU 浪费墓碑个数多导致逐键跳过的 CPU 开销尺寸阈值防 I/O 浪费墓碑体积大导致加载块时浪费 I/O。注意两个条件之间是“或”的关系只要命中其一即算墓碑密集。源码印证写路径上的逐块判定判定逻辑在 SSTable 写入期实时执行。在行式块row block写入器 sstable/rowblk_writer.go 中maybeIncrementTombstoneDenseBlocks在每个数据块 flush 完成后被调用func (w *RawRowWriter) maybeIncrementTombstoneDenseBlocks() { minSize : w.deletionSizeRatioThreshold * float32(len(w.dataBlockBuf.uncompressed)) if w.dataBlockBuf.numDeletions w.numDeletionsThreshold || float32(w.dataBlockBuf.deletionSize) minSize { w.props.NumTombstoneDenseBlocks } w.dataBlockBuf.numDeletions 0 w.dataBlockBuf.deletionSize 0 }其中numDeletions是块内点墓碑DEL数量deletionSize是块内点墓碑的未压缩字节数。列式块columnar block写入器 sstable/colblk_writer.go 中实现了完全相同的判定逻辑RawColumnWriter.maybeIncrementTombstoneDenseBlocks。两处实现共享同一判定语义说明该特性同时覆盖了 Pebble 的行式与列式两种 SSTable 块格式。阈值常量定义在 sstable/options.go// DefaultNumDeletionsThreshold defines the minimum number of point // tombstones that must be present in a data block for it to be // considered tombstone-dense. DefaultNumDeletionsThreshold 100 // DefaultDeletionSizeRatioThreshold defines the minimum ratio of the size // of point tombstones to the size of the data block in order to consider the // block as tombstone-dense. DefaultDeletionSizeRatioThreshold 0.5三个配置项详解三个新选项全部位于options.Experimental命名空间下options.go用于控制“块级判定”与“表级入选”两个层面的阈值选项类型默认值作用Experimental.NumDeletionsThresholdint100单块内点墓碑数量下限块级判定判据一Experimental.DeletionSizeRatioThresholdfloat320.5块内点墓碑未压缩尺寸占比下限块级判定判据二Experimental.TombstoneDenseCompactionThresholdfunc() float640.10当前源码表中墓碑密集块占比下限比例以 1 为满值表级入选判据三者在Options.EnsureDefaults中被赋默认值options.goif o.NumDeletionsThreshold 0 { o.NumDeletionsThreshold sstable.DefaultNumDeletionsThreshold } if o.DeletionSizeRatioThreshold 0 { o.DeletionSizeRatioThreshold sstable.DefaultDeletionSizeRatioThreshold } if o.TombstoneDenseCompactionThreshold nil { o.TombstoneDenseCompactionThreshold func() float64 { return 0.10 } }值得注意的两处细节TombstoneDenseCompactionThreshold是一个函数而非普通数值。注释明确指出这允许“基于工作负载特征动态重配阈值”同时“设为 0 或负值即禁用墓碑密度压缩”。这一点在压缩选择器中也得到了印证见下文pickTombstoneDensityCompaction开头对threshold 0的提前返回。RFC 原文给出的表级默认值是 5%而当前仓库options.go的实际默认是0.1010%说明实现演进过程中调整过默认值——本文以当前源码为准。NumDeletionsThreshold的 0 值语义EnsureDefaults只在值为 0 时填充默认值因此该选项无法通过显式置 0 来“禁用数量判据”只能调高或调低。要整体关闭墓碑密度压缩应把TombstoneDenseCompactionThreshold设为 0 或负值。序列化与字符串格式这三个选项同样参与 Pebble 选项文件的序列化Options.String()见 options.gonum_deletions_threshold100 deletion_size_ratio_threshold0.500000 tombstone_dense_compaction_threshold0.100000反向解析在 options.go 的Parse分支中num_deletions_threshold经strconv.Atoi解析为intdeletion_size_ratio_threshold经strconv.ParseFloat(…, 32)后转为float32tombstone_dense_compaction_threshold经strconv.ParseFloat(…, 64)解析后封装为闭包函数。这三个键名可供通过 OPTIONS 文件配置数据库时直接使用。从写入到压缩选择的完整链路RFC 给出了四步流水线源码实现与之一一对应Step 1 — 写入期识别write timeSSTable 写入时逐块判定墓碑密集块计数写入新表属性NumTombstoneDenseBlocks。该属性定义在 sstable/properties.go序列化键名为pebble.num.tombstone-dense-blocks// NumTombstoneDenseBlocks is the number of data blocks in this table that are // considered tombstone-dense. NumTombstoneDenseBlocks uint64 prop:pebble.num.tombstone-dense-blocksStep 2 — 表统计汇聚table stats collection表统计收集阶段将NumTombstoneDenseBlocks与既有的NumDataBlocks属性结合计算出新表统计TombstoneDenseBlocksRatio。该计算发生在 internal/manifest/table_metadata.go// TombstoneDenseBlocksRatio is the fraction of data blocks in this table that // are tombstone-dense. TombstoneDenseBlocksRatio float64 ... if props.NumDataBlocks ! 0 { b.props.TombstoneDenseBlocksRatio float64(props.NumTombstoneDenseBlocks) / float64(props.NumDataBlocks) }即TombstoneDenseBlocksRatio NumTombstoneDenseBlocks / NumDataBlocks。Step 3 — 入选判定eligibility当某表的TombstoneDenseBlocksRatio高于TombstoneDenseCompactionThresholdX%时该表即具备墓碑密度压缩的入选资格。Step 4 — 选择与执行picking execution若有多个表入选通过manifest.Annotator机制与 elision-only 压缩同款方式优先压缩TombstoneDenseBlocksRatio最高的表候选表选定后压缩以与默认压缩相同的方式执行。压缩选择器的实现细节调度入口与优先级墓碑密度压缩由compactionPickerByScore.pickTombstoneDensityCompaction实现compaction_picker.go并在pickAutoNonScore中被最先检查compaction_picker.go// pickAutoNonScore picks the best non-score-based compaction, if any. func (p *compactionPickerByScore) pickAutoNonScore(env compactionEnv) (pc pickedCompaction) { // Check for files which contain excessive point tombstones that could slow // down reads. Unlike elision-only compactions, these compactions may select // a file at any level rather than only the lowest level. if pc : p.pickTombstoneDensityCompaction(env); pc ! nil { return pc } // Check for L6 files with tombstones that may be elided. ... if pc : p.pickElisionOnlyCompaction(env); pc ! nil { return pc } ...结合pickAutoNonScore的调用顺序墓碑密度 → elision-only → virtual rewrite → blob rewrite → read-triggered以及 score-based基于各层压缩分数的默认压缩在pickAutoNonScore之前先行检查的事实RFC 中“优先级严格低于默认压缩、严格高于 read-triggered 压缩”的定位在代码中得到完整印证。新压缩类型compactionKindTombstoneDensity定义在 compaction.go其字符串表示为tombstone-density——在压缩日志中即以compacted(tombstone-density)形式出现。候选扫描规则pickTombstoneDensityCompaction的实现体现了若干关键的工程决策禁用检查threshold 0时直接返回nil即表级阈值置 0/负值可完全禁用该压缩类型。跳过最低层扫描从倒数第二层numLevels - 2向上遍历到 L0注释明确说明“不考虑最低层因为 elision-only 压缩负责处理那一层的情形”——最低层的墓碑抹除elision是既有能力的职责。重叠比护栏maxOverlappingRatio 40.0若候选文件在最低非空层的重叠文件总尺寸AggregateSizeSum与其自身尺寸之比超过 40则该文件被排除。设计意图是重叠比极高的文件即使自身墓碑密集其墓碑在键空间中也往往稀疏、迭代开销不大强行压缩反而可能产生超大压缩任务。该常量取值带有经验性注释原文“chosen somewhat arbitrarily, after some observations around excessively large tombstone density compactions”。无效统计跳过TableBacking.Properties()无效propsValid false的文件被跳过——没有统计依据就不做判断。择优逻辑同一层内选取TombstoneDenseBlocksRatio最高的文件且优先选择更低层的候选如 L5 的候选优先于 L4命中后即停止向更高层扫描。常规防御跳过IsCompacting()中或尺寸为 0 的文件最终经pickedCompactionFromCandidateFile复用既有流程生成压缩计划输出层由defaultOutputLevel决定。move 优化尽量少重写墓碑密度压缩还可能被优化为 move 压缩。在 compaction.go 的maybeSwitchToMoveOrCopy中case compactionKindTombstoneDensity: // Tombstone density compaction can be optimized into a move compaction. // However, we want to avoid performing a move compaction into the lowest // level, since the goal there is to actually remove the tombstones; even if // they dont prevent a lot of space from being reclaimed, tombstones can // still be expensive to scan over. if c.outputLevel.level numLevels-1 { return }即当压缩满足“单输入文件、输出层无重叠文件、无额外层数据、且输出层不是最低层”等条件时墓碑密度压缩可以被降级为不重写数据的 move 压缩——除非目标输出层是最底层L6。注释给出的理由很关键压缩进最底层的目的是真正抹掉墓碑若在最低层还做 move墓碑仍将留在那里继续拖慢扫描。这套“能 move 就 move、进最底层必须真压缩”的策略让新启发式在改善读性能的同时避免无谓的写放大。测试与验证单元测试四个边界场景compaction_test.go 中成组地测试了墓碑密度压缩及其 move 优化覆盖四个方向TestTombstoneDensityCompactionMoveOptimizationL4 单文件、L5 无重叠、祖辈L6重叠低于阈值 → 压缩被优化为compactionKindMove文件从 L4 移动至 L5。TestTombstoneDensityCompactionMoveOptimization_NoMoveWithOverlap输出层 L5 存在重叠文件 → 优化不生效压缩保持compactionKindTombstoneDensity。TestTombstoneDensityCompactionMoveOptimization_GrandparentOverlapTooLarge祖辈层 L6 存在 1GB 大文件 → 不选择任何压缩pc nil。TestTombstoneDensityCompactionMoveOptimization_BelowDensityThreshold与..._InvalidStats密度低于阈值、或表统计缺失/无效时均不触发压缩。测试中通过PopulateProperties直接构造NumTombstoneDenseBlocks/NumDataBlocks并下调TombstoneDenseCompactionThreshold或NumDeletionsThreshold来制造候选手法与 RFC 的设计完全对应。数据驱动测试端到端行为testdata/compaction_tombstones 用注释完整复述了该特性的入选条件与优化条件# A file is eligible for tombstone density compaction when: # - Its TombstoneDenseBlocksRatio exceeds TombstoneDenseCompactionThreshold # - The file has valid stats # - The file is not already compacting # # The TombstoneDenseBlocksRatio is calculated during SSTable creation: # - NumTombstoneDenseBlocks / NumDataBlocks # - A block is considered tombstone dense if it meets criteria set by # NumDeletionsThreshold and DeletionSizeRatioThreshold三个测试用例分别演示L4→L5 的 move 优化日志输出compacted(move)与compacted(tombstone-density)L5 重叠文件阻止 move、退化为常规重写压缩compacted(tombstone-density) L4 … L5 … - L5以及祖辈层重叠字节超过MaxOverlapBytes时完全不触发压缩。测试数据使用force-tombstone-density-ratio指令强制覆盖TombstoneDenseBlocksRatio见 data_test.go以便在固定场景下精确验证选择器行为。性能基准与压测建议RFC 指出墓碑堆积在队列式工作负载queue-based workloads中最为常见配套基准由 PR #3744 引入bench/queue.go即仓库内对应基准的载体位于 bench/queue.go。RFC 还给出了三条未来压测思路用复制快照replication snapshot为新节点播种——该场景天然具有墓碑堆积倾向借鉴 CockroachDB 的 liveness 区间扫描性能测试原由墓碑拖慢改造为诱导慢读的复现用例为历史观测到的慢读场景创建可复现的 roachtests。设计探索四个备选方案的取舍RFC 记录了设计过程中的四次探索其中“块级粒度”方案最终被 #3790 落地其余方案作为未来储备1. 墓碑比率Tombstone Ratio——已实现但效果有限最朴素的做法定义阈值TOMBSTONE_THRESHOLD当某 SSTable 满足NumDeletions/NumEntries TOMBSTONE_THRESHOLD即调度压缩。例如阈值 0.6 时含 10000 个内部键的 SSTable 若有 6000 个墓碑就应压缩。其局限在于只看单表密度不考虑与其它表的重叠超大 SSTable 中若只有一小段墓碑簇其占全表键的比例很低会漏判——但这段墓碑仍拖慢读单独使用大概率不够但可与其他方法组合RocksDB 的CompactOnDeleteCollector与 ScyllaDB 的 ICS 垃圾回收都采用“组合策略”。该方案在 #3793 中实现过实测效果不佳。2. 更细粒度More Granularity——本方案所属类别在单表内进一步定位墓碑簇的位置两种思路把 SSTable 键范围切成若干桶bucket统计有多少桶满足“ Y% 为墓碑”若至少 Z 个桶墓碑密集则压缩该表借鉴 RocksDB 的滑动窗口法compact_on_deletion_collector写表时滑过一个窗口若最近 Y 个键中至少有 X 个墓碑就调度压缩可扩展为“边写边扩大窗口”以精确锁定墓碑密集的键区间并按墓碑段长度排序压缩优先级。RFC 特别注明最终在 #3790 落地的块级粒度方案正是这一类别——因为块级度量本质上把数据块当作固定大小的“桶/窗口”。3. 键区间统计Key Range Statistics——跨层视角前两种方案都只看单表。真实场景中一个连续键区间的墓碑簇可能横跨 LSM 多层单表视角无法感知。该方案希望支持“查询键区间 a→b 是否墓碑密集”若是则压缩与之重叠的表可借助范围注解range annotations实现。RFC 给出了压缩流程草图写表后用version.Overlaps找出与该表键区间重叠的所有表加入全局集合needsTombstoneCheck若检查逻辑足够快也可在写入时直接判定而不引入集合在pickAuto中逐个检查needsTombstoneCheck借助Annotator与既有表统计获取本表与全 LSM 的墓碑数/内部键数输出信号强度可考虑该表墓碑数 / 重叠表的总键数之类的比例并探讨是否显式区分高低层优先级与 read-based 压缩类似压缩前需确认表仍存在可能已被其它压缩移走。4. 最大粒度Maximum Granularity——精确到块的键区间查询若方案 3 中“全 LSM 统计的过估计”成为问题可在每个 SSTable 的 index block 中记录每块的墓碑/键计数从而对任意键区间给出精确计数。这与estimateReclaimedSizesBeneath中区分“部分重叠/完全重叠”的逻辑类似只是把“磁盘用量”换成“墓碑/键计数”在 index entry 中保存每块墓碑累计值后查询复杂度可降至 O(log n) 或更低不含读取 index block 的 I/O。实践建议与注意事项默认即启用按需微调三个选项都有合理默认值100 / 0.5 / 0.10一般负载开箱即用。若观察到墓碑密集压缩过于频繁日志中大量tombstone-density任务可上调TombstoneDenseCompactionThreshold若队列/outbox 型负载仍有读延迟问题可反向调低。关闭入口将TombstoneDenseCompactionThreshold设为 0 或负值即可整体禁用由于EnsureDefaults只在值为 0 时填充默认NumDeletionsThreshold不适合用作总开关。动态调参TombstoneDenseCompactionThreshold是函数类型适合根据运行时工作负载特征动态重配这是它与另外两个静态选项的本质区别。配合既有压缩最低层的墓碑抹除仍由 elision-only 压缩负责墓碑密度压缩不会重复处理最低层两者在pickAutoNonScore中的调用顺序也保证了不会互相抢占。小结Pebble 的墓碑密度压缩启发式把压缩触发信号从“空间回收潜力”扩展到“键空间中的墓碑密度”通过“块级判定 表级比率”的两级度量精准打击了文件大小启发式无法感知的墓碑堆积问题。整个机制闭环清晰写入期逐块计数NumTombstoneDenseBlocks→ 表统计期计算比率TombstoneDenseBlocksRatio→ 选择器按阈值筛选并优先压缩高比率文件 → 尽可能降级为 move 压缩控制写放大。从 RFC 文档 到 compaction_picker.go 的实现、再到 compaction_test.go 与 testdata/compaction_tombstones 的验证本文给出的每一条结论都可以在仓库源码中追溯——这也是阅读和理解 Pebble 压缩体系的一份高质量入口。赞分享KV存储嵌入式数据库【免费下载链接】pebbleRocksDB/LevelDB inspired key-value database in Go项目地址https://gitcode.com/gh_mirrors/pe/pebble点击查看免费下载相关推荐RocksDB 墓碑Tombstone生命周期深度解析从写入、读取可见性到压缩清理与空间回收RocksDB 墓碑Tombstone生命周期深度解析从写入、读取可见性到压缩清理与空间回收 本文基于 docs/components/write_flo数据库KV存储嵌入式数据库存储CubeSandbox Soft-delete 墓碑清理机制解析tombstone 定时清理器的设计与实战CubeSandbox Soft delete 墓碑清理机制解析 tombstone 定时清理器的设计与实战 导读 CubeSandbox 的数据库层大量使用Agent 沙箱虚拟化云原生人工智能后端容器运行时COCA 20K 词汇学习四步自适应提示词20K Vocab Builder 完整复刻指南COCA 20K 词汇学习四步自适应提示词20K Vocab Builder 完整复刻指南 在 GPTs 泄露提示词合集中prompts/20K Vocab提示工程上一篇AndroidScreenAdaptation源码深度解析actualWidth/designWidth这一个公式如何实现全分辨率一致布局下一篇Cppcheck 检查器解析va_list_usedBeforeStarted —— 在 va_start() 之前使用 va_list 的未定义行为检测创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表