数据库基础体系 · 第 78/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

数据库容量规划:工作集、缓存命中、IOPS、连接、增长和压测

数据库容量规划不是简单地把“当前数据量”乘以一个安全系数。数据库能否满足 SLO,取决于一组相互制约的量:

  • 请求在一个时间窗口内访问哪些数据,即工作集
  • 内存缓存能否保留这些数据,即缓存命中
  • 未命中的访问如何转化为存储读写,即 IOPS、吞吐和延迟
  • 同时有多少请求、事务和连接,即并发与连接
  • 数据、索引、WAL 或 binlog、临时文件和副本如何增长;
  • 在真实数据分布和事务语义下,系统何时进入排队、抖动或故障。

容量规划的目标不是预测一个永远正确的数字,而是建立从业务请求到数据库资源的可验证模型,并通过监控和压测不断修正它。


一、先定义容量:不是“能存多少”,而是“在 SLO 下能承受多少”

数据库容量至少包含三个不同问题。

1. 存储容量

存储容量回答:

数据库能否保存未来一段时间的数据、索引、日志、临时文件和运维余量?

它主要受以下对象影响:

  • 表数据;
  • 二级索引、主键索引和内部元数据;
  • PostgreSQL 的 WAL,MySQL InnoDB 的 redo log、undo 相关空间以及 binlog;
  • 排序、哈希、临时表产生的临时文件或临时表空间;
  • MVCC 旧版本、死元组或尚未 purge 的 undo;
  • 备份、归档和复制保留所需的空间。

2. 性能容量

性能容量回答:

在指定读写比例、请求大小、并发量和 SLO 下,数据库每秒能处理多少请求或事务?

性能瓶颈可能来自:

  • CPU;
  • 内存带宽;
  • 数据库缓存;
  • 存储 IOPS;
  • 存储带宽和单次 IO 延迟;
  • 锁、事务冲突和日志刷盘;
  • 连接管理;
  • 复制延迟;
  • SQL 执行计划。

3. 故障与增长容量

故障和增长容量回答:

发生流量峰值、缓存冷启动、主从切换、批量任务或磁盘故障时,系统是否仍有恢复空间?

因此,容量规划的基本约束可以写成:

实际负载SLO 允许的系统容量\text{实际负载} \leq \text{SLO 允许的系统容量}

其中系统容量不是单一数字,而是 CPU、内存、IOPS、连接数、锁等待、复制延迟和存储空间共同决定的最小值。

例如,存储还能提供 20 万 IOPS,并不意味着数据库能承受 20 万次请求每秒。每个请求可能执行多次索引访问、数据页访问和 WAL 写入;同时 CPU、锁和连接池也可能更早成为瓶颈。


二、工作集:决定缓存是否有意义的访问集合

2.1 工作集不是数据库总大小

**工作集(working set)**是某个时间窗口内,业务请求高概率访问的数据页、索引页和相关元数据的集合。

设数据库中的页集合为 PP,在观察窗口 TT 内访问页 pp 的次数为 ap(T)a_p(T),则可以按访问次数或访问概率定义工作集。例如,以访问频率阈值 θ\theta 定义:

Wθ(T)={pPap(T)qPaq(T)θ}W_\theta(T)=\{p \in P \mid \frac{a_p(T)}{\sum_{q \in P}a_q(T)} \geq \theta\}

这个定义表达了两个重要事实:

  1. 工作集与时间窗口有关;
  2. 工作集是访问模式的属性,不是表大小的别名。

一个 10 TB 的历史订单库,最近 30 天可能只有 200 GB 被频繁访问;但如果业务每天运行一次全表报表,报表执行期间的工作集又会暂时接近整个表和索引。

2.2 工作集通常分层

实际系统经常同时存在多个工作集:

  • 热点工作集:例如最近几分钟的订单、库存和会话;
  • 业务工作集:例如最近 30 天的订单;
  • 批处理工作集:夜间报表或对账涉及的大范围数据;
  • 索引工作集:查询可能只访问索引页,不立即访问表数据页;
  • 写入工作集:新增记录对应的数据页、索引页和日志缓冲。

