前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 6|回复: 0

数据库容量规划实战:增长预测与扩容决策

[复制链接]

数据库容量规划实战:增长预测与扩容决策

[复制链接]
dbaai

主题

0

回帖

186

积分

DBAAI

积分
186
3 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
做 DBA 这些年,最怕的不是数据库挂了,而是某天早上收到一条磁盘告警:/data 使用率 91%。挂了有应急预案,慢慢涨到 90% 才是真的折磨人——你不知道它是明天满,还是三个月后满,也不知道该清数据、加硬盘,还是该上分区。这篇就把容量规划这件事拆成能落地的动作:量什么、怎么预测、什么时候该做哪种扩容。

一、具体的问题

多数团队的容量管理停留在"看剩余百分比"这个层面,于是反复踩下面这几个坑:


  • 只看空闲率,不看增速。 剩 30% 看着很安全,但如果上个月涨了 10%,三个月后就得半夜扩容。反过来剩 15% 也不一定危险——如果那是一张半年没动过的历史表。
  • 只看数据文件,不看日志和临时空间。 我见过最典型的一次:数据目录 60G 稳如老狗,结果一个没加 WHERE 的大批量 UPDATE 把 binlog 撑到 240G,先挂的是 binlog 所在盘。undo 表空间、tempdb、归档目录、临时表空间同理,它们才是突然爆仓的那一个。
  • 没有历史数据,全凭感觉。 出事的时候才去翻监控,发现只保留 15 天,压根算不出月增速。容量这件事,没有历史采样就没有预测
  • 清理等于删数据,不考虑归档和压缩。 一刀切删掉历史数据,业务下周要查去年的对账单,又得从备份里捞,捞回来还得找地方放——比不清理还麻烦。
  • 扩容方案只有"加盘"一种。 加盘最省事,也最贵、最不可持续。真正的决策顺序应该是:清理 → 归档 → 压缩 → 分区 → 加盘 → 分库分表,越往后成本越高。


二、核心原理

1. 容量要量三个口径,不是一个


  • 已用容量(used):当前真实占用,最容易拿,也最没信息量。
  • 增速(growth):单位时间的增量,决定你还有多少缓冲期。这是核心指标。
  • 水位与阈值(threshold):不是拍脑袋定 80%,而是按"从告警到扩容完成需要多久"反推。假设扩容流程要走 3 天审批 + 1 天执行,那告警线就应设在"剩余可撑 7 天"的位置,而不是固定百分比。


2. 三种可用的预测算法

方法算法适用场景缺点
简单线性外推月增量 = (本次 - 上次) / 间隔月数增长平稳的业务表遇到促销、双十一会严重低估
近 N 期移动平均取最近 3 期月增量的平均值有波动,想平滑掉异常月对趋势转折反应慢
复合增长率 CAGR(末值/初值)^(1/月数) - 1长期指数型增长短期样本少时不稳定


实际工作里我用得最多的是移动平均 + 最坏值双轨:平时看移动平均定采购计划,风险评估看最近 3 期的最大值。两者差得远,说明增长不稳定,预警线要再往上抬。

3. 剩余天数怎么算才靠谱
  1. 剩余天数 = (可用容量 - 安全预留) / 日均增量
复制代码

关键是安全预留这一项不能省。我一般按总容量的 10% 或"最大单表的一次性增长量"取大者。很多数据库在磁盘 100% 时会直接崩且难以原地恢复,留出不至于踩到 100% 的缓冲,比预测精度更重要。

4. 扩容决策树

按单位成本从低到高排,永远先试便宜的:


  • 清理:真的没用的数据(临时表、过期会话、孤儿大对象)——零成本。
  • 归档:冷数据挪到历史库/对象存储,主库只留近 N 个月——低成本,需要改造查询入口。
  • 压缩:InnoDB 表压缩、Oracle 表压缩、PG 的 TOAST——几乎零改造,代价是少量 CPU。
  • 分区:按时间分区,冷分区直接 detach 或换到廉价存储——需要改表结构,收益是清理变成 O(1) 的 DDL。
  • 加盘:最快见效,成本线性上升,治标。
  • 分库分表:成本最高,只有在单实例确实撑不住写入时才动。


三、实例参考(动手步骤)

下面这套在 MySQL 8.0 上跑通,Oracle 和 PG 的等价语句我一并给出。

步骤 1:先建采样表,从今天开始攒数据

这一步最容易被跳过,但它是后面所有预测的基础。没有历史,什么都算不出来。
  1. CREATE TABLE IF NOT EXISTS ops_cap_snapshot (
  2.   id           BIGINT AUTO_INCREMENT PRIMARY KEY,
  3.   snap_date    DATE NOT NULL,
  4.   db_name      VARCHAR(64) NOT NULL,
  5.   data_mb      DECIMAL(18,2) NOT NULL,
  6.   idx_mb       DECIMAL(18,2) NOT NULL,
  7.   total_mb     DECIMAL(18,2) NOT NULL,
  8.   rows_cnt     BIGINT DEFAULT 0,
  9.   UNIQUE KEY uk_day_db (snap_date, db_name)
  10. );
