容量规划不是"当前用了多少"的报表,而是回答四个问题:还能撑多久、瓶颈先从哪一侧到、扩容要多久、撑不住时降级成什么样。答案只来自四个维度:QPS、连接数、磁盘、单表行数。每个维度的估算都有明确误差来源,把它们写清楚,比算出一个漂亮的小数点更有用。
1. 四个问题各自的输入与输出
| 问题 | 需要的输入 | 输出形态 | 常见错误 |
|---|---|---|---|
| 能撑多久 | 当前水位、日增量、增长率 | 剩余天数区间 | 用平均值掩盖高峰 |
| 瓶颈先到哪 | 各维度的余量比例 | 一个有序清单 | 只看 CPU 不看连接数 |
| 扩容要多久 | 手段 × 单次操作耗时 | 排期表 | 忘算数据迁移与验证 |
| 降级怎么做 | 业务可牺牲顺序 | 分级预案 | 把降级写成"限流"一条 |
四个问题要同时回答:只算撑多久会在错误方向扩容,只排期不定降级线等于押注排期。
2. 四个维度的估算口径
-- 当前写入量级:按业务主键表估算日增量(示例口径,按 MySQL 8.x 核对语法)
SELECT table_name,
table_rows,
ROUND(data_length/1024/1024/1024, 2) AS data_gb,
ROUND(index_length/1024/1024/1024, 2) AS index_gb
FROM information_schema.tables
WHERE table_schema = 'biz'
ORDER BY data_length DESC LIMIT 10;
剩余天数 = (容量上限 × 安全水位 − 当前占用) / 日均净增量
日均净增量 = 日均写入 − 日均清理(归档、TTL、删除标记回收)
| 维度 | 估算方法 | 主要误差来源 |
|---|---|---|
| QPS | 峰值采样 P99 分钟值 × 安全系数 | 采样窗口太短,漏掉活动峰 |
| 连接数 | 应用实例数 × 池上限 + 后台任务 | 池上限配了但没算中间件与运维连接 |
| 磁盘 | 单行字节 × 行数 × (1 + 索引系数) + 日志 | 行变宽(新增列)、索引重建翻倍 |
| 单表行数 | 日增行数 × 保留天数 | 保留策略没落地,实际不删 |
table_rows 是采样值,与 COUNT(*) 有偏差,够用于量级判断。
3. QPS 到 CPU 的换算为什么不可靠
同一个"QPS"在不同查询构成下,CPU 成本能差一个数量级:
| 因素 | 影响机制 | 规划含义 |
|---|---|---|
| 查询成本差异 | 主键点查与多表聚合的 CPU 差几个量级 | 按"查询类型占比"分桶估,不按总 QPS |
| 缓存命中率 | 命中缓存时数据库不消耗 CPU | 高峰时缓存击穿会把隐藏成本暴露 |
| 并发度 | 高并发带来上下文切换与锁竞争 | 低 QPS 高并发也可能打满 CPU |
可用的换算形式(不用单点系数):
CPU 需求 ≈ Σ(某类查询 QPS × 该类单次 CPU 成本) × 冗余系数
某类单次 CPU 成本:在接近生产的数据量与并发下实测得到,不能从文档抄
结论:QPS→CPU 只能给区间,不能给点值。规划留冗余系数,并把"缓存命中率下降 20%"作为独立压测场景跑一遍。
4. 连接数不是越大越好
数据库侧的并发处理能力受工作线程数与锁竞争约束,应用侧连接池开大只是把排队从应用搬到数据库。
| 连接数过大的代价 | 表现 |
|---|---|
| 上下文切换 | CPU 系统态占比上升,吞吐反而下降 |
| 内存 | 每连接独占排序缓冲、会话缓存,累加可观 |
| 数据库侧并发上限 | 超出 max_connections 直接拒绝,或进入锁等待 |
| 故障放大 | 一个慢查询占住连接,池被逐步耗尽 |
# 连接池配比示例(HikariCP 口径,按版本核对默认值)
pool:
maximum-pool-size: 20 # 单实例,经验上不超过数据库可并发数的 1/N
minimum-idle: 5
connection-timeout: 3000 # 宁可快速失败,不要让请求无限排队
max-lifetime: 1800000 # 短于数据库/中间件的空闲断开时间
leak-detection-threshold: 60000
配比原则:实例数 × maximum-pool-size + 后台任务 + 运维保留 ≤ 数据库并发上限 × 0.8。剩余 20% 给运维、备份、监控与故障手动介入——事故里它比任何调优参数都值钱。
5. 磁盘增长的三部分
| 组成 | 增长驱动 | 处理手段 |
|---|---|---|
| 数据文件 | 业务写入、行变宽 | 归档、冷热分离 |
| 索引 | 新增索引、更新导致页分裂 | 索引审计、合并冗余索引 |
| binlog / 备份 | 写入量、保留天数、全备频率 | 缩短保留、增量备份、异地存 |
经验区间:索引占数据量的比例通常在 20%–60% 之间,宽表加多列索引可超过数据本身。这个比例不是常数,随索引数量与列宽变化,规划时要按自己的表抽样核对,不要套固定系数。
# 磁盘水位与增长趋势(每分钟采样,落到时序库)
df -B1 /var/lib/mysql | awk 'NR==2{print $2, $3}'
# binlog 占用单独统计,别和业务数据混在一个盘
du -sh /var/lib/mysql/binlog 2>/dev/null
磁盘规划的关键不是总量,而是单盘写满的后果:binlog 满会阻塞写入,临时表满会中断大查询,备份盘满会让备份静默失败。三类盘分开是最便宜的防线。
6. 单表行数的实际上限取决于什么
"单表 2000 万行"是个流传的经验值,真实上限由四件事共同决定:
| 约束 | 机制 | 触发信号 |
|---|---|---|
| 索引层数 | B+ 树每层一次页访问,行数增大层数增加 | 点查 P99 出现台阶式抬升 |
| DDL 时间 | 大表加列/改索引耗时随行数增长 | 变更窗口从分钟级走向小时级 |
| 备份恢复 | 恢复时间 = 数据量 / 恢复吞吐 | 演练恢复超过 RTO |
| 统计信息质量 | 大表采样比例不足导致执行计划跑偏 | 同一 SQL 计划突然改变 |
分表触发条件(满足两条以上再动):
1. 单表 > 单机可维护行数,且点查 P99 已受层数影响
2. 在线 DDL 影响写入,需跨窗口变更
3. 备份恢复演练超 RTO
4. 单表写入已成热点,索引维护成本占比过高
先分表还是先分库,判断依据是热点在哪:单表写入集中 → 分表;多表共同撑爆单机 → 分库。分表不解决单机 IO 与连接数上限,这一步必须分清。
7. 增长曲线的三种形态
| 形态 | 特征 | 应对节奏 |
|---|---|---|
| 线性 | 日增量稳定,方差小 | 按剩余天数排期,季度复核 |
| 阶跃 | 上线新业务/导入历史数据后跳变 | 变更前预留,变更后重算基线 |
| 病毒式 | 增长率本身在增长 | 按周复核,预设降级线 |
线性增长最危险的是"看起来还很久":按剩余 180 天排期,留给评估、采购、迁移、验证的时间往往只剩一半。阶跃用变更日历驱动复核;病毒式没有规划空间,只能提前把降级做成可开关能力。
8. 扩容手段的代价排序
| 顺序 | 手段 | 单次代价 | 引入的新问题 |
|---|---|---|---|
| 1 | 加索引 / 改 SQL | 分钟级,可回滚 | 索引过多拖慢写入 |
| 2 | 加缓存 | 天级 | 一致性、击穿、雪崩 |
| 3 | 读写分离 | 天级 | 复制延迟、读到旧数据 |
| 4 | 垂直拆分 | 周级 | 跨库查询、事务边界 |
| 5 | 分库分表 | 月级 | 路由、分布式事务、聚合查询、扩容再均衡 |
| 6 | 换存储 | 季度级 | 双写迁移、回滚路径、运维体系重建 |
每一步都要先回答:回滚路径是什么、验证口径是什么、业务感知是什么。
没有回滚路径的手段,等价于一次性赌博。
代价排序的意义是先穷尽便宜的:第 1、2 步能解决的问题硬上第 5 步,等于把可用性押在一次跨月迁移上。
9. 压测、阈值与降级
压测的三个必要条件:数据量接近生产、读写混合、看 P99 与慢查询而非平均 QPS。
压测口径:
- 数据量:与生产同量级(至少同数量级的行数与索引结构)
- 混合比:按生产读写比,含一定比例的范围查询与聚合
- 观测:P99 延迟、慢查询条数、连接池等待时间、复制延迟
- 递增:阶梯加压,记录每级的水位与拒绝率
| 预警项 | 阈值形态 | 动作 |
|---|---|---|
| 磁盘 | 70% / 85% 双线 | 70% 启动归档,85% 冻结非必要写入 |
| 连接池等待 | 等待时间持续 > 10ms | 查慢查询,不是先加池 |
| 复制延迟 | 超过业务可容忍窗口 | 读流量切主或降级为只读展示 |
| 单表行数 | 接近分表触发线 | 启动评估,不立即动手 |
降级按"业务可牺牲顺序"排,而不是按组件:关非核心展示 → 降级为异步写 → 限流入口 → 只保留下单支付。每级要有开关、验证与恢复步骤。
10. 排错:容量判断失准的五种情形
| 现象 | 原因 | 处理 |
|---|---|---|
| 监控显示余量充足,却突然打满 | 采样窗口太短,漏掉周期性峰值 | 用 P99 分钟值重算,加活动日历 |
| 扩容后性能没改善 | 瓶颈在连接数或锁竞争,不在 CPU | 按维度清单逐项核对余量 |
| 磁盘增长远超写入量 | binlog/备份与业务数据同盘 | 分盘,单独监控 |
| 单表变大后点查变慢 | 索引层数增加、缓冲命中下降 | 评估归档与分表,而非盲加索引 |
| 压测达标,上线不达标 | 数据量或读写比与生产不符 | 用生产子集重建压测数据集 |
最后一条最常见:压测库几百万行、生产几亿行,执行计划完全不同。压测的价值取决于数据集像不像生产。
11. 边界:这些情况不必急着动结构
| 场景 | 判断 | 理由 |
|---|---|---|
| 数据量小、增长可预测 | 不做分片 | 分片带来的复杂度远超收益 |
| 有强一致需求的核心账务 | 优先垂直拆分而非分库分表 | 跨分片事务代价高,边界清晰更划算 |
| 读写比极高且能容忍短暂旧数据 | 先读写分离 + 缓存 | 成本低、可回滚 |
| 增长已停止的历史库 | 只做归档与冷备 | 结构改造无收益 |
| 单机余量 > 一年且增长线性 | 季度复核即可 | 过度规划会锁死架构选择 |
容量规划的正确姿势是保留选择权:用足便宜手段的余量,把昂贵手段的触发条件写清楚,到线再动手。
12. 自测
- 容量规划要回答的四个问题里,哪一个决定了应急预案的有效性?
- 为什么 QPS 到 CPU 只能给区间,不能给点值?举出三个误差来源。
- 连接池调大之后吞吐反而下降,最可能是哪两类代价在起作用?
- 磁盘增长的三部分里,哪一部分最容易被漏算,漏算的后果是什么?
- 单表行数的实际上限由哪四件事决定?其中哪一件最先暴露?
- 扩容手段按代价排序后,为什么"先穷尽便宜的"比"一次到位"更安全?