因此,“内存大于某张表”并不自动意味着查询都会命中缓存。查询可能访问的是多个索引、随机数据页和临时结果;反过来,内存小于整张表,也可能因为热点高度集中而获得很高的命中率。

2.3 一个完整的工作集例子

假设某订单查询执行以下访问:

  1. user_id 查二级索引;
  2. 通过索引定位订单行;
  3. 回表读取订单页;
  4. 读取订单明细;
  5. 写入访问日志。

在一个时间窗口内,逻辑页访问统计如下:

页类别 逻辑访问次数/秒 说明
用户订单索引 40,000 热点用户反复访问
订单数据页 30,000 随机回表
订单明细索引 20,000 查询明细
订单明细数据页 10,000 访问频率较低
WAL/redo 相关写入 另计 不等于数据页读取

如果缓存能保留前两类热点页,它可能已经消除大部分物理读;如果缓存只能保留索引,却无法保留订单数据页,索引命中率看起来很高,查询延迟仍可能很差。

这也是“索引命中率高”不能直接推出“查询性能好”的原因。


三、缓存命中:逻辑访问如何减少为物理访问

3.1 逻辑读与物理读

数据库执行器产生的是逻辑页访问:它需要某个数据页或索引页。

如果该页已经在数据库缓存或可用的操作系统页缓存中,系统可以直接使用;否则需要从存储设备读取,这才形成物理读。

设:

  • LL:单位时间逻辑读次数;
  • HH:缓存命中率;
  • M=1HM=1-H:缓存未命中率;
  • RR:单位时间物理读次数。

理想化情况下:

R=L(1H)R=L(1-H)

例如:

L=100,000,H=95%L=100{,}000,\quad H=95\%

则:

R=100,000×(10.95)=5,000R=100{,}000\times(1-0.95)=5{,}000

如果命中率从 95% 提升到 99%,物理读变为:

100,000×(10.99)=1,000100{,}000\times(1-0.99)=1{,}000

物理读减少了 80%,不是“命中率只提高了 4 个百分点”那么简单。

3.2 命中率的统计口径必须明确

不同引擎、不同指标的统计范围可能不同:

  • 数据库缓存在共享内存中的命中;
  • 操作系统页缓存命中;
  • 存储设备控制器缓存命中;
  • 某个实例、数据库、表或索引的命中;
  • 累计统计还是某个时间窗口内的统计。

以 PostgreSQL 为例,pg_stat_database 中的 blks_hitblks_read 可用于观察数据库统计口径下的块命中和读取:

SELECT
    datname,
    blks_hit,
    blks_read,
    CASE
        WHEN blks_hit + blks_read = 0 THEN NULL
        ELSE blks_hit::numeric / (blks_hit + blks_read)
    END AS buffer_hit_ratio
FROM pg_stat_database
WHERE datname = current_database();

这个比值是:

blks_hitblks_hit+blks_read\frac{\text{blks\_hit}} {\text{blks\_hit}+\text{blks\_read}}

它是累计值。若系统刚启动、统计刚重置,或业务模式刚改变,结果不能代表稳定状态。更重要的是,命中率高只能说明很多块访问不需要从更低层次读取,不能证明执行计划合理、锁等待低或延迟满足 SLO。

MySQL InnoDB 可以观察缓冲池请求与物理读请求。例如:

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';

通常可关注:

  • Innodb_buffer_pool_read_requests:对 InnoDB 缓冲池的逻辑读请求;
  • Innodb_buffer_pool_reads:无法从缓冲池满足、需要进一步读取的请求。

在近似且相同统计窗口下,可以使用:

H1ΔInnodb_buffer_pool_readsΔInnodb_buffer_pool_read_requestsH \approx 1 - \frac{\Delta \text{Innodb\_buffer\_pool\_reads}} {\Delta \text{Innodb\_buffer\_pool\_read\_requests}}

必须使用增量而不是直接比较累计总数,否则无法知道当前业务窗口的实际变化。

3.3 数据库缓存与操作系统缓存不能重复计算

