CTCBF与DTCBF:冷块过滤如何解决缓冲池缓存命中率暴跌
做数据库内核和存储引擎这些年,最常被问起的一个问题是:缓冲池明明越来越大,为什么线上还是会出现“缓存命中率突然掉底”的情况?排查到最后,十次里有七八次不是内存不够,而是冷数据把热数据挤出去了。CTCBF和DTCBF这两个词,就是在处理这个问题时绕不开的两条技术路线。这篇博文从工程实践角度聊聊二者的核心思路、适用场景、落地细节和调参经验,给正在做存储引擎优化、数据库内核开发或大规模缓存治理的朋友一份可以直接参考的梳理。
1. 为什么需要 CTCBF 和 DTCBF:先看缓冲池的困境
1.1 传统 LRU 在真实负载下的失灵现象
绝大多数数据库和存储系统,最基础的数据页淘汰算法都是 LRU(Least Recently Used)或它的变种。LRU 的核心假设是“最近被访问过的数据,未来被访问的概率更高”,这个假设在绝大多数 OLTP 场景下是成立的。但实际生产负载里有两种常见情况,会让 LRU 暴露出明显缺陷。
第一种是全表扫描或大批量扫描。比如凌晨的 ETL 任务、数仓同步、报表统计,会把一张大表的全部数据页顺序扫一遍。这些页被访问一次之后,短期内大概率不会再被访问,但它们刚刚“最近使用过”,会被放到 LRU 链表的头部。如果扫描的量足够大,就能把真正高命中的热数据页一路挤到链表尾部,然后被淘汰。等白天业务高峰一来,缓冲池里全是那些“一次性的冷页面”,热数据反而要从磁盘重新加载,命中率瞬间崩掉。
第二种是低频大对象的周期性访问。比如某个业务每天只跑一次的全量分析,每次读取几百 GB 的历史数据。这些数据总量远大于缓冲池,每次访问都在“刷池”,对线上正常交易的影响非常明显。传统 LRU 对这类“低频但大批量”的访问几乎没有任何免疫力。
所以问题很清楚:LRU 只看到了“最近是否被使用”,没有区分“这个页到底是核心热数据,还是偶然扫过的冷数据”。这时就需要引入一种额外的识别机制,把冷页面在进入缓冲池时就拦截掉,或者给它们一个更低的保留优先级。CTCBF 和 DTCBF,本质上都是这种“冷块过滤(Cold Block Filtering)”思路的具体实现,区别在于“如何定义冷”和“如何动态调整判定标准”。
1.2 冷热数据识别的两条技术路线
CBF(Cold Block Filtering)并不是某一个具体的算法,而是一类策略的统称。这类策略专门回答一个问题:在不牺牲热数据命中率的前提下,如何以极小的代价识别并过滤掉“看起来会只被访问一次”的冷数据块。
围绕这个问题,工程上延伸出两条典型路线。一条是基于聚类与计数的识别路线,也就是 CTCBF 这类思路——把数据块按访问来源、访问模式、所在区间聚成“簇”,统计每个簇的访问频率和热度,簇级别就做好冷热分档。这样做的好处是识别比较精准,能捕捉“某一片区域的数据总是成批被扫”这样的规律。
另一条是基于动态阈值与时间感知的识别路线,也就是 DTCBF 这类方向——不再固定认为“访问次数低于 N 就是冷”,而是根据当前负载特征、时间窗口、历史命中反馈动态调整冷热判定阈值,像温度一样让热度随时间“衰减”,同时又对突发流量保持快速响应。
这两条路线不是互斥的,实际生产系统里经常会把它们揉在一起用。但先清楚各自的出发点,后面看实现和调参才能不迷路。
2. CTCBF:以聚类与计数为核心的冷块过滤思路
2.1 核心机制拆解
CTCBF 里“CT”这部分,重点是 Clustering 和 Counting。它的基本逻辑分三步:把数据块划分到不同的“簇”中,维护每个簇的访问计数,然后根据簇的计数决定簇内数据块的淘汰优先级。
这里的关键词是“簇”。为什么不直接统计每个数据页的访问次数,而要先做聚类?我刚开始自己实现时也觉得多此一举,后来在真实负载里验证过,直接按页统计有一个很头疼的问题:页级别的计数太碎,噪声太大。一个频繁扫描区间的页可能每次命中一个不同的页,单页计数都上不去,但整个区间明显是“冷扫描”。反过来,一个热点簇里的某个页可能由于某种原因短期内没被访问,单看这个页会把它误判成冷页,但它所在的簇明显是热的。
聚类的价值就在于把“个体噪声”收敛成“群体信号”。具体实现时,聚类维度可以根据存储格式灵活选择。比如行存表可以按“段(Segment)”“区(Extent)”聚类;列存表可以按“行组(Row Group)”;对象存储场景可以按“前缀目录”或者“对象大小区间”。选簇的原则是:簇内数据在访问行为上尽可能相似,簇间尽可能不同。
在每轮统计周期内,每个簇维护一个计数器,记录该周期内簇内页被访问的总次数。判定冷热时先看簇级别,如果簇被判定为“冷簇”,簇内的数据页在淘汰排序时会被直接压到最低优先级;如果簇被判定为“热簇”,内部再按页级 LRU 或访问频次做细粒度排序。这就是“簇级粗筛、页级细排”的两级结构。
2.2 实现层面需要注意的细节
计数器是 CTCBF 最容易翻车的地方。如果不加衰减,计数器会无限增长,历史累积次数会让“曾经的访问”主导当前的冷热判断,导致热度僵化。比如某个簇三个月前被疯狂扫描过,计数冲得很高,之后再也不碰它,但它的计数仍然碾压其他新产生的热簇,数据就一直赖在缓冲池里不出去。
解决办法有两个方向。一是滑动窗口计数,只保留最近 N 个时间窗口内的访问次数,超过时间就丢弃,实现上可以用环形数组或者时间戳桶。二是指数衰减,每个统计周期结束时把计数器乘以一个小于 1 的衰减因子(比如 0.8),让旧的访问痕迹按指数级淡出。滑动窗口更直观、好解释,但要占更多内存;指数衰减省内存,但要小心衰减因子的选择——太接近 1 会让历史影响持续过久,太小又会让热度记忆太短。我自己的习惯是先上指数衰减,因为内存开销好估算,调试时观察热度变化曲线也更平滑。
内存开销方面要会算账。如果全量统计到页级,假设有 1 亿个页,每个计数器用 4 字节(uint32),光计数器就要 400MB,这还没算哈希表本身的结构开销。所以 CTCBF 在实现时通常只统计到簇级,假设簇数量 10 万个,每个簇一个计数器,内存开销可以压缩到几百 KB 到几 MB 这个量级,这才是可以接受的。
布隆过滤器也可以做个“先遣部队”。在数据页真正读入缓冲池之前,先用布隆过滤器判断这个页是否属于已知的“历史热簇”。如果命中布隆过滤器,说明可能是热的,正常放行;如果没有命中,说明大概率是冷数据,可以标记为“低优先级”再放入。这样连计数器的访问路径都可以省掉一部分,代价是布隆过滤器本身有误判率,需要把误判率控制在 1% 以下。
2.3 适用场景与不适用场景
CTCBF 比较适合访问倾斜度明显的业务场景。比如订单中心,绝大多数流量都打在小部分近期订单上,历史订单的访问则呈现“按天/按月成批扫描”的规律。这时候把订单表按“日期分区 + 分区内按主键范围聚类”,CTCBF 能非常精准地拦截掉历史月份的批量扫描,同时保证近期订单数据全部留在缓冲池。
再比如推荐系统的 Item 特征库,热门 Item 的访问频率和长尾 Item 差几个数量级,用 CTCBF 可以把长尾特征页非常激进地挡在缓冲池外,只保留头部热 Item 特征,命中率和资源利用率同时提升。
但是 CTCBF 也有明显不擅长的地方。如果业务负载是全随机访问,每个页被访问的概率都差不多,没有明显的聚类特征,那任何聚类策略都失效了,CTCBF 反而会因为维护簇计数而白白耗费内存。还有一种情况是访问模式变化特别剧烈的场景,比如秒杀、大促、热点突然转移,分了簇之后热点簇可能会在一瞬间整体变化,而簇计数器的更新有一定的滞后性,容易出现“旧热簇没退、新热簇没进”的尴尬期。
所以 CTCBF 适合“规律明显、倾斜明显”的负载,不适合“完全随机、突变剧烈”的负载。在选型时,先看清楚业务的访问分布再决定要不要上。
3. DTCBF:动态阈值与时间感知的自适应冷块过滤
3.1 与 CTCBF 的核心差异
DTCBF 的“DT”指的是 Dynamic Threshold(动态阈值)。如果说 CTCBF 是在“空间维度”上通过聚类发现冷热模式,DTCBF 则更多在“时间维度”上通过热度的动态变化来做判断。
DTCBF 与 CTCBF 最本质的区别,是它不提前假设“什么算冷、什么算热”。CTCBF 的阈值往往相对固定——比如“窗口内访问次数低于 5 次就算冷簇”,这个 5 是人工拍出来的。DTCBF 会让阈值跟着系统当前的负载状态走:当缓冲池整体命中率下降时,判定“热”的门槛会自动抬高,更积极地保护那些被频繁访问的页;当系统很空闲,缓冲池充足时,判定“热”的门槛又会适当放宽,给更多数据页保留机会。
这种自适应特性在负载潮汐变化明显的系统里特别有价值。我以前维护过一个在线分析系统,白天是几万个并发用户做交互式查询,晚上是自动化的全量批处理任务,两班负载的访问模式完全不一样。如果用一个固定阈值,只能照顾其中一端的负载。用 DTCBF 的动态阈值,系统能在两种形态之间自动切换,这是固定策略做不到的。
3.2 关键技术点拆解
DTCBF 要实现“动态”,至少需要解决三个技术点。
第一,冷热判定需要结合时间衰减。 DTCBF 通常会维护一个“温度值”,每个数据块或簇在被访问时温度增加,然后随时间按指数衰减。这个温度值和“最近访问时间”是强相关的,比单纯的次数统计更能反映真实热度。衰减的实现可以周期性扫描,也可以在每次访问时惰性计算,后者性能更好,但要保存每个块的最后访问时间戳。
第二,动态阈值要有反馈闭环。 阈值不能是拍脑袋,而是要根据系统表现自动调整。最常见的做法是用“命中率反馈”:如果最近一个时间窗口内,缓冲池命中率持续低于目标值,就把热判定阈值上调,意味着只有更热的块才能留下;如果命中率达标且还有空闲缓冲空间,就下调阈值,多留一些中间地带的块。这个反馈周期很关键,太短会震荡,太长则反应迟钝。一般建议以分钟级窗口做粗调,以秒级采样做监控,两层配合。
第三,要处理突发流量。 纯时间衰减的模型有一个问题:如果一个冷块在短时间内被大量访问,它的温度还没来得及升上来,就可能会被淘汰,也就是“冷启动期”。DTCBF 的工程实现里通常会加一个“快速升温”机制:比如单个时间片内访问次数超过某个突发阈值时,温度直接乘以一个更大的增量系数,让它迅速进入热区。这个机制对秒杀、热点事件这类流量非常重要,我在实践里看到过很多次因为缺这个机制,导致热点数据在升温期反复被淘汰、反复从磁盘读取,性能雪崩。
3.3 参数选择与调优
DTCBF 的调参比 CTCBF 更麻烦,但也更有意思。几个关键参数我整理了实用的设定建议:
| 参数 | 含义 | 建议初始值 | 调整方向 |
|---|---|---|---|
| 衰减半衰期 | 温度衰减到一半所需的时间 | 5~15 分钟 | 访问模式越稳定,半衰期越长;突发越多,半衰期越短 |
| 温度初始值 | 新数据块进入时的初始温度 | 当前平均温度的 50% | 偏高则冷块滞留增多,偏低则容易被过早淘汰 |
| 突发升温倍数 | 短时间多次访问时的温度增量放大系数 | 3~5 倍 | 热点突变场景调到 8 倍以上 |
| 反馈窗口 | 命中率反馈的计算窗口 | 60 秒 | 负载波动大则缩短,追求稳定则延长 |
| 阈值步长 | 每次命中率反馈后阈值调整的幅度 | 5%~10% | 步长过大会震荡,过小则自适应速度慢 |
这组参数我最早的版本全是靠线上事故调出来的。衰减半衰期一开始设成 1 小时,结果白天热点转移后旧热点温度迟迟不降,新热点又进不来,命中率一直在低位徘徊。后来改成 10 分钟,效果立竿见影。但也不是越短越好——如果把半衰期压到 1 分钟,一条慢查询在 3 秒内扫描的热点区,可能刚加热完就被浇灭了,等于 DTCBF 完全失效。所以参数选择一定要回归业务负载特征。
另外要注意,DTCBF 的计算模型比 CTCBF 复杂不少。温度值如果精确到每个页,内存开销会非常大;如果只做到簇级,又会丢失页级差异。折中做法是“簇级保存温度,页级保存最后访问时间”,用的时候按簇温度做粗排,页级时间戳做细排。这个两级结构与 CTCBF 类似,但 DTCBF 的排序依据是动态变化的“温度”而不是“访问次数”,后者更灵敏。
4. 两者的对比与选型建议
4.1 核心差异对比
我把 CTCBF 和 DTCBF 放在一个表里对比,这张表是我做技术方案评审时常用的,结构比较一目了然:
| 对比维度 | CTCBF | DTCBF |
|---|---|---|
| 核心思想 | 按访问模式聚类,统计簇计数 | 按时间维度计算温度,动态调整阈值 |
| 冷热判定依据 | 聚类后的访问次数 | 温度值 + 动态阈值 |
| 主要优势 | 精准识别“区域冷扫描”,簇级信号稳定 | 自适应负载变化,对突发流量更友好 |
| 主要劣势 | 阈值相对固定,负载剧变时反应慢 | 实现复杂,参数多,调优成本高 |
| 内存开销 | 低(只需要簇级计数) | 中(需要温度值 + 时间戳) |
| 调参难度 | 中(聚类粒度、窗口大小) | 高(半衰期、反馈窗口、步长多参数耦合) |
| 适合负载 | 倾斜明显、规律清晰的场景 | 潮汐变化、热点频繁转移的场景 |
| 典型形态 | 离线分析 + 批量扫描伴生的 OLTP | 高并发交互 + 热点事件伴生的在线服务 |
这张表不说明谁一定比谁强,只是两个不同的“性格”。CTCBF 像一个守规矩的老员工,有稳定的分类标准,按部就班地执行;DTCBF 像一个反应快的新人,会根据环境自动调整自己的策略,但需要有人盯住他,防止反应过度。
4.2 我个人的选型建议
选型不能只看算法本身,要看业务访问模式和管理成本。
如果你的业务访问模式相对固定,比如电商的订单库、金融系统的流水表,热点永远集中在“最近一个月”,冷数据永远来自“历史归档区”,那我建议优先考虑 CTCBF。因为这种场景规律性太明显,用聚类 + 计数就能以极低的复杂度解决问题,完全没必要引入 DTCBF 的动态模型。
如果你的业务有明确的波峰波谷,比如报表系统白天高并发查询、晚上跑批,或者经常有突发热点,比如新闻、短视频的瞬时爆款,那就应该优先考虑 DTCBF。它能自动调整阈值,在两个负载形态之间平滑切换,省去人工干预的成本。但是,前提是你有足够的时间去盯参数,并且有配套的可观测性手段,否则动态参数一旦失控,排障会比固定策略困难得多。
还有一个思路值得参考:把两者串起来用。外面一层用 CTCBF 做粗糙的“区域级冷热分档”,把明显的大批量冷扫描先挡住;里面一层用 DTCBF 做精细的“页级温度排序”,处理热点区域内的动态变化。这种分层混合设计在大型存储引擎里很常见,每一层都做自己擅长的事,整体效果比单独用任何一种都好。代价是实现复杂度更高,适合真正要做底层优化的团队。
4.3 落地时的工程建议
实际接入这两类策略时,有几个工程细节比算法本身更影响成败。
第一,先埋点再上策略。不要一上来就改淘汰逻辑。先在现有缓冲池的读路径上记录访问日志,统计页/簇维度的访问分布、LRU 链上的老化速率。有了这些基线数据,你才能知道自己是不是真的需要 CTCBF 或 DTCBF,以及该把阈值调到什么量级。我见过不少团队兴冲冲上了复杂的淘汰算法,结果发现瓶颈根本不在缓冲池淘汰策略上,而在 IO 调度和锁竞争,白忙活一场。
第二,给策略加开关和灰度能力。CTCBF/DTCBF 的代码最好做成可配置的,运行时可以动态切换回传统 LRU。上线时先在小流量实例上验证命中率曲线,确认没有异常再逐步扩展。绝不要第一次就在大促前直接全量上线,这种事故我见过不止一次。
第三,关注策略本身的计算开销。冷块过滤是在数据页读取路径上执行的,每次访问都会触发判断逻辑。如果判断逻辑里做了复杂的哈希查找或温度计算,就要注意对热路径的性能影响。常规做法是把这个判断做成 O(1) 的哈希表查找 + 少量的整数运算,避免在热路径上出现任何磁盘 IO 或锁竞争。
5. 实操中的常见问题与排查技巧
5.1 命中率不升反降
这是一个最诡异但也最常遇到的问题——明明上了冷块过滤,命中率却比原来更差了。排查思路先看误判。CTCBF 聚类粒度如果太大,可能把包含大量热页的簇整体判成冷簇,导致热页被误杀;DTCBF 的半衰期如果太短,则可能让热页温度来不及积累就被淘汰。
排查方法很直接:在缓冲池命中率下降的时间点,抓取淘汰日志,看被淘汰的页里有多少在淘汰后的短时间内又被重新访问了。这个比例如果超过 10%,说明策略的“误杀率”偏高,需要放宽冷热判定边界。我看到过的最极端案例,误杀率到了 30%,那已经不是优化而是事故了。
5.2 淘汰抖动
抖动指的是某些数据块刚被淘汰,立刻又被访问并重新加载,然后又被淘汰,反复在内存和磁盘之间横跳。这种情况在 DTCBF 的参数设置不当时特别明显,尤其是“反馈窗口”和“阈值步长”配合不好,阈值一会儿抬高一会儿降低,导致系统状态在两个阈值之间震荡。
解决抖动的两个实用技巧:一是给淘汰加一个“冷却期”,一个块被淘汰后,在 N 秒内不允许再次因为“冷”被淘汰,给它一个充分升温的机会;二是把反馈调整做成“只升不降”或“分阶段升降”,短时间内不要反复切换方向。这两个小技巧在工程实现里都是几行代码的事,但对稳定性的提升非常明显。
5.3 内存开销超出预期
很多人在设计时只估算了计数器/温度表本身的大小,忽略了哈希表的负载因子、缓存行对齐、并发控制的额外结构。实际落地时内存可能翻 2~3 倍。
一个比较实用的兜底手段是给冷块过滤的元数据设一个内存预算。当元数据内存超过预算时,自动降级——比如从“全量统计”退化为“抽样统计”,或者把哈希表直接淘汰,退回传统 LRU。很多系统在内存紧张时宁可牺牲策略精度,也不能让元数据本身成为新的内存压力源。
5.4 与预读机制互相干扰
预读(Read Ahead)是存储引擎常见的优化手段,顺序扫描时会提前读入多个数据页。问题在于,预读进来的页很多是“还没被真正访问”的,但它已经占用了缓冲池空间。如果冷块过滤逻辑没有正确处理预读页,可能出现两种情况:一是预读页被当场判冷,预读功亏一篑;二是预读页被当成热点保留,挤占了真正的热页空间。
解决办法是在预读页的标记上做文章。预读页在读入时打上特殊标记,在冷块过滤时给予一次“豁免权”,等下一次真正被访问时再参与正常的冷热统计。这样既不影响预读的效果,又能在后续访问中准确评估预读页的真实热度。
5.5 小验证环境中的可复现测试
很多团队在实际部署前,都想先在测试环境里验证 CTCBF/DTCBF 的效果,但经常发现测试结果和生产不一致。原因通常出在负载特征上。测试环境如果只是用 sysbench 或简单的 TPCC 压测,访问模式太均匀,根本无法体现出冷块过滤的优势。
要复现有效测试,需要先在测试环境把“冷扫描污染”这个场景造出来:准备一张大数据量表(比如 10 倍缓冲池大小),同时跑两条负载,一条是持续的点查热点小表,另一条是周期性全表扫描大表。观察没有冷块过滤时点查命中率的下滑程度,再对比启用了 CTCBF 或 DTCBF 之后的效果。这个对比实验才是冷块过滤策略的真正价值证明,比只看单条 SQL 耗时靠谱得多。
6. 最后想说的
说实话,CTCBF 和 DTCBF 都不复杂,核心概念半天就能讲完,真正难的是在真实的业务负载里把它们调到一个“刚好”的状态。我在没有可观测系统的阶段吃过很大亏——策略一上,命中率曲线跟心电图一样跳,根本分不清是参数问题还是负载波动。后来老老实实把访问追踪、淘汰日志、耗时分布都做了出来,调参才变成一件有方向感的事。
所以我最想分享的一条体会是:别指望靠某个算法一步到位,先把自己的系统看清楚。CTCBF 也好,DTCBF 也好,本质上都是在帮你更精确地理解“哪些数据真正值得留在内存里”。这个理解到位了,选哪个、怎么调,都是水到渠成的事。要是你正在做这块工作,我建议先从上面说的埋点观察做起,哪怕最终你决定不上任何冷块过滤策略,光是把访问特征摸清楚这件事实,就已经值回票价了。