TouchAll技术博客

TouchAll技术博客

[系统设计 - 数据库设计专题] 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 触发条件、操作步骤、回滚方案
迁移预案 同构/异构迁移方案、停机时间、回滚策略