常见架构中可能存在两层甚至更多层缓存:

查询
  -> 数据库缓冲区
  -> 操作系统页缓存(取决于引擎和文件打开方式)
  -> 存储设备缓存
  -> 物理介质

因此:

  • 数据库报告的“未命中”不一定等于物理盘真的完成了一次介质读取;
  • 存储设备报告的 IOPS 可能高于数据库看到的请求数,因为存在预读、写放大或内部合并;
  • 把数据库缓存、OS 缓存和存储缓存容量简单相加,不能得到有效缓存容量;
  • 容量规划应分别观察数据库层、操作系统层和存储层指标。

3.4 缓存命中率高仍然可能很慢

以下查询可能命中率很高,但延迟仍然很差:

SELECT *
FROM orders
WHERE status = 'pending'
ORDER BY created_at DESC;

如果 status='pending' 的记录很多,数据库即使从缓存中读取大量页面,也可能需要:

  • 扫描大量索引项或表页;
  • 排序;
  • 产生大量结果;
  • 持有锁或等待其他事务。

反例是:一个查询只返回一行,但执行计划错误地扫描了数百万个缓存中的页面。它的物理读很少,CPU 和执行时间却很高。

所以缓存命中率是资源指标,不是端到端性能指标。必须与执行计划、逻辑读量、CPU 时间、锁等待和请求延迟一起解释。


四、从逻辑读推导 IOPS、吞吐和延迟

4.1 IOPS 的含义

**IOPS(Input/Output Operations Per Second)**是每秒完成的存储 IO 操作数。它不是带宽,也不是数据库请求数。

三者区别如下:

  • IOPS:每秒多少次 IO;
  • 吞吐:每秒传输多少字节;
  • 延迟:一次 IO 完成需要多长时间。

如果每次 IO 大小为 BB 字节,IOPS 为 II,理想吞吐约为:

Throughput=I×B\text{Throughput}=I\times B

例如,20,000 IOPS、每次 16 KiB,理论传输量为:

20,000×16 KiB312.5 MiB/s20{,}000 \times 16\text{ KiB} \approx 312.5\text{ MiB/s}

但真实结果还受读写比例、队列深度、随机性、存储并发模型和延迟上限影响。

4.2 一个从请求到 IOPS 的算例

假设某接口每秒接收 2,000 个请求,每个请求平均产生:

  • 6 次逻辑索引页访问;
  • 4 次逻辑数据页访问;
  • 1 次写入相关日志操作。

读逻辑页访问量:

L=2,000×(6+4)=20,000 次/秒L=2{,}000\times(6+4)=20{,}000\text{ 次/秒}

如果读缓存命中率为 97%,估算的读物理访问为:

R=20,000×(10.97)=600 次/秒R=20{,}000\times(1-0.97)=600\text{ 次/秒}

但这还不是最终存储 IOPS,原因包括:

  1. 数据库可能预读或合并读取;
  2. 一次数据库逻辑访问未必对应一次设备 IO;
  3. 脏页刷盘会额外产生写 IO;
  4. WAL 或 redo 通常有独立的顺序写和刷盘行为;
  5. 临时表、排序和后台任务可能产生额外 IO;
  6. 复制、备份或校验任务可能共享存储。

因此更现实的模型是:

IstorageIread-miss+Idirty-page-write+IWAL/redo+Itemp+IbackgroundI_{\text{storage}} \approx I_{\text{read-miss}} +I_{\text{dirty-page-write}} +I_{\text{WAL/redo}} +I_{\text{temp}} +I_{\text{background}}

这个模型的价值不在于得到一个精确预测值,而在于避免把“业务 SQL 数量”直接当成“设备 IOPS”。

4.3 排队会放大延迟

设存储设备平均服务时间为 SS,到达率为 λ\lambda。当利用率:

ρ=λS\rho=\lambda S

接近 1 时,排队等待会快速增加。即使设备名义 IOPS 尚未达到规格上限,延迟也可能已经超过 SLO。

