成本和稳定性 Serverless 全都要:热页驱动的内存弹性实践
编辑标准与来源政策: 编辑标准, 团队. 内容均链至原始来源,见 方法论.
业务上云之后,流量起伏成了常态。一次营销活动、一条传播开的内容、一个突发热点,都可能让数据库在几分钟内承受平时数倍的压力。TDSQL-C Serverless 基于计算存储分离架构,按负载自动扩缩容、按实际用量计费,正是为这类场景而生。
Serverless 的弹性扩缩容,本质就是针对 CPU 和内存扩缩的精密控制。这其中,CPU 的弹性相对容易管理控制:并发上来,CPU 消耗随之上升,调度系统可即时做出准确判断。内存则不同:内存里的数据对调度系统不透明,只能从数据库暴露的运行状态去推断业务需要多少内存。如果推断偏小,业务会变慢;反之,推断偏大,客户要为闲置的内存付费。
为了更好地解决这一难题, TDSQL-C Serverless 在内存弹性上进行了一项核心升级:在原有的缓存命中率、内存使用率之外,引入页面热度作为弹性决策的核心信号,让系统能够直接度量“内存里有多少负载业务真正在使用”。对企业级客户而言,这意味着内存弹性不再需要在成本和稳定性之间做取舍:流量上涨时,资源在业务感知到延迟之前就已扩展到所需量级;流量回落时,及时有效发起缩容,同时精确释放业务不需要的冷数据,在保障稳定性、准确性的基础上,进一步为企业级客户极致降本。
| 1 | 内存弹性的核心难题:让 Buffer Pool 始终 100% 贴合业务 |
数据库实例的内存由几部分组成:连接与会话、排序和连接缓冲、临时表、锁与元数据等内核结构,而占整体内存比重最大的是 Buffer Pool(缓冲池)。Buffer Pool 是 InnoDB 在内存中维护的数据页缓存,查询要读的数据页如果在 Buffer Pool 里,就直接从内存返回;如果不在,就要去存储层读取。TDSQL-C 采用计算存储分离架构,数据页保存在远端共享存储上,一次缓存未命中(下文称“穿透”)需要走一次网络 IO,耗时比内存访问高出几个数量级。当 Buffer Pool 使用量达到最大值后,Buffer Pool 会按照 LRU 的访问规则淘汰旧的页面,这时如果访问到了旧页面,就会发生穿透。

