[系统设计 - 数据库设计专题] 2.数据库容量规划
编辑做数据库容量规划的核心目的,是精准评估当前存储基线、通过预估数据增长速率、预留未来 3–5 年业务容量上限,通过预留合理冗余余量、规范单表 / 索引容量边界、设计分层存储与平滑扩容架构,从根源避免业务突发增量、数据持续膨胀导致的磁盘打满、数据库性能雪崩、架构无力承载业务迭代的问题,保障系统长期稳定、可预判、可演进。
容量现状分析
如果已有现实运行项目
统计现有数据库的数据量(表大小、索引、日志等)、存储占用、QPS/TPS(请求量)、连接数、CPU/内存使用率等指标。
根据业务口径估算
核心思路:技术指标(QPS/存储)是结果,业务指标(用户量/订单量)才是源头。容量规划必须从业务口径出发,倒推技术需求。
估算方法论
业务指标 → 数据模型 → 存储估算 → 性能估算
↑
产品/运营提供
关键业务指标清单
| 业务维度 | 核心指标 | 数据来源 |
|---|---|---|
| 用户规模 | DAU、MAU、注册用户总数 | 产品/运营 |
| 交易规模 | 日订单量、月 GMV、客单价 | 业务方 |
| 内容规模 | 日新增帖子/评论/图片数 | 内容运营 |
| 增长预期 | 未来 1~3 年用户/交易增长倍数 | CEO/业务负责人 |
估算公式
日新增行数 = 日活跃用户数 × 每用户日均操作次数
单行大小 = 字段类型长度之和 + 行头开销(MySQL约9字节)
日增存储 = 日新增行数 × 单行大小 × (1 + 索引膨胀系数)
月增存储 = 日增存储 × 30
年增存储 = 日增存储 × 365
案例:电商平台容量估算
业务背景:某电商平台,当前 MAU 100万,日订单量 5万单,客单价 200元。业务预期未来 3 年用户量增长 5 倍。
Step 1:梳理核心表结构
-- 订单表
orders (
id BIGINT, -- 8B
user_id BIGINT, -- 8B
order_no VARCHAR(32),-- 32B
amount DECIMAL(10,2),-- 5B
status TINYINT, -- 1B
created_at DATETIME, -- 8B
updated_at DATETIME, -- 8B
-- 行头开销 ~9B
-- 合计: ~79B ≈ 80B
)
-- 订单明细表
order_items (
id BIGINT, -- 8B
order_id BIGINT, -- 8B
product_id BIGINT, -- 8B
quantity INT, -- 4B
price DECIMAL(10,2), -- 5B
-- 合计: ~42B ≈ 50B
)
-- 用户表
users (
id BIGINT, -- 8B
phone VARCHAR(20), -- 20B
nickname VARCHAR(50),-- 50B
status TINYINT, -- 1B
created_at DATETIME, -- 8B
-- 合计: ~96B ≈ 100B
)
Step 2:计算日增量
| 表 | 日新增行数 | 单行大小 | 日增存储 | 索引膨胀(+40%) |
|---|---|---|---|---|
| orders | 5万 | 80B | 4MB | 5.6MB |
| order_items | 15万(平均3件/单) | 50B | 7.5MB | 10.5MB |
| users | 1000(日新增注册) | 100B | 0.1MB | 0.14MB |
| 合计 | - | - | 11.6MB | 16.24MB |
Step 3:推算未来容量
当前数据量:已运行 2 年,约 100GB
日增量:16.24MB/天 ≈ 0.5GB/月
年增量:约 6GB/年
未来 3 年(用户增长 5 倍):
- 第 1 年:日增 16MB,年增 6GB,累计 106GB
- 第 2 年:日增 40MB(用户 2x),年增 15GB,累计 121GB
- 第 3 年:日增 80MB(用户 5x),年增 30GB,累计 151GB
3 年后预估总数据量:150GB ~ 200GB(含索引)
Step 4:性能指标估算
当前 QPS:
- 读:200 QPS(订单查询、用户查询)
- 写:50 TPS(下单、更新状态)
未来 3 年(5 倍增长):
- 读:1000 QPS
- 写:250 TPS
峰值系数(大促 3 倍):
- 读峰值:3000 QPS
- 写峰值:750 TPS
资源需求计算
存储资源计算
总存储需求 = 数据量 + 索引量 + 日志量 + 备份量 + 冗余余量
数据量 = 预估总行数 × 单行大小
索引量 = 数据量 × 索引膨胀系数(0.3~0.5)
日志量 = Binlog/WAL 日增量 × 保留天数
备份量 = 全量备份大小 × 备份份数 + 增量备份
冗余余量 = (数据+索引+日志) × 20%~30%
案例续:电商平台资源计算
存储资源:
| 项目 | 计算 | 大小 |
|---|---|---|
| 数据量 | 3 年后 150GB | 150GB |
| 索引量 | 150GB × 40% | 60GB |
| Binlog/WAL 日志 | 5GB/天 × 30天 | 35GB |
| 全量备份 | 200GB × 2份(本地+异地) | 400GB |
| 增量备份 | 5GB/天 × 30天 | 150GB |
| 小计 | - | 795GB |
| 冗余 30% | 795GB × 30% | 240GB |
| 总计 | - | ~1TB |
计算资源:
CPU 需求:
- 基准:每 1000 QPS 需要 2 核(经验值)
- 峰值 3000 QPS → 6 核
- 预留 30% → 8 核
内存需求:
- 热数据缓存:150GB × 20%(活跃数据比例)= 30GB
- Buffer Pool:热数据 × 1.5 = 45GB
- 连接内存:500连接 × 10MB = 5GB
- 系统预留:8GB
- 总计:~60GB → 选 64GB
磁盘 IOPS:
- 峰值写:750 TPS × 每次写 10 次 IO = 7500 IOPS
- 峰值读:3000 QPS × 缓存命中率 80% → 600 次实际读 × 5 IO = 3000 IOPS
- 总计:~10000 IOPS
- 选 SSD 云盘(通常 3000~10000 IOPS/TB)
最终配置建议:
| 资源 | 规格 |
|---|---|
| CPU | 8 核 |
| 内存 | 64GB |
| 磁盘 | 1TB SSD(IOPS > 10000) |
| 网络 | 万兆以上 |
冗余与扩展设计
冗余设计原则
| 层级 | 冗余策略 | 目的 |
|---|---|---|
| 磁盘 | 预留 30% 空间 | 应对突发增长、避免磁盘打满 |
| 连接 | 连接池上限 > 峰值 × 1.5 | 应对流量突增 |
| CPU/内存 | 峰值时使用率 < 70% | 预留处理余量 |
| 副本 | 至少 1 主 2 从 | 读扩展 + 高可用 |
扩展方案设计
纵向扩展(Scale Up)— 优先选择
触发条件:单表数据量 < 5000万,磁盘 < 500GB
方案:升级实例规格(CPU/内存/磁盘)
优点:无需改代码,运维简单
缺点:有上限,成本非线性增长
横向扩展(Scale Out)— 分阶段实施
阶段 1:读写分离(单表 < 1亿,读多写少)
┌────────┐
│ 应用 │
└────┬───┘
↓
┌────────┐
│ Proxy │ ← 读写路由
└────┬───┘
↓
┌────┴────┬─────────┐
│ 主库 │ 从库×2 │
│ (写) │ (读) │
└─────────┴─────────┘
阶段 2:分库分表(单表 > 5000万 或 磁盘 > 500GB)
- 按 user_id 取模分 16 库 × 16 表 = 256 张表
- 单表数据量控制在 500万以内
- 引入 ShardingSphere / Vitess 中间件
阶段 3:冷热分离(数据量 > 1TB)
- 热数据(近 3 个月):SSD
- 温数据(3~12 个月):HDD
- 冷数据(> 1 年):对象存储 + 元数据索引
案例续:电商平台扩展路径
当前(100GB):
- 单库单表,8C64G,1TB SSD
- 读写分离:1主1从
第 1 年(120GB):
- 纵向扩展:16C128G,2TB SSD
- 增加从库:1主2从
第 2 年(200GB):
- 引入分库分表:按 user_id 分 4 库
- 单库数据量控制在 50GB 以内
第 3 年(400GB):
- 扩展到 16 库
- 引入冷热分离,历史订单归档到 HDD
预备扩容方案设计
扩容触发条件
| 指标 | 预警阈值 | 扩容阈值 | 紧急阈值 |
|---|---|---|---|
| 磁盘使用率 | > 70% | > 80% | > 90% |
| CPU 使用率 | > 60% | > 75% | > 90% |
| 内存使用率 | > 70% | > 85% | > 95% |
| 单表行数 | > 3000万 | > 5000万 | > 1亿 |
| 主从延迟 | > 1秒 | > 5秒 | > 30秒 |
扩容方案矩阵
| 场景 | 扩容方式 | 操作步骤 | 停机时间 | 回滚方案 |
|---|---|---|---|---|
| 磁盘不足 | 云盘在线扩容 | 控制台扩容 → 文件系统 resize | 0(在线) | 无需回滚 |
| CPU/内存不足 | 升级实例规格 | 创建新实例 → 数据同步 → 切换 | 分钟级 | 切回老实例 |
| 读压力高 | 增加从库 | 搭建从库 → 配置路由 | 0(在线) | 移除从库 |
| 写压力高 | 分库分表 | 双写 → 数据迁移 → 切读 → 切写 | 小时级 | 回切老库 |
| 单表过大 | 分表 + 归档 | 分表迁移 → 历史归档 | 小时级 | 回切老表 |
案例:磁盘扩容实操
场景:磁盘使用率达到 82%,预计 2 个月后打满
Step 1:评估现状
# 查看磁盘使用
df -h /data/mysql
# Filesystem Size Used Avail Use% Mounted on
# /dev/xvdb 500G 410G 90G 82% /data/mysql
# 查看各表大小
SELECT
table_name,
ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb
FROM information_schema.tables
WHERE table_schema = 'ecommerce'
ORDER BY (data_length + index_length) DESC
LIMIT 10;
# 结果:
# orders | 45000 MB
# order_items | 32000 MB
# users | 8000 MB
# products | 5000 MB
Step 2:快速释放空间
-- 清理 7 天前的 Binlog
PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);
-- 释放约 35GB
-- 归档 1 年前的订单到历史表
INSERT INTO orders_history SELECT * FROM orders
WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);
DELETE FROM orders WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);
-- 释放约 20GB
Step 3:在线扩容(云环境)
# AWS RDS
aws rds modify-db-instance \
--db-instance-identifier ecommerce-prod \
--allocated-storage 1000 # 从 500GB 扩到 1TB
# 监控扩容进度
aws rds describe-db-instances \
--db-instance-identifier ecommerce-prod \
--query 'DBInstances[0].StorageEncrypted'
Step 4:验证与监控
# 确认扩容生效
df -h /data/mysql
# Filesystem Size Used Avail Use% Mounted on
# /dev/xvdb 1.0T 355G 645G 36% /data/mysql ✓
# 配置告警
# 磁盘使用率 > 70% 告警
案例:分库分表扩容
场景:orders 表达到 8000 万行,查询变慢,需要分表
Step 1:选择分片键
分析:
- 90% 查询都带 user_id
- 订单查询都是"我的订单"场景
- 结论:按 user_id 分片
分片策略:
- user_id % 16 = 分片编号(0~15)
- 16 个库,每库 16 张表(orders_00 ~ orders_15)
- 单表数据量控制在 500万以内
Step 2:双写阶段
# 应用层双写逻辑
def create_order(user_id, order_data):
shard_id = user_id % 16
# 1. 写新库(分片库)
new_db.insert(f"orders_{shard_id}", order_data)
# 2. 写老库(兜底)
old_db.insert("orders", order_data)
# 3. 记录迁移日志
migration_log.insert({
"order_id": order_data["id"],
"shard_id": shard_id,
"status": "dual_write"
})
Step 3:数据迁移
# 批量迁移历史数据
def migrate_orders():
offset = 0
batch_size = 1000
while True:
# 从老库读取
orders = old_db.query(
"SELECT * FROM orders ORDER BY id LIMIT %s OFFSET %s",
batch_size, offset
)
if not orders:
break
# 写入新库
for order in orders:
shard_id = order["user_id"] % 16
new_db.insert(f"orders_{shard_id}", order)
offset += batch_size
print(f"Migrated {offset} rows")
Step 4:切读验证
# 灰度切读:10% 流量走新库
def get_orders(user_id):
if hash(user_id) % 100 < 10: # 10% 灰度
return new_db.query(f"orders_{user_id % 16}", user_id=user_id)
else:
return old_db.query("orders", user_id=user_id)
# 对比验证
def verify_consistency(user_id):
old_data = old_db.query("orders", user_id=user_id)
new_data = new_db.query(f"orders_{user_id % 16}", user_id=user_id)
if old_data != new_data:
alert("Data inconsistency detected!")
Step 5:全量切读 → 切写 → 下线老库
Week 1: 10% 读流量 → 新库
Week 2: 50% 读流量 → 新库
Week 3: 100% 读流量 → 新库,老库只写
Week 4: 100% 写流量 → 新库,老库只读(兜底)
Week 5: 确认无问题,下线老库
预备迁移方案设计
迁移场景分类
| 场景 | 触发条件 | 迁移方式 | 风险等级 |
|---|---|---|---|
| 同构迁移 | 机房迁移、云厂商切换 | 物理备份 + 增量同步 | 低 |
| 跨构迁移 | MySQL → PostgreSQL | 数据转换 + 双写验证 | 高 |
| 分片迁移 | 单库 → 分库分表 | 分片路由 + 数据拆分 | 中 |
| 版本升级 | MySQL 5.7 → 8.0 | 就地升级 / 迁移到新实例 | 中 |
迁移方案模板
1. 迁移目标
- 源环境:xxx
- 目标环境:xxx
- 迁移数据量:xxx
- 停机窗口:xxx
2. 迁移策略
- 全量迁移:xxx
- 增量同步:xxx
- 数据校验:xxx
3. 回滚方案
- 回滚触发条件:xxx
- 回滚步骤:xxx
- 回滚时间:xxx
4. 迁移步骤
- Step 1: xxx
- Step 2: xxx
...
5. 验证清单
- [ ] 数据完整性
- [ ] 业务功能
- [ ] 性能指标
- [ ] 监控告警
案例:MySQL 跨云迁移
场景:从 AWS RDS 迁移到阿里云 RDS,数据量 500GB,要求停机时间 < 30 分钟
Step 1:迁移方案选择
方案 A:停机导出导入
- 停机时间:500GB / 100MB/s = 5000秒 ≈ 83分钟
- 不满足要求 ✗
方案 B:物理备份 + 增量同步
- 全量备份:500GB,耗时 2 小时
- 增量同步:基于 Binlog,实时同步
- 停机切换:分钟级
- 满足要求 ✓
Step 2:迁移步骤
# Day 1:搭建目标环境
# 1. 创建阿里云 RDS 实例(同配置:8C64G,1TB SSD)
# 2. 配置网络:打通 AWS ↔ 阿里云专线
# Day 2:全量备份
# AWS 侧
mysqldump -h aws-rds.xxx.amazonaws.com \
--single-transaction \
--master-data=2 \
--all-databases > full_backup.sql
# 传输到阿里云(500GB,专线 1Gbps,约 1.5 小时)
scp full_backup.sql ali-rds:/tmp/
# Day 3:导入 + 增量同步
# 阿里云侧导入
mysql -h ali-rds.xxx.aliyuncs.com < full_backup.sql
# 配置增量同步(使用 DTS 数据迁移服务)
# 1. 创建 DTS 迁移任务
# 2. 源:AWS RDS,目标:阿里云 RDS
# 3. 同步类型:增量同步(基于 Binlog)
# 4. 启动同步,延迟 < 1秒
# Day 4:数据校验
# 对比行数
SELECT COUNT(*) FROM orders; # AWS: 8000万,阿里云: 8000万 ✓
SELECT COUNT(*) FROM order_items; # AWS: 2.4亿,阿里云: 2.4亿 ✓
# 抽样校验
SELECT * FROM orders ORDER BY RAND() LIMIT 100;
# 对比两端数据一致性 ✓
# Day 5:停机切换
# 1. 通知业务方:凌晨 2:00~2:30 停机维护
# 2. 2:00 停止应用写入(只读模式)
# 3. 等待 DTS 同步完成(延迟 = 0)
# 4. 切换 DNS / 连接串到阿里云
# 5. 启动应用,验证读写正常
# 6. 2:15 切换完成,总停机 15 分钟 ✓
# Day 6~12:观察期
# - 监控性能指标
# - 对比 AWS 和阿里云的 QPS/延迟
# - 确认无异常后,下线 AWS RDS
Step 3:回滚方案
回滚触发条件:
- 切换后 1 小时内出现数据不一致
- 性能指标下降 > 50%
- 出现无法快速修复的 Bug
回滚步骤:
1. 停止应用写入
2. 反向配置 DTS(阿里云 → AWS)
3. 等待增量同步完成
4. 切换 DNS 回 AWS
5. 启动应用
回滚时间:约 30 分钟
案例:MySQL → PostgreSQL 跨构迁移
场景:业务从 MySQL 5.7 迁移到 PostgreSQL 14,数据量 100GB
Step 1:差异分析
| 差异点 | MySQL | PostgreSQL | 处理方式 |
|---|---|---|---|
| 自增主键 | AUTO_INCREMENT |
SERIAL/IDENTITY |
DDL 转换 |
| 字符串 | VARCHAR默认不补空格 |
同 | 兼容 |
| 日期函数 | NOW() |
NOW() |
兼容 |
| 分页 | LIMIT offset, count |
LIMIT count OFFSET offset |
SQL 改写 |
| 事务 | 支持 DDL 回滚 | 支持 DDL 回滚 | PG 更强 |
| JSON | JSON类型 |
JSONB(索引支持) |
需改造 |
Step 2:DDL 转换
-- MySQL 原始 DDL
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(32) NOT NULL,
amount DECIMAL(10,2),
status TINYINT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
UNIQUE INDEX uk_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- PostgreSQL 转换后
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(32) NOT NULL,
amount NUMERIC(10,2),
status SMALLINT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE UNIQUE INDEX uk_orders_order_no ON orders(order_no);
Step 3:数据迁移
# 使用 pgloader 工具(支持 MySQL → PostgreSQL 自动转换)
pgloader mysql://user:pass@mysql-host/ecommerce \
postgresql://user:pass@pg-host/ecommerce
# 或使用 AWS DMS / 阿里云 DTS(支持异构迁移)
Step 4:应用改造
# 分页查询改造
# MySQL
cursor.execute("SELECT * FROM orders LIMIT %s, %s", (offset, limit))
# PostgreSQL
cursor.execute("SELECT * FROM orders LIMIT %s OFFSET %s", (limit, offset))
# JSON 查询改造
# MySQL
SELECT data->>'$.name' FROM users WHERE id = 1;
# PostgreSQL
SELECT data->>'name' FROM users WHERE id = 1;
Step 5:双写验证
# 应用层双写,对比一致性
def create_order(order_data):
# 写 MySQL
mysql_id = mysql_db.insert("orders", order_data)
# 写 PostgreSQL
pg_id = pg_db.insert("orders", order_data)
# 对比
mysql_row = mysql_db.query("orders", mysql_id)
pg_row = pg_db.query("orders", pg_id)
if mysql_row != pg_row:
log_error("Data inconsistency", mysql_row, pg_row)
总结
数据库容量规划的核心交付物:
| 交付物 | 内容 |
|---|---|
| 容量评估报告 | 当前数据量、增长速率、3 年预测 |
| 资源配置清单 | CPU/内存/磁盘/IOPS 规格 |
| 扩展路径图 | 纵向扩展 → 读写分离 → 分库分表 → 冷热分离 |
| 扩容 SOP | 触发条件、操作步骤、回滚方案 |
| 迁移预案 | 同构/异构迁移方案、停机时间、回滚策略 |
- 0
- 0
-
分享