例如,一次 IO 的服务时间从 0.5 ms 增加到 2 ms,在相同请求量下,设备可处理能力近似从:

10.0005=2,000 IOPS\frac{1}{0.0005}=2{,}000\text{ IOPS}

下降到:

10.002=500 IOPS\frac{1}{0.002}=500\text{ IOPS}

实际设备支持并发 IO,因此不能把这个倒数当作设备规格;这个计算只是说明:单次延迟变化会直接改变服务能力和排队状态。

4.4 写入不能只看数据页写入

事务提交通常还涉及持久化语义:

事务修改
  -> 内存中的脏页
  -> WAL/redo 记录
  -> 日志刷盘或提交确认
  -> 后台将脏数据页写回

日志写入与数据页写回不是同一个阶段。数据库可能先持久化日志,再异步写脏页,这能减少随机数据页写入对提交延迟的影响,但不会消除最终的写入压力。

因此,规划写负载时至少要分开估算:

  • 业务数据页写入;
  • 索引页写入;
  • WAL 或 redo 日志量;
  • binlog 或归档日志量;
  • checkpoint 或刷脏产生的周期性写峰值。

如果只用“每秒写入多少行”估算 IOPS,通常会漏掉索引、日志和后台写入。


五、连接、并发与事务:连接数不是吞吐量

5.1 连接数、活动请求和事务数不同

需要区分:

  • 连接数:客户端与数据库建立的会话数量;
  • 活动请求数:当前正在执行 SQL 的数量;
  • 活动事务数:已开始但尚未提交或回滚的事务数量;
  • 锁等待数:正在等待锁或其他资源的请求数量;
  • 连接池中的空闲连接:已经建立连接,但当前没有执行请求。

一个系统可以有 1,000 个连接,却只有 20 个活动查询;也可以只有 100 个连接,但每个连接都在执行耗时很长的事务。

PostgreSQL 中可以查看会话状态:

SELECT
    state,
    count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY state;

进一步查看长事务:

SELECT
    pid,
    usename,
    application_name,
    client_addr,
    state,
    xact_start,
    query_start,
    wait_event_type,
    wait_event,
    query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;

其中,idle in transaction 特别值得关注:它没有执行 SQL,但事务尚未结束,可能阻止旧版本清理、持有锁或扩大复制和维护压力。

MySQL 可使用:

SHOW FULL PROCESSLIST;

或在启用相应 Performance Schema 采集的前提下查询线程、等待和事务信息。Sleep 连接不等于正在消耗同等程度的执行资源,但大量连接仍会消耗会话内存、线程调度和管理开销。

5.2 用 Little 定律估算活动并发

Little 定律为:

L=λWL=\lambda W

其中:

  • LL:系统中平均存在的请求数;
  • λ\lambda:稳定状态下的请求到达率;
  • WW:请求平均停留时间。

假设数据库每秒处理 2,000 个请求,平均数据库服务时间为 50 ms:

L=2,000×0.05=100L=2{,}000\times0.05=100

平均活动请求约为 100,而不是 2,000。

如果由于锁等待,平均停留时间上升到 500 ms:

L=2,000×0.5=1,000L=2{,}000\times0.5=1{,}000

吞吐没有增加,活动并发却扩大了 10 倍。连接池、线程、内存和队列由此迅速膨胀。

这说明连接数增加不能解决 CPU、IO 或锁瓶颈。连接过多的常见失败路径是:

请求增加
  -> 连接池扩大
  -> 数据库并发执行增加
  -> CPU/IO/锁争用加剧
  -> 单请求延迟上升
  -> 活动连接继续增加
  -> 超时、重试和连接风暴

5.3 连接池大小应由活动并发反推

连接池的目的通常是复用连接、限制数据库并发,而不是让数据库拥有尽可能多的连接。

一个简单估算过程是:

  1. 根据 SLO 和压测得到平均或高分位数据库服务时间;
  2. L=λWL=\lambda W 估算稳定状态的活动并发;
  3. 根据 CPU、IO、锁和事务冲突压测池大小;
  4. 为突发流量设置有限排队,而不是无限创建连接;
  5. 为每个实例、每个应用和管理任务预留连接。

