|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
做 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. 剩余天数怎么算才靠谱
- 剩余天数 = (可用容量 - 安全预留) / 日均增量
复制代码
关键是安全预留这一项不能省。我一般按总容量的 10% 或"最大单表的一次性增长量"取大者。很多数据库在磁盘 100% 时会直接崩且难以原地恢复,留出不至于踩到 100% 的缓冲,比预测精度更重要。
4. 扩容决策树
按单位成本从低到高排,永远先试便宜的:
- 清理:真的没用的数据(临时表、过期会话、孤儿大对象)——零成本。
- 归档:冷数据挪到历史库/对象存储,主库只留近 N 个月——低成本,需要改造查询入口。
- 压缩:InnoDB 表压缩、Oracle 表压缩、PG 的 TOAST——几乎零改造,代价是少量 CPU。
- 分区:按时间分区,冷分区直接 detach 或换到廉价存储——需要改表结构,收益是清理变成 O(1) 的 DDL。
- 加盘:最快见效,成本线性上升,治标。
- 分库分表:成本最高,只有在单实例确实撑不住写入时才动。
三、实例参考(动手步骤)
下面这套在 MySQL 8.0 上跑通,Oracle 和 PG 的等价语句我一并给出。
步骤 1:先建采样表,从今天开始攒数据
这一步最容易被跳过,但它是后面所有预测的基础。没有历史,什么都算不出来。
- CREATE TABLE IF NOT EXISTS ops_cap_snapshot (
- id BIGINT AUTO_INCREMENT PRIMARY KEY,
- snap_date DATE NOT NULL,
- db_name VARCHAR(64) NOT NULL,
- data_mb DECIMAL(18,2) NOT NULL,
- idx_mb DECIMAL(18,2) NOT NULL,
- total_mb DECIMAL(18,2) NOT NULL,
- rows_cnt BIGINT DEFAULT 0,
- UNIQUE KEY uk_day_db (snap_date, db_name)
- );
复制代码
步骤 2:每天采一次全库占用
- INSERT INTO ops_cap_snapshot (snap_date, db_name, data_mb, idx_mb, total_mb, rows_cnt)
- SELECT CURDATE(),
- table_schema,
- ROUND(SUM(data_length)/1024/1024, 2),
- ROUND(SUM(index_length)/1024/1024, 2),
- ROUND(SUM(data_length + index_length)/1024/1024, 2),
- SUM(table_rows)
- FROM information_schema.tables
- WHERE table_schema NOT IN ('mysql','sys','performance_schema','information_schema')
- GROUP BY table_schema
- ON DUPLICATE KEY UPDATE
- data_mb = VALUES(data_mb), idx_mb = VALUES(idx_mb),
- total_mb = VALUES(total_mb), rows_cnt = VALUES(rows_cnt);
复制代码
配个 cron 每天凌晨跑一次即可:
- 0 3 * * * mysql -udbamon -p'xxx' ops -e "source /opt/dba/cap_snapshot.sql" >> /var/log/dba/cap.log 2>&1
复制代码
Oracle 等价:
- SELECT owner, ROUND(SUM(bytes)/1024/1024, 2) AS total_mb
- FROM dba_segments
- WHERE owner NOT IN ('SYS','SYSTEM')
- GROUP BY owner ORDER BY total_mb DESC;
- -- 有 AWR 许可的,直接拿历史快照,连采样都省了
- SELECT s.snap_id, TO_CHAR(s.begin_interval_time,'YYYY-MM-DD') snap_day,
- o.owner, ROUND(SUM(t.space_used_total)/1024/1024, 2) mb
- FROM dba_hist_seg_stat t
- JOIN dba_hist_snapshot s ON s.snap_id = t.snap_id
- JOIN dba_objects o ON o.object_id = t.obj#
- GROUP BY s.snap_id, s.begin_interval_time, o.owner
- ORDER BY s.snap_id;
复制代码
PostgreSQL 等价:
- SELECT schemaname,
- ROUND(SUM(pg_total_relation_size(schemaname||'.'||tablename))/1024/1024, 2) AS total_mb
- FROM pg_tables
- WHERE schemaname NOT IN ('pg_catalog','information_schema')
- GROUP BY schemaname ORDER BY total_mb DESC;
复制代码
步骤 3:算增速并外推剩余天数
- WITH base AS (
- SELECT db_name, snap_date, total_mb,
- LAG(total_mb) OVER (PARTITION BY db_name ORDER BY snap_date) AS prev_mb,
- LAG(snap_date) OVER (PARTITION BY db_name ORDER BY snap_date) AS prev_date
- FROM ops_cap_snapshot
- ),
- delta AS (
- SELECT db_name, snap_date,
- total_mb - prev_mb AS add_mb,
- DATEDIFF(snap_date, prev_date) AS days
- FROM base WHERE prev_mb IS NOT NULL AND DATEDIFF(snap_date, prev_date) > 0
- ),
- rate AS (
- SELECT db_name,
- AVG(add_mb/days) AS avg_mb_per_day,
- MAX(add_mb/days) AS worst_mb_per_day,
- COUNT(*) AS samples
- FROM delta WHERE snap_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)
- GROUP BY db_name
- ),
- cur AS (
- SELECT db_name, total_mb FROM ops_cap_snapshot
- WHERE snap_date = (SELECT MAX(snap_date) FROM ops_cap_snapshot)
- )
- SELECT c.db_name,
- ROUND(c.total_mb/1024, 2) AS cur_gb,
- ROUND(r.avg_mb_per_day, 2) AS avg_mb_day,
- ROUND(r.worst_mb_per_day, 2) AS worst_mb_day,
- r.samples,
- ROUND((500*1024 - c.total_mb) / NULLIF(r.avg_mb_per_day, 0)) AS days_left_avg,
- ROUND((500*1024 - c.total_mb) / NULLIF(r.worst_mb_per_day, 0)) AS days_left_worst
- FROM cur c JOIN rate r USING (db_name)
- ORDER BY days_left_worst;
复制代码
上面的 500*1024 换成你的实际可用容量(MB)。重点看 days_left_worst 这一列——它才是你可能被叫醒的时间点。
步骤 4:定位到底是谁在涨
- SELECT table_schema, table_name,
- ROUND((data_length+index_length)/1024/1024, 2) AS mb,
- table_rows,
- ROUND(index_length/1024/1024, 2) AS idx_mb,
- ROUND(index_length/GREATEST(data_length,1), 2) AS idx_ratio
- FROM information_schema.tables
- WHERE table_schema = 'orderdb'
- ORDER BY mb DESC LIMIT 20;
复制代码
看两个信号:idx_ratio 大于 1.5 说明索引比数据还大,多半有重复索引或冗余前缀索引;表里 table_rows 不大但 mb 很大,多半是碎片,或者有大字段被删过没回收。
步骤 5:按顺序做治理,并做前后对比
真实案例,一个订单库 orderdb,可用容量 500G,治理前水位 421G:
- -- 1) 清理:干掉重复索引(先确认再用)
- SELECT * FROM sys.schema_redundant_indexes WHERE table_schema='orderdb';
- -- 2) 归档:把两年前数据挪走,分批删避免长事务
- INSERT INTO archive_db.t_order_hist SELECT * FROM orderdb.t_order WHERE create_time < '2024-01-01';
- DELETE FROM orderdb.t_order WHERE create_time < '2024-01-01' LIMIT 5000; -- 循环执行直到影响 0 行
- -- 3) 压缩:对归档库大表开压缩
- ALTER TABLE archive_db.t_order_hist ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
- -- 4) 碎片回收(会锁表,放维护窗口)
- ALTER TABLE orderdb.t_order ENGINE=InnoDB;
复制代码
治理前后对比:
| 项目 | 治理前 | 清理重复索引后 | 归档两年前数据后 | 归档+压缩后 | | orderdb 占用 | 421 GB | 392 GB | 214 GB | 158 GB | | 水位 | 84% | 78% | 43% | 32% | | 日均增量 | 1.9 GB/天 | 1.9 GB/天 | 0.85 GB/天 | 0.62 GB/天 | | 按最坏值剩余天数 | 38 天 | 52 天 | 121 天 | 176 天 | | 是否加盘 | 必须 | 必须 | 不必 | 不必 |
一次归档+压缩,把 38 天的紧张局面拉到 176 天,省掉了这次扩容采购。这就是"先做便宜的"的价值。
步骤 6:别漏了非数据文件的空间
- # binlog / 归档 / 临时目录,这些才是突然爆仓的元凶
- du -sh /var/lib/mysql/binlog /var/lib/mysql/undotbs /var/lib/mysql/tmp 2>/dev/null
- # MySQL 自动清理 binlog,别设太长
- SET GLOBAL binlog_expire_logs_seconds = 604800; -- 保留 7 天
- # Oracle 归档目录满了会直接 hang 住整个库
- 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 天后满",所有人都会动起来。 |
|