图1 Buffer Pool 运行架构概览
对业务来说,穿透变多首先体现为查询变慢。单次穿透多出来的是毫秒级的等待,但一条 SQL 往往要读几十上百个数据页,延迟会成倍放大。查询变慢之后,连接占用时间变长,并发请求开始排队,在流量高峰期还可能出现超时和重试,重试又进一步推高负载。对交易、支付、在线服务这类对响应时间敏感的业务,延迟的增加会很明显。但是,如果在业务处于流量高峰时,请求的并发量恰巧不足以让 CPU 负载上升到超过扩容阈值(IO 密集型场景尤甚),数据库就会长时间处于请求穿透的高延迟状态。
要避免这种情况,Buffer Pool 的大小需要跟上业务工作集(当前被频繁访问的数据集合)的变化。具体来说有两个要求:
● 流量上升时提前扩容:在工作集超出 Buffer Pool 容量、穿透开始增多之前,把内存补上;
● 流量回落时及时缩容:
○ 业务不再需要的内存尽快释放,避免客户为闲置资源持续付费;
○ 缩容不能误伤业务近期还会访问的数据,否则刚缩完就会迎来一波穿透。
两个要求归结到一点:调度系统需要一个能反映业务此刻真实需要多少内存的信号。
| 2 | 后验指标在内存变动预测上的局限性 |
检测内存变动的常见做法是用 Buffer Pool 的缓存命中率和内存使用率组合判断。这两个指标都是 MySQL 自带的系统指标,命中率是读请求中由内存直接满足的比例,可以根据 MySQL 标准状态变量 Innodb_buffer_pool_read_requests 与 Innodb_buffer_pool_reads 按采样周期计算;使用率是 Buffer Pool 中已被数据页占用的比例。命中率偏低且使用率接近满载,说明内存已满、请求发生穿透,需要扩容;命中率很高,说明内存够用,可以考虑缩容。这套组合在直觉上没有问题,但在生产环境中有三个明显的局限。
局限一:命中率和使用率都是满的,看不出有多少内存在闲置。 InnoDB 不会主动归还空闲的数据页,读过的页会一直留在 Buffer Pool 里,直到被新数据挤出去。一个实例凌晨跑过一轮报表,或者做过一次全表扫描,Buffer Pool 就会被这些一次性访问的页填满。之后业务回到日常状态,只访问少量热点数据,这些页仍然占着内存。
设想两个 8 GB Buffer Pool 的实例:一个里面有 7 GB 是业务正在访问的数据,另一个只有 200 MB,其余 7.8 GB 是报表留下的残留页。两个实例的命中率都接近 100%,使用率也都是 100%,读数完全一样。但第二个实例有超过 97% 的内存在空转,而客户一直在为它付费。
局限二:少量离散请求会拉低命中率,引发误扩容。 命中率是整个实例的平均值,不区分请求来源。一个后台任务每秒扫描几个冷门分区,就会持续产生穿透,把命中率压到 98% 以下;与此同时 Buffer Pool 又是满的,扩容条件成立。可实际上,这段时间 99% 以上的业务请求都在内存中命中,业务延迟没有任何变化。更麻烦的是,这些离散请求访问的数据本来就不该常驻内存,扩容之后它们照样穿透,命中率回不到阈值以上,系统会继续扩,直到触达实例配额上限。
局限三:命中率是事后反馈,不能提前预判。 命中率的分子是已经发生的未命中,它记录的是结果。我们把一次扩容的过程展开:
1. 业务工作集开始增长,Buffer Pool 逐渐被填满,直至工作集超出容量,InnoDB 开始淘汰仍在使用的数据页,被淘汰的页再次被访问,产生穿透,业务延迟从这里开始上升;
2. 穿透在一个采样周期内持续满足一段时间,命中率才跌破阈值,扩容才会下发;
3. 扩容完成后,被淘汰的数据还要重新从存储读回,命中率逐步恢复。
从穿透产生开始,业务一直在承受额外的延迟。而且因为命中率只能说明不够,说明不了差多少,扩容幅度只能按当前规格翻倍,扩多了浪费,扩少了还要再来一轮。负载上升越快,这段窗口越容易演变成业务侧的排队和超时。本质上,用命中率驱动扩缩容是一种反馈控制:等偏差出现,再去纠正。要做到提前扩容,需要的是一个前置的、能反映内存需求本身的指标。
三个局限的根源是同一个:命中率和使用率描述的是内存变化的现象,而弹性决策需要知道的是业务实际的内存需要。
| 3 | 页面热度:针对业务实际使用的直接度量 |
TDSQL-C 新版内核内置了页面热度管理器,根据每个数据页最近一次被访问的时间,给 Buffer Pool 中的页标记温度:
● 热页:最近 60 秒内被访问过,业务正在活跃使用的数据。
● 温页:最后访问在 1 ~ 10 分钟之间,近期用过但当前不活跃的数据。
● 冷页:超过 10 分钟未被访问,可优先淘汰的数据。

图2 页面热度判定与动态传递
数据页每次被访问(无论命中还是穿透)都会记录访问时间。记录操作写入线程本地缓冲、批量提交,不在查询路径上加锁,对业务性能的影响可以忽略。内核后台线程每秒巡检一次,把超过热页窗口的页降为温页,把超过温页窗口的页标记为冷页;温页和冷页一旦被再次访问,会立即回到热页。
节点上的采集组件以秒级粒度读取热页数、温页数和总页数,与 CPU 使用率一起上报给调度系统。弹性决策使用的核心指标是热页占比,即热页数占 Buffer Pool 总页数的比例,它直接反映了内存里有多大比例是业务正在用的。
回到 Part.2 的两个场景:
● 大报表残留页占满内存:这些页超过 10 分钟没有被访问,已经全部是冷页,热页占比只有百分之几。系统可以直接判断内存严重过剩,并算出缩容目标。
● 少量离散请求持续穿透:这些请求访问的页也会计入热页,但数量很少,热页占比远低于扩容阈值,不会触发误扩容。
热页占比并不是要取代命中率和使用率,三类信号在弹性决策中各有分工:
● 热页占比:直接感知业务正在使用多少内存。内核支持页面热度时,作为内存扩缩容的主判据,并用于计算目标规格。
● 缓存命中率 & 缓存使用率:作为后置判定指标,感知内存是否已经不够用。内核不支持页面热度时自动接管内存决策;同时作为监控中的结果校验。
● CPU 使用量:实时感知算力是否够用。与内存信号共同参与扩缩容判断,扩缩容时要求 CPU 同时满足条件,覆盖高 CPU 或 I/O 稀疏型场景。
| 4 | 扩容:前置弹性,赶在穿透发生之前 |
当前,Serverless 采用的扩容判定条件为:热页占比超过 80% 并持续 5 秒,系统即触发扩容。
和命中率相比,最大的区别在于触发的时间点。数据页在被访问时就会计入热页,不需要等到它被淘汰、再被重新读回。