例如,目标流量为 2,000 请求/秒,数据库平均服务时间 50 ms,估算活动并发为 100。若压测显示 120 个并发已经使 p95 延迟越过 SLO,则连接池不应盲目设置为 500。额外连接只会在数据库内部排队,并使故障时的恢复更困难。

需要特别注意事务边界:

获取连接
  -> 开始事务
  -> 执行多条 SQL
  -> 提交/回滚
  -> 归还连接

连接归还池之前必须结束事务。否则,连接虽然表面上空闲,事务仍可能持有锁或快照。


六、增长规划:不仅预测数据表大小

6.1 数据增长的基本模型

设:

  • rr:每天新增业务记录数;
  • ss:每条记录及其行存储平均占用;
  • ii:索引、页填充和内部开销相对于表数据的比例;
  • dd:每天日志、临时和维护额外空间;
  • TT:规划天数。

粗略估算为:

Gdata=r×s×(1+i)×T+d×TG_{\text{data}}=r\times s\times(1+i)\times T+d\times T

例如:

  • 每天新增 1,000 万行;
  • 每行及行级平均数据占用 500 字节;
  • 索引和内部开销按 60% 估算;
  • 日志及其他额外空间按 80 GiB/天;
  • 规划 180 天。

仅表与索引的估算:

10,000,000×500×1.6=8,000,000,000 bytes/day10{,}000{,}000\times500 \times1.6 =8{,}000{,}000{,}000\text{ bytes/day}

约为 7.45 GiB/天,180 天约 1.34 TiB。再加上日志、临时空间、复制保留、备份和运维余量,实际磁盘规划不能只按 1.34 TiB 配置。

该公式只是初始模型。更可靠的方法是从实际数据库中按周或按月观察:

  • 表大小;
  • 索引大小;
  • 日志生成量;
  • 临时文件;
  • 复制槽或 binlog 保留;
  • MVCC 清理滞后;
  • 备份和归档保留量。

6.2 空间增长和性能增长不是同一条曲线

数据增长后,可能发生以下变化:

  • 索引树高度增加;
  • 热点页竞争增加;
  • 工作集超过内存;
  • 查询选择性下降;
  • 统计信息失真;
  • 扫描和排序的数据量增加;
  • vacuum、purge、checkpoint 或备份持续时间增长;
  • 复制延迟增大。

因此,不能只问“磁盘还能用多久”,还要问:

在磁盘耗尽之前,工作集是否已经无法缓存?维护窗口是否已经无法完成?副本是否已经追不上主库?

6.3 空间余量必须覆盖故障路径

磁盘余量的用途不只是接收新数据,还需要覆盖:

  • 索引创建期间的临时空间;
  • 表重写、在线变更或重建索引;
  • 批量更新产生的日志;
  • 长事务导致旧版本无法清理;
  • 复制中断后的日志保留;
  • 失败操作的残留文件;
  • 恢复或导入时的临时文件;
  • 监控、诊断和备份元数据。

例如,某次索引创建需要额外空间。如果磁盘只剩 5%,即使日常写入尚未超限,DDL 也可能因空间不足失败。容量规划必须把“正常空间”和“操作空间”分开。


七、事务和 MVCC 会改变容量与性能

7.1 长事务为什么会影响空间

MVCC 引擎通常不会立即覆盖所有旧版本,而是保留旧版本,直到确认没有活跃事务需要读取它们。

一个典型路径是:

事务 A 开始
  -> 事务 B 更新大量行
  -> 旧版本暂时不能清理
  -> 表膨胀、undo 或历史版本增加
  -> 读放大、维护成本和空间占用上升

在 PostgreSQL 中,长事务、idle in transaction、复制槽保留 WAL 等因素都可能使清理受阻。VACUUM 可以回收可复用空间,但通常不会把普通表占用的空间立即还给操作系统;要缩小文件,往往需要会重写表并带来额外锁定或空间需求的操作,具体取决于操作方式。

