数据库改个字段而已,为什么总能把线上系统搞崩?
数据库改个字段而已,为什么总能把线上系统搞崩?
数据库 Schema 变更,真正难的从来不是“怎么改”,而是“怎么在业务不停机的情况下改”。
大家好,我是 Echo_Wish。
做运维久了,你会发现一个特别有意思的现象:
代码上线,大家已经越来越习惯灰度、回滚、监控、限流。
但一说到数据库变更,很多团队还是:
“改个字段嘛,停一下服务不就行了?”
然后凌晨两点,业务群突然开始疯狂弹消息:
“数据库连接超时了。”
“接口怎么全部 500 了?”
“谁在执行 ALTER TABLE?”
最后发现,罪魁祸首就是一条看起来平平无奇的 SQL:
ALTER TABLE user_info
ADD COLUMN phone VARCHAR(20);
这篇文章就聊一个非常现实的话题:
数据库运维自动化,到底应该怎么做 Schema 在线变更,以及如何保证新旧代码兼容?
一、Schema 变更最大的坑,不是 SQL 写错
很多人理解数据库变更,思路特别简单:
修改表结构
↓
发布代码
↓
完成
但在线业务真正面对的是:
旧代码 ───────┐
├──→ 数据库
新代码 ───────┘
也就是说,在发布过程中,数据库面对的可能不是一套代码,而是两套甚至多套代码同时运行。
比如原来的表:
CREATE TABLE user_info (
id BIGINT PRIMARY KEY,
name VARCHAR(50)
);
现在产品要求增加手机号。
最直接的方案:
ALTER TABLE user_info
ADD COLUMN phone VARCHAR(20) NOT NULL;
看起来没问题。
但如果线上还有旧版本代码,它执行:
INSERT INTO user_info(id, name)
VALUES (1001, '张三');
数据库就可能直接报错。
问题来了:
明明只是增加一个字段,为什么旧代码也挂了?
因为数据库 Schema 本质上也是一个 API。
这一点,我认为很多团队都低估了。
代码接口有兼容性要求:
API v1
API v2
数据库同样应该有:
Schema v1
Schema v2
而且数据库往往比 HTTP API 更难改。
二、真正靠谱的方案:扩展,而不是破坏
在线 Schema 变更有一个非常重要的思想:
先扩展,再迁移,最后收缩。
也就是我们经常说的:
Expand → Migrate → Contract
这个思想看起来简单,但它几乎可以解决大量数据库发布事故。
三、第一阶段:Expand,先把新结构加进去
比如我们准备增加:
phone
第一步不要急着:
ALTER TABLE user_info
ADD phone VARCHAR(20) NOT NULL;
而应该优先:
ALTER TABLE user_info
ADD phone VARCHAR(20) NULL;
为什么允许 NULL?
因为:
旧代码根本不知道这个字段存在。
旧代码继续:
INSERT INTO user_info(id, name)
VALUES (1001, '张三');
依然可以工作。
与此同时,新代码已经可以开始使用:
SELECT id, name, phone
FROM user_info;
这就形成了一个非常重要的状态:
数据库:
┌────────────────────┐
│ id │
│ name │
│ phone │ ← 新字段
└────────────────────┘
旧代码 → 只认识 id/name
新代码 → 认识 id/name/phone
数据库已经升级,但旧代码仍然能够正常运行。
这就是兼容性的核心。
四、第二阶段:Migrate,把历史数据慢慢迁过去
字段加好了,并不意味着工作结束。
假设之前已经有:
1000 万用户
现在新增 phone 字段。
你如果直接:
UPDATE user_info
SET phone = 'xxx';
那线上数据库可能直接开始“喘气”。
尤其是大表。
所以数据迁移也必须在线化。
例如:
UPDATE user_info
SET phone = ...
WHERE id > 100000
AND id <= 110000;
然后循环执行:
10万
10万
10万
10万
...
而不是一次性:
UPDATE user_info
SET phone = ...;
五、为什么数据库自动化不能只自动执行 SQL?
这是我比较想强调的一点。
很多所谓的“数据库自动化平台”,本质上就是:
输入 SQL
↓
执行 SQL
↓
返回结果
这其实不叫数据库运维自动化。
最多只能叫:
SQL 自动执行器。
真正的数据库自动化应该知道:
这张表有多大?
当前 QPS 多高?
有没有长事务?
有没有锁等待?
当前连接数多少?
这个 SQL 会不会锁表?
执行预计影响多少行?
当前是不是业务高峰?
失败以后怎么回滚?
比如我们提交:
ALTER TABLE orders
ADD COLUMN delivery_address VARCHAR(500);
系统至少应该自动检查:
表名:orders
数据量:1.2 亿
当前 QPS:8600
活跃事务:37
长事务:2
数据库负载:72%
DDL 风险:中
建议:
❌ 当前不建议执行
这才叫真正意义上的:
数据库运维自动化。
六、第三阶段:新旧代码双写
这是 Schema 兼容里面非常经典的一招。
例如原来只有:
username
现在准备改成:
user_name
千万不要直接:
ALTER TABLE user_info
CHANGE username user_name VARCHAR(50);
因为旧代码还在使用:
SELECT username
FROM user_info;
直接改名:
旧代码瞬间报错。
更稳妥的方式是:
ALTER TABLE user_info
ADD COLUMN user_name VARCHAR(50);
然后新代码开始:
写 username
写 user_name
也就是双写:
user.setUsername(username);
user.setUserName(username);
读取的时候,新代码优先读取:
String name = user.getUserName();
if (name == null) {
name = user.getUsername();
}
经过一段时间以后:
旧代码
↓
username
新代码
↓
user_name
数据逐渐完成迁移
最终才删除旧字段。
七、这就是“Expand → Migrate → Contract”
把整个过程串起来,其实特别清晰。
第一步:Expand
ALTER TABLE user_info
ADD COLUMN user_name VARCHAR(50);
第二步:双写
username
↓
┌───────────────┐
│ username │
│ user_name │
└───────────────┘
第三步:历史数据迁移
UPDATE user_info
SET user_name = username
WHERE user_name IS NULL
LIMIT 10000;
循环执行。
第四步:新代码完全切换
读取 user_name
写入 user_name
第五步:停止旧字段写入
username ← 不再使用
第六步:Contract
最后才:
ALTER TABLE user_info
DROP COLUMN username;
这时候,旧代码已经彻底退出。
删除字段才是最后一步,而不是第一步。
八、再说一个更容易踩坑的问题:字段删除
很多开发喜欢这样操作:
ALTER TABLE user_info
DROP COLUMN old_field;
然后代码:
user.getOldField();
线上直接:
Unknown column 'old_field'
所以数据库 Schema 变更里面有一个特别重要的原则:
新增字段通常容易兼容,删除字段通常最危险。
因为:
ADD
属于扩展。
而:
DROP
RENAME
TYPE CHANGE
往往属于破坏性变更。
因此数据库自动化平台最好能够自动识别:
ADD COLUMN → 低风险
ADD INDEX → 中风险
MODIFY COLUMN → 高风险
DROP COLUMN → 极高风险
RENAME COLUMN → 极高风险
然后采用不同审批策略。
九、索引变更,同样不能想当然
很多线上事故不是因为字段,而是:
加索引。
开发发现:
SELECT *
FROM orders
WHERE user_id = 10001;
很慢。
于是:
CREATE INDEX idx_user_id
ON orders(user_id);
然后:
线上开始抖。
因为在某些数据库和版本、表规模、执行方式下,创建索引可能产生明显的 I/O、CPU 或锁竞争。
所以自动化系统不能只判断:
SQL 是 CREATE INDEX
↓
执行
而应该:
CREATE INDEX
↓
检查数据库类型
↓
检查版本
↓
检查表规模
↓
检查当前负载
↓
判断是否支持 Online DDL
↓
生成执行计划
↓
执行
↓
监控
例如 MySQL 环境下,可以结合具体版本和存储引擎评估:
ALTER TABLE orders
ADD INDEX idx_user_id(user_id),
ALGORITHM=INPLACE,
LOCK=NONE;
但这里也不要形成一个误区:
看到
LOCK=NONE就认为绝对不会阻塞。
数据库的实际行为还受到具体版本、操作类型、存储引擎、并发事务等因素影响。
所以:
Online DDL ≠ 零风险 DDL。
十、自动化平台最应该做的,其实是“刹车”
我一直觉得:
数据库自动化最重要的能力,不是油门,而是刹车。
比如有人提交:
ALTER TABLE big_table
MODIFY COLUMN amount DECIMAL(10,2);
平台发现:
表数据量:8.7 亿
当前 QPS:12000
数据库 CPU:82%
存在长事务:3
DDL 类型:字段类型修改
风险等级:高
这时候最好的自动化结果不是:
正在执行……
而应该:
❌ 拒绝自动执行
原因:
1. 超大表
2. 高峰期
3. 字段类型发生变化
4. 存在长事务
建议:
低峰期执行
或采用在线 Schema 变更工具
能够拒绝危险操作,本身就是自动化能力。
十一、把 Schema 变更真正纳入 CI/CD
如果团队规模再大一点,我建议直接把数据库变更纳入发布流水线。
例如:
Git
│
├── application
│
└── migrations
│
↓
Schema Analyzer
│
↓
Compatibility Check
│
↓
Risk Assessment
│
├── LOW ─────→ 自动执行
│
├── MEDIUM ──→ 人工审批
│
└── HIGH ────→ 禁止自动执行
│
↓
DBA
数据库变更文件也可以进入 Git:
db/
├── V001__create_user.sql
├── V002__add_phone.sql
├── V003__add_user_name.sql
└── V004__create_user_index.sql
这样数据库就不再是:
“谁在生产库上手工改了什么?”
而变成:
“Git 里记录了这次 Schema 演进。”
这才是真正可审计、可追踪的数据库运维。
十二、我最推荐的一套线上 Schema 变更原则
如果让我给团队制定规则,我会简单粗暴地定下面几条。
规则 1:禁止直接修改生产 Schema
不要:
开发人员
↓
Navicat
↓
生产数据库
而应该:
Git
↓
Review
↓
自动检查
↓
审批
↓
执行
规则 2:新增优先,删除滞后
优先:
ADD COLUMN
谨慎:
MODIFY COLUMN
最后:
DROP COLUMN
规则 3:代码必须兼容两个 Schema 版本
发布过程中:
旧代码 + 新 Schema
必须能够运行。
同时:
新代码 + 旧 Schema
最好也能够运行,至少要通过明确设计避免发布窗口中的不兼容。
规则 4:大表禁止“一把梭”
不要:
UPDATE 1亿条数据;
应该:
分批
↓
限速
↓
监控
↓
异常暂停
↓
继续
规则 5:自动化必须有“风险等级”
例如:
LOW
└── ADD NULL COLUMN
MEDIUM
└── ADD INDEX
HIGH
├── MODIFY COLUMN
├── RENAME COLUMN
└── 大表 DDL
CRITICAL
└── DROP COLUMN
不同等级使用不同审批机制。
十三、最后聊聊我的一个感受
数据库运维发展到现在,真正高级的能力已经不是:
“我会写 SQL。”
而是:
“我能让数据库在业务不停机的情况下安全地演进。”
这两句话,看起来差不多,实际上差得非常远。
以前我们习惯:
数据库是基础设施
代码依赖数据库
数据库由 DBA 管
现在越来越应该把数据库 Schema 当成:
代码的一部分
它需要:
版本管理
自动检测
兼容性分析
灰度发布
风险控制
审计
监控
回滚策略
尤其是微服务、Kubernetes、DevOps 体系越来越成熟以后,应用可以秒级发布,数据库却不能再靠人工凌晨两点改表。
所以我认为未来真正成熟的数据库运维平台,不应该只是帮你“执行 SQL”。
它应该在你提交 SQL 的时候告诉你:
这条 SQL 能不能执行。
在执行之前告诉你:
现在是不是执行的好时机。
执行过程中告诉你:
数据库有没有出现异常。
执行完成以后告诉你:
Schema、代码和数据是不是已经完成迁移。
甚至在你准备删除字段的时候直接拦住:
“等等,还有 3 个服务正在使用这个字段。”
这才是我理解的:
数据库运维自动化。
说到底,Schema 变更不是一次 SQL 操作,而是一次线上系统的“渐进式升级”。
真正靠谱的团队,不是从来不改数据库。
而是:
数据库天天改,业务照样跑;代码持续发布,线上依然稳。
这才是数据库运维自动化真正应该追求的终点。
Echo_Wish|专注云原生、DevOps、数据库与 AI 运维实践
别把数据库当成“存数据的地方”。
它其实是整个业务系统最不能随便动的核心基础设施。
- 点赞
- 收藏
- 关注作者
评论(0)