复制代码

步骤 2:每天采一次全库占用
  1. INSERT INTO ops_cap_snapshot (snap_date, db_name, data_mb, idx_mb, total_mb, rows_cnt)
  2. SELECT CURDATE(),
  3.        table_schema,
  4.        ROUND(SUM(data_length)/1024/1024, 2),
  5.        ROUND(SUM(index_length)/1024/1024, 2),
  6.        ROUND(SUM(data_length + index_length)/1024/1024, 2),
  7.        SUM(table_rows)
  8. FROM information_schema.tables
  9. WHERE table_schema NOT IN ('mysql','sys','performance_schema','information_schema')
  10. GROUP BY table_schema
  11. ON DUPLICATE KEY UPDATE
  12.   data_mb = VALUES(data_mb), idx_mb = VALUES(idx_mb),
  13.   total_mb = VALUES(total_mb), rows_cnt = VALUES(rows_cnt);
复制代码

配个 cron 每天凌晨跑一次即可:
  1. 0 3 * * * mysql -udbamon -p'xxx' ops -e "source /opt/dba/cap_snapshot.sql" >> /var/log/dba/cap.log 2>&1
复制代码

Oracle 等价
  1. SELECT owner, ROUND(SUM(bytes)/1024/1024, 2) AS total_mb
  2. FROM dba_segments
  3. WHERE owner NOT IN ('SYS','SYSTEM')
  4. GROUP BY owner ORDER BY total_mb DESC;
  5. -- 有 AWR 许可的,直接拿历史快照,连采样都省了
  6. SELECT s.snap_id, TO_CHAR(s.begin_interval_time,'YYYY-MM-DD') snap_day,
  7.        o.owner, ROUND(SUM(t.space_used_total)/1024/1024, 2) mb
  8. FROM dba_hist_seg_stat t
  9. JOIN dba_hist_snapshot s ON s.snap_id = t.snap_id
  10. JOIN dba_objects o ON o.object_id = t.obj#
  11. GROUP BY s.snap_id, s.begin_interval_time, o.owner
  12. ORDER BY s.snap_id;
复制代码

PostgreSQL 等价
  1. SELECT schemaname,
  2.        ROUND(SUM(pg_total_relation_size(schemaname||'.'||tablename))/1024/1024, 2) AS total_mb
  3. FROM pg_tables
  4. WHERE schemaname NOT IN ('pg_catalog','information_schema')
  5. GROUP BY schemaname ORDER BY total_mb DESC;
复制代码

步骤 3:算增速并外推剩余天数
  1. WITH base AS (
  2.   SELECT db_name, snap_date, total_mb,
  3.          LAG(total_mb) OVER (PARTITION BY db_name ORDER BY snap_date) AS prev_mb,
  4.          LAG(snap_date) OVER (PARTITION BY db_name ORDER BY snap_date) AS prev_date
  5.   FROM ops_cap_snapshot
  6. ),
  7. delta AS (
  8.   SELECT db_name, snap_date,
  9.          total_mb - prev_mb AS add_mb,
  10.          DATEDIFF(snap_date, prev_date) AS days
  11.   FROM base WHERE prev_mb IS NOT NULL AND DATEDIFF(snap_date, prev_date) > 0
  12. ),
  13. rate AS (
  14.   SELECT db_name,
  15.          AVG(add_mb/days) AS avg_mb_per_day,
  16.          MAX(add_mb/days) AS worst_mb_per_day,
  17.          COUNT(*) AS samples
  18.   FROM delta WHERE snap_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)
  19.   GROUP BY db_name
  20. ),
  21. cur AS (
  22.   SELECT db_name, total_mb FROM ops_cap_snapshot
  23.   WHERE snap_date = (SELECT MAX(snap_date) FROM ops_cap_snapshot)
  24. )
  25. SELECT c.db_name,
  26.        ROUND(c.total_mb/1024, 2) AS cur_gb,
  27.        ROUND(r.avg_mb_per_day, 2) AS avg_mb_day,
  28.        ROUND(r.worst_mb_per_day, 2) AS worst_mb_day,
  29.        r.samples,
  30.        ROUND((500*1024 - c.total_mb) / NULLIF(r.avg_mb_per_day, 0)) AS days_left_avg,
  31.        ROUND((500*1024 - c.total_mb) / NULLIF(r.worst_mb_per_day, 0)) AS days_left_worst
  32. FROM cur c JOIN rate r USING (db_name)
  33. ORDER BY days_left_worst;
复制代码