在 MySQL InnoDB 中,旧版本由 undo 和 purge 机制管理。长事务可能阻止 purge 前进,使 undo 历史增长。表现可能包括:

  • undo 表空间或历史列表增长;
  • 读旧版本的成本上升;
  • 复制和备份时间变长;
  • 磁盘增长速度异常。

因此,增长监控不能只看“插入量”,还要观察事务年龄、清理滞后和日志保留。

7.2 一个反例:低 QPS 也可能耗尽空间

假设业务只有每秒 10 次更新,但一个事务持续 12 小时并读取大量数据。期间其他事务持续更新相同表:

  • 新写入量并不高;
  • 但旧版本无法及时清理;
  • 表或 undo 历史持续增长;
  • 查询和维护逐渐变慢。

这说明“QPS 低”不等于“资源压力低”,事务持续时间和快照范围同样是容量变量。


八、如何建立可验证的容量模型

一个实用的模型应从业务请求开始,而不是从数据库总表大小开始。

8.1 第一步:定义请求单位和 SLO

明确:

  • 请求是 HTTP 接口、SQL、事务还是批处理任务;
  • 读写比例;
  • 每个请求包含多少 SQL;
  • 每条 SQL 的事务边界;
  • 目标吞吐;
  • p95、p99 延迟;
  • 可接受错误率;
  • 高峰持续时间;
  • 是否允许异步化、降级或排队。

例如,“每秒 5,000 次请求”没有足够信息。必须知道每次请求是一次简单主键读取,还是包含多次随机读、更新、日志提交和跨表锁竞争的事务。

8.2 第二步:测量每种请求的逻辑资源消耗

对每类 SQL 记录:

  • 逻辑读;
  • 物理读;
  • CPU 时间;
  • 执行时间;
  • 返回行数;
  • 临时空间;
  • WAL/redo/binlog 增量;
  • 锁等待;
  • 事务持续时间。

PostgreSQL 可以在测试环境使用:

EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM orders
WHERE user_id = 10001
ORDER BY created_at DESC
LIMIT 20;

其中:

  • ANALYZE 会实际执行查询,因此只应对安全的测试查询使用;
  • BUFFERS 用于显示缓冲区访问情况;
  • 执行计划中的行数估算与实际行数差异,可能揭示统计信息或条件选择性问题。

生产环境直接对高频语句使用 EXPLAIN ANALYZE 需要谨慎,因为它会真正执行语句,写操作尤其不能随意执行。对于只读查询,也要考虑额外执行开销。

MySQL 8.4 可使用普通 EXPLAIN 查看优化器计划;EXPLAIN ANALYZE 会执行语句并提供实际执行信息,适合在受控环境使用。不同语句类型和版本支持范围应以对应版本手册为准,不能把测试命令直接用于生产写操作。

8.3 第三步:将请求模型换算为资源需求

设第 kk 类请求的到达率为 λk\lambda_k,每个请求的平均物理读量为 rkr_k,则总读 IO 近似为:

Iread=kλkrkI_{\text{read}}=\sum_k \lambda_k r_k

写入、日志和临时 IO 可以用相同方法分别求和:

Iwrite=kλkwk+IbackgroundI_{\text{write}} =\sum_k \lambda_k w_k+I_{\text{background}}

活动并发则为:

L=kλkWkL=\sum_k \lambda_k W_k

其中 WkW_k 是第 kk 类请求在数据库内的平均停留时间。需要用 p95 或峰值窗口重新计算,因为平均值会掩盖突发排队。

8.4 第四步:寻找最先饱和的资源

将估算结果与测试结果结合,检查:

  • CPU 是否达到平台可持续范围;
  • 数据库缓存是否出现持续抖动;
  • 物理读和写入是否导致存储延迟上升;
  • WAL、redo 或 binlog 刷盘是否进入等待;
  • 活动连接是否达到池或数据库上限;
  • 锁等待和事务年龄是否持续增长;
  • 副本延迟是否超过业务容忍范围;
  • 磁盘空间和维护窗口是否仍然足够。

真正的性能容量通常由最早越过 SLO 的资源决定,而不是某个规格表上的最大值。


