上个月刚进组的新人在深夜值班时搞出了一个特大事故。由于业务紧急上线,他直接登录生产数据库,对着一张存有三千万条数据的订单主表执行了 ALTER TABLE order_records ADD COLUMN ext_info VARCHAR(512);。结果一瞬间,写操作全部被阻塞,应用服务器连接池迅速爆满,报警短信直接把整个团队的手机轰炸到了直接崩溃的边缘。最后不得不强行干预杀掉 DDL 线程,花了大半个晚上修复受损的数据和索引。
很多人以为 MySQL 5.6 之后支持了 Online DDL 就可以高枕无忧了。这绝对是最大的误区。在大数据量、高并发写操作的场景下,原生的 Online DDL 经常会因为申请 Metadata Lock(元数据锁)而导致灾难。比如有一个慢查询未结束,你的 ALTER 命令就会排队等待锁,而这又会阻塞后续所有对该表的 SELECT/INSERT/UPDATE,导致整个业务线瘫痪。此外,主从同步延迟问题也极其致命,主库做完 DDL,从库重放 BINLOG 时会造成极大的延迟,直接废掉读写分离。
为了彻底摆脱原生 ALTER 带来的心智负担,我们团队现在明文规定:凡是表数据量超过 50 万行的,必须强制使用 gh-ost 执行无感知迁移。gh-ost 是 GitHub 开源的 MySQL 无触发器在线表结构变更工具。它不依赖 Trigger,而是通过模拟从库解析 Binlog 并以小批量(chunk-size)的模式异步同步数据,从而最大程度减小对主库的负载压力。
以下是我们封装的、用于在生产环境自动化执行 gh-ost 迁移的完整 Bash 脚本:
#!/usr/bin/env bash
set -euo pipefail
# 数据库连接配置与目标变更
DB_HOST="prod-rds-master.internal"
DB_PORT="3306"
DB_USER="migration_user"
DB_PASS="SecurePassword123!"
DATABASE_NAME="ecom_orders"
TABLE_NAME="order_records"
# 临时生成的辅助表结构变更 SQL
ALTER_STATEMENT="ADD COLUMN ext_info VARCHAR(512) DEFAULT '' NOT NULL"
# 监听控制台套接字路径
PANEL_SOCKET="/tmp/gh-ost.${TABLE_NAME}.sock"
echo "==== Starting gh-ost migration process ===="
gh-ost \
--user="${DB_USER}" \
--password="${DB_PASS}" \
--host="${DB_HOST}" \
--port="${DB_PORT}" \
--database="${DATABASE_NAME}" \
--table="${TABLE_NAME}" \
--alter="${ALTER_STATEMENT}" \
--chunk-size=1000 \
--max-lag-millis=1500 \
--max-load="Threads_running=50,Questions=1000" \
--critical-load="Threads_running=100" \
--throttle-control-replicas="prod-rds-replica-01.internal:3306" \
--pane-socket-file="${PANEL_SOCKET}" \
--initially-drop-ghost-table \
--initially-drop-old-table \
--serve-socket-file="/tmp/gh-ost-server-${TABLE_NAME}.sock" \
--allow-on-master \
--execute
echo "==== gh-ost migration finished successfully ===="
在这个脚本里,我们特别开启了 --max-lag-millis=1500 和 --throttle-control-replicas 选项。一旦从库的复制延迟超过 1.5 秒,gh-ost 会自动暂停拷贝数据,直到延迟消退。同时,通过配置 --max-load,当主库并发活跃线程数(Threads_running)超过 50 或每秒查询数超过 1000 时,迁移流程也会自动“踩刹车”让路给线上业务。如果遇到突发紧急情况,甚至可以通过本地的 Socket 端口发送 echo throttle | nc -U /tmp/gh-ost.order_records.sock 命令,让其无限期挂起。
自从推行了这套 gh-ost 迁移机制,我们再也没有在发布日因为数据库迁移提心吊胆过。不过频繁执行这个工具会对磁盘空间有一定要求(需要双倍的空间来容纳临时表),这可以说是唯一的心智成本。
最后想问问大家,你们在千万级以上的数据库中做冷热数据分离时,是直接通过业务代码定时迁移,还是利用底层 CDC(比如 Debezium)监听 BINLOG 异步写入归档表?