上面的 500*1024 换成你的实际可用容量(MB)。重点看 days_left_worst 这一列——它才是你可能被叫醒的时间点。

步骤 4:定位到底是谁在涨
  1. SELECT table_schema, table_name,
  2.        ROUND((data_length+index_length)/1024/1024, 2) AS mb,
  3.        table_rows,
  4.        ROUND(index_length/1024/1024, 2) AS idx_mb,
  5.        ROUND(index_length/GREATEST(data_length,1), 2) AS idx_ratio
  6. FROM information_schema.tables
  7. WHERE table_schema = 'orderdb'
  8. ORDER BY mb DESC LIMIT 20;
复制代码

看两个信号:idx_ratio 大于 1.5 说明索引比数据还大,多半有重复索引或冗余前缀索引;表里 table_rows 不大但 mb 很大,多半是碎片,或者有大字段被删过没回收。

步骤 5:按顺序做治理,并做前后对比

真实案例,一个订单库 orderdb,可用容量 500G,治理前水位 421G:
  1. -- 1) 清理:干掉重复索引(先确认再用)
  2. SELECT * FROM sys.schema_redundant_indexes WHERE table_schema='orderdb';
  3. -- 2) 归档:把两年前数据挪走,分批删避免长事务
  4. INSERT INTO archive_db.t_order_hist SELECT * FROM orderdb.t_order WHERE create_time < '2024-01-01';
  5. DELETE FROM orderdb.t_order WHERE create_time < '2024-01-01' LIMIT 5000;  -- 循环执行直到影响 0 行
  6. -- 3) 压缩:对归档库大表开压缩
  7. ALTER TABLE archive_db.t_order_hist ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
  8. -- 4) 碎片回收(会锁表,放维护窗口)
  9. ALTER TABLE orderdb.t_order ENGINE=InnoDB;
复制代码

治理前后对比:

项目治理前清理重复索引后归档两年前数据后归档+压缩后
orderdb 占用421 GB392 GB214 GB158 GB
水位84%78%43%32%
日均增量1.9 GB/天1.9 GB/天0.85 GB/天0.62 GB/天
按最坏值剩余天数38 天52 天121 天176 天
是否加盘必须必须不必不必


一次归档+压缩,把 38 天的紧张局面拉到 176 天,省掉了这次扩容采购。这就是"先做便宜的"的价值。

步骤 6:别漏了非数据文件的空间
  1. # binlog / 归档 / 临时目录,这些才是突然爆仓的元凶
  2. du -sh /var/lib/mysql/binlog /var/lib/mysql/undotbs /var/lib/mysql/tmp 2>/dev/null
  3. # MySQL 自动清理 binlog,别设太长
  4. SET GLOBAL binlog_expire_logs_seconds = 604800;   -- 保留 7 天
  5. # Oracle 归档目录满了会直接 hang 住整个库
  6. RMAN> DELETE NOPROMPT ARCHIVELOG ALL COMPLETED BEFORE 'SYSDATE-7';
复制代码

四、实操检查清单


  • [ ] 今天就建采样表,哪怕只有一行。容量预测的前提是历史数据,晚一天开始就少一天样本。
  • [ ] 采样脚本放进 cron,并加失败告警(脚本静默失败是最常见的断供原因)。
  • [ ] 监控指标里同时有 used日均增量剩余天数,不能只有百分比。
  • [ ] 告警阈值按"扩容流程耗时 + 缓冲"反推,不要固定写 80%。
  • [ ] 同时监控 binlog / undo / temp / 归档目录,这几处爆仓的概率高于数据文件。
  • [ ] 预测同时给出平均增速和最坏增速两条线,采购看平均、告警看最坏
  • [ ] 每月跑一次"谁在涨"的 TOP 20 排查,关注 index/data 比值大于 1.5 的表。
  • [ ] 大表删除一律分批(每次 5000 行 + sleep),避免长事务和主从延迟。
  • [ ] 时间字段的大表优先改分区,冷分区直接归档,把 DELETE 变成 DDL。
  • [ ] 归档前先跟业务确认保留期,并用 INSERT ... SELECT 落历史库后再删,别直接 DELETE。
  • [ ] ALTER TABLE ... ENGINE=InnoDB 会锁表,必须放维护窗口,先在从库验证耗时。
  • [ ] 每次治理后回填对比表,让"省下多少"可见——这是下一次申请资源时的最好证据。
  • [ ] 每次扩容后重新校准预测模型,业务增长曲线变了,旧结论就作废。


容量规划这件事,技术含量不高,但它是少数几个"做了就能明确省钱、不做就一定会在半夜出事"的工作。核心就一句:把百分比换成天数。你说"还剩 30%",没人有感觉;你说"按最坏增速 38 天后满",所有人都会动起来。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

QQ|Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-9-17 10:53 , Processed in 0.027529 second(s), 10 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表