九、压测:验证模型,而不是制造一个漂亮的 QPS

9.1 压测数据必须接近生产

压测至少应匹配:

  • 数据量级;
  • 热点分布;
  • 主键和索引基数;
  • 读写比例;
  • 事务大小;
  • 提交频率;
  • SQL 参数分布;
  • 并发连接模型;
  • 复制和后台任务;
  • 缓存冷启动或预热状态。

用 1 万行均匀随机数据测试一个拥有数十亿行、热点高度集中的生产系统,得到的 QPS 没有代表性。

9.2 冷缓存和热缓存必须分别测试

至少准备两种场景:

热缓存测试

先运行足够长时间,使热点工作集稳定进入缓存,然后观察:

  • 稳态 p50、p95、p99;
  • 数据库缓存命中;
  • 存储读写延迟;
  • CPU 和锁等待;
  • 日志写入。

冷缓存测试

重启实例、清空测试环境缓存或使用新的数据范围,观察:

  • 缓存预热时间;
  • 启动后的物理读峰值;
  • 请求延迟尾部;
  • 是否出现连接超时和重试;
  • 预热是否影响线上流量。

冷缓存表现特别重要,因为故障恢复、主备切换和扩容后的新节点都可能经历冷启动。

9.3 压测负载应逐级增加

不要只测一个并发值。可以按以下阶段进行:

基线负载
  -> 目标平均负载
  -> 目标峰值
  -> 峰值持续一段时间
  -> 超过峰值的过载
  -> 降载观察恢复

每一阶段都记录:

  • 吞吐是否仍在增长;
  • p95、p99 是否突然上升;
  • 活动连接是否线性增长;
  • 存储延迟是否先于 IOPS 达到瓶颈;
  • 锁等待是否积累;
  • 复制延迟是否能恢复;
  • 降载后队列、事务和日志是否回落。

如果继续增加并发后吞吐不再增加、延迟却迅速上升,系统已经进入排队区。此时再把连接池扩大,通常只能把排队从应用层转移到数据库层。

9.4 必须压测失败和恢复路径

容量评估不能只测正常状态,还应在受控环境测试:

  • 主库重启或故障转移;
  • 副本延迟;
  • 存储延迟暂时升高;
  • 连接池全部失效后重建;
  • 日志保留增加;
  • 长事务或批量任务并发运行;
  • 备份与在线业务同时运行;
  • 缓存冷启动;
  • 单节点恢复期间的流量转移。

例如,主库切换后,新主库可能缓存未预热,且连接池会同时重建大量连接。稳态压测未必能暴露这一瞬间的连接风暴和物理读峰值。


十、压测结果如何判断:不要只看最大 QPS

一个可用的压测结论至少应包含以下关系:

在数据集规模 X、读写比例 Y、缓存状态 Z、
并发连接 C、事务模型 T 下,

吞吐为 Q,
p95 延迟为 P95,
p99 延迟为 P99,
错误率为 E,
CPU/IOPS/存储延迟/锁等待/复制延迟为……

例如,下面两个结果不能简单比较:

  • 测试 A:热缓存、单行主键读取、1,000 个连接;
  • 测试 B:冷缓存、包含更新和提交、100 个连接。

连接数更高的 A 不一定更难;请求模型和缓存状态才决定资源消耗。

应重点寻找“拐点”:

  1. 吞吐在增加并发时仍然增长;
  2. 延迟保持在 SLO 内;
  3. 错误率稳定;
  4. 资源利用率没有持续排队;
  5. 降载后系统能恢复到原来的稳态。

如果吞吐不再增长而 p99 延迟持续上升,说明新增并发主要变成等待。这个点通常比“压到数据库报错时的最大 QPS”更适合作为容量边界。


十一、常见误区与诊断路径

误区一:数据库总大小就是工作集

失败表现:增加内存后,某些查询变快,但报表仍然慢;或者热点查询已很快,继续增加内存收益很小。