图3 基于热页占比的前置弹性
业务工作集增长的过程中,热页占比持续上升,达到 80% 时内存仍然够用,命中率仍是 100%,扩容就已经发起。等工作集继续增长到装不下的程度,扩容早已完成,业务侧不会感知到延迟变化。
扩容的幅度也可以精确计算。调度系统根据当前的热页规模反推所需的 Buffer Pool 容量,目标是让扩容后的热页占比回到 安全的中间态。

图4 Buffer Pool 扩缩容分区界定
目标选在区间中点,是为了给后续波动留出余量:扩容后既不会很快再次触发扩容,也不会马上满足缩容条件,规格不会来回震荡。扩容瞬间热页数量不变,Buffer Pool 变大后热页占比按比例下降,正好落在这个区间里。
作为场景补充,由 CPU 以及缓存命中率触发的扩容,仍然会按照原有规则参与扩缩容决策。
| 5 | 缩容:一步到位,冷页精准释放 |
释放资源比补充资源更谨慎。扩容时任一指标满足条件即可触发;缩容则要求热页占比低于 30%,并且 CPU 使用量也满足缩容条件,两者同时持续 60 秒后才会执行,避免短时的负载波动引起规格抖动。
缩容的目标规格同时参考两个值,取其中较大者。第一个值是缩容后热页占比回到安全区间所需的 Buffer Pool 容量,通过当前的热页占比计算。第二个值是当前热页与温页的实际总量,这个指标存在的核心目的是进行温页保护:温页是最近几分钟访问过、当前不活跃的数据,但是业务很可能马上还会用到。如果只按第一个值缩容,在温页占比较大时,部分温页可能会在缩容时被集中淘汰,业务随后就会遇到一波穿透和延迟尖峰。以热页和温页的总和作为容量下限,缩容后的内存至少能装进近期活跃过的全部数据。
因为目标可以计算推演,缩容一次就能定位到终点。再来回看前面那个 8 GB Buffer Pool 的例子。这个 Buffer Pool 中,即使只有 200 MB 热页,缩容也可以将最小规格作为目标直接一步到位,完成下发。
执行时,缩容采用渐进的方式:按固定的小步长逐步缩小,每一步等数据库完成内存释放后再进行下一步,避免一次性释放大量内存对实例造成冲击;同一节点在同一时刻只执行一个扩缩容任务,防止多个动作叠加。

图5 缩容界定与流转闭环
结语
内存弹性的准确度,取决于调度系统能否如何有效感知业务真正的内存负载。缓存命中率和内存使用率反映的是内存使用的结果:内存被残留数据占满时,它们发现不了浪费;少量离散穿透会让它们误判;而且只有在穿透发生之后,命中率才会变化。页面热度在数据页被访问时就完成统计,为 TDSQL-C Serverless 带来了三点改进:
● 扩容更早:在命中率下降之前触发,业务在性能受影响之前就拿到资源;
● 扩容更准:按热页规模计算目标规格,一步到位,落到安全区间;
● 缩容更稳:一次定位到实际需要的容量,温页保护避免缩容后的延迟抖动,等待时间从 10 分钟缩短到 1 分钟。
通过更早触发预判、更准定位需求、更稳平滑收缩的极致弹性能力,我们在保障业务零感知、无抖动的前提下,实现了性能与成本的最佳平衡,让 Serverless 真正成为兼具高可用与高性价比的坚实底座。