诊断方法

  • 按时间窗口观察表和索引访问频率;
  • 区分热点查询、全表扫描和批处理;
  • 查看执行计划的逻辑读和实际行数;
  • 比较热缓存与冷缓存结果。

误区二:缓存命中率高就说明数据库健康

失败表现:命中率 99%,p99 仍然超时。

可能原因

  • 执行计划扫描了大量缓存页;
  • CPU 已饱和;
  • 锁等待严重;
  • 事务提交被 WAL/redo 刷盘阻塞;
  • 查询返回数据量过大;
  • 连接在数据库内部排队。

诊断方法:把命中率与 CPU、逻辑读量、锁等待、事务延迟和端到端 SLO 放在同一个时间窗口中比较。

误区三:IOPS 规格等于数据库可用 IOPS

失败表现:存储标称 IOPS 很高,但数据库写入延迟仍然周期性升高。

可能原因

  • 随机读写与顺序读写性能不同;
  • IO 大小与规格测试不同;
  • 写缓存策略和持久化语义不同;
  • checkpoint、刷脏、备份与业务争用;
  • 存储延迟在高队列深度下恶化。

诊断方法:同时观察数据库层 IO、设备层 IOPS、吞吐、队列深度和 p95/p99 延迟,不只看一个 IOPS 数值。

误区四:把 max_connections 当成容量

max_connections 或类似上限只是允许的会话数量,不是数据库可持续处理的并发量。

失败表现

  • 应用频繁收到连接拒绝;
  • 数据库内存突然升高;
  • 大量会话处于空闲或等待;
  • 连接池重试进一步放大压力。

诊断方法

  • 区分总连接、活动查询、活动事务和等待连接;
  • 检查连接池是否在每个应用实例都配置了过大的上限;
  • 检查是否有连接泄漏和 idle in transaction
  • 通过压测确定数据库可持续的活动并发。

误区五:只按行数预测磁盘增长

失败表现:表数据增长符合预期,但磁盘增长远超预测。

可能原因

  • 索引增长;
  • WAL、redo 或 binlog 保留;
  • 长事务导致旧版本积累;
  • 临时文件;
  • 复制中断后的日志堆积;
  • 在线 DDL 或重建索引产生临时副本;
  • 表和索引膨胀。

诊断方法:分别绘制表、索引、日志、临时空间、复制保留和维护产生的空间曲线,而不是只看数据库总目录大小。


十二、一个可落地的容量规划闭环

容量规划最终应形成持续闭环:

业务请求模型
  -> SQL 与事务模型
  -> 工作集和逻辑读测量
  -> 缓存、IOPS、连接和日志换算
  -> 受控压测
  -> 找到 SLO 拐点
  -> 生产监控校准
  -> 重新估算增长和故障余量

生产监控至少应把以下维度关联起来:

  • 请求量、读写比例和请求延迟;
  • 活动连接、连接等待和连接建立速率;
  • 活动事务、长事务和锁等待;
  • 逻辑读、物理读、缓存命中;
  • CPU、内存和数据库缓存使用;
  • IOPS、吞吐、设备延迟和队列深度;
  • WAL、redo、binlog 生成与保留;
  • 副本延迟;
  • 表、索引、临时空间和磁盘剩余;
  • 备份、vacuum、purge、checkpoint 等后台任务。

当这些指标同时记录在相同时间窗口内,才能解释因果链:

热点扩大
  -> 缓存未命中增加
  -> 物理读增加
  -> 存储队列变长
  -> SQL 延迟增加
  -> 活动连接增加
  -> 事务持续时间变长
  -> 旧版本和日志保留增加
  -> 存储空间继续下降

这条链条也说明,数据库容量问题通常不是某一个参数的问题。增加内存可能降低物理读,但不能修复错误的执行计划;增加 IOPS 可能缓解读压力,但不能解决长事务和锁竞争;增加连接上限可能暂时隐藏应用排队,却可能把系统推入更严重的资源争用。

可靠的容量规划,是在明确的事务、缓存、存储和 SLO 边界内,用可重复的测量验证这些关系,并在工作集、流量模型和数据规模发生变化时重新计算。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。