饮墨

子安饮墨馀三斗,留与卿儿作赋来

用 gh-ost 在线改造 AWS Aurora 6000 万行大表:一次生产实战复盘

痛点

某生产 Aurora MySQL 集群上有两张日历相关的大表,需要把两个 varchar(255) 字段扩展到 varchar(500)。看似只是一条 ALTER TABLE MODIFY COLUMN,但这两张表分别是 6000 万行 / 37GB3500 万行 / 28GB,而且集群还被另一套业务共用。

如果直接执行原生 ALTER TABLE,会踩到 MySQL 的一个经典陷阱:

InnoDB 的 MODIFY COLUMN 在涉及字符集 / COLLATE 变更时,会强制走 ALGORITHM=COPY(全表重建),即使你显式写 ALGORITHM=INPLACE 也会被拒绝。

全表重建意味着:长时间持有 MDL(元数据锁)、消耗大量 CPU/IO。在几十 GB 的大表上,这会让集群 CPU 飙到 90%+、锁住在线业务的读写——事实上我们之前正是因为一次未评估的生产 DDL,导致 Writer CPU 冲到 96%、连锁触发上层服务 OOM、整条链路雪崩。

所以结论很明确:几十 GB 的生产大表,绝不能直接 ALTER,必须用在线 DDL 工具。 本文用的是 gh-ost

为什么选 gh-ost

gh-ost(GitHub Online Schema Transmogrifier)的核心思路是影子表 + binlog 增量同步,全程几乎不锁原表:

  • 建一张与原表结构相同的「影子表」,在影子表上执行目标 DDL;
  • 把原表的历史数据分块(chunk)拷贝到影子表;
  • 同时把原表的实时增量变更(通过订阅 binlog)应用到影子表;
  • 数据追平后,用一次毫秒级的原子 RENAME 把影子表切换成正式表(cut-over)。

关键点:拷贝历史数据 + 同步增量的整个过程,原表都是可读可写的;只有最后 cut-over 的一瞬间有极短暂的锁。这正是它替代会长时间锁表的原生 ALTER 的意义。

对比原生 DDL:

原生 ALTER(COPY) gh-ost
锁表时间 全程持 MDL,可能数小时 仅 cut-over 毫秒级
在线读写 阻塞 正常
负载控制 无,一口气跑满 自动 throttle,可运行时调
中途可暂停 是(暂停/恢复/中止都安全)

前置条件(务必先检查)

gh-ost 不是装上就能跑,Aurora 上尤其有几个坑:

1. binlog 必须开启且为 ROW 格式

gh-ost 靠订阅 binlog 捕获增量。Aurora 默认可能没开 binlog。需要在集群参数组(不是实例参数组)确认:

-- 在 writer 上查运行值(不是看参数组的待生效值)
SHOW VARIABLES LIKE 'binlog_format';     -- 需为 ROW
SHOW VARIABLES LIKE 'binlog_row_image';  -- FULL(Aurora 8.0 引擎默认即 FULL)

⚠️ 踩坑提醒:binlog_format 是 Aurora 的集群级静态参数,改完必须重启实例才生效(reader 先重启、writer 走 failover)。别等到执行当天才发现没开——重启有中断,要提前排期。

另外,如果你在 reader 端点上查会看到 log_bin=OFF——这是 Aurora reader 的正常现象(reader 不产生 binlog),要以 writer 的运行值为准。

2. 专用账号 + 最小权限

CREATE USER 'ghost_user'@'%' IDENTIFIED BY '<强密码>';
-- 库级 DML/DDL(建影子表、回填、换表)
GRANT ALTER, CREATE, DELETE, DROP, INDEX, INSERT, LOCK TABLES, SELECT, TRIGGER, UPDATE
  ON `<db>`.* TO 'ghost_user'@'%';
-- 读 binlog(复制权限只能授在全局 *.*)
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'ghost_user'@'%';
FLUSH PRIVILEGES;

Aurora 不开放 SUPER 给用户,但 gh-ost 不需要 SUPER——加 --assume-rbr 跳过需要 SUPER 的 binlog 格式检查即可。

3. 表结构:无外键、无触发器

gh-ost 遇到外键触发器会直接拒绝执行。先确认:

-- 外键
SELECT * FROM information_schema.key_column_usage
WHERE table_schema='<db>' AND (table_name='<t>' OR referenced_table_name='<t>')
  AND referenced_table_name IS NOT NULL;
-- 触发器
SELECT * FROM information_schema.triggers
WHERE trigger_schema='<db>' AND event_object_table='<t>';

若有外键/触发器,改用 pt-online-schema-change

4. 评估表大小和耗时

SELECT table_rows,
       ROUND((data_length+index_length)/1024/1024/1024,2) AS total_gb
FROM information_schema.tables
WHERE table_schema='<db>' AND table_name='<t>';

经验值(4 vCPU / 32GB 实例 + I/O 优化存储):几十 GB 的表,gh-ost 拷贝耗时 2–4 小时(生产因业务负载 throttle 会更久)。一个 1 小时的维护窗口远远不够——但没关系,gh-ost 可以后台跑数小时,只有 cut-over 需要挑低负载时刻。

实操步骤

Step 1:封装一个执行脚本

把 gh-ost 参数固化成脚本,默认 dry-run,加 --execute 才真跑:

#!/usr/bin/env bash
set -euo pipefail

GHOST_BIN="./gh-ost"
DB_HOST="<cluster-writer-endpoint>"   # 用集群 writer 端点,自动指向当前 writer
DB_PORT="3306"
DB_USER="ghost_user"
DB_PASS="${DB_PASS:-}"                 # 从环境变量传,避免明文/特殊字符问题
DB_NAME="<db>"
DB_TABLE="$1"
ALTER_STMT="$2"
POSTPONE="/tmp/ghost.${DB_TABLE}.postpone"

MODE=""                                # 默认 dry-run
[[ "${3:-}" == "--execute" ]] && MODE="--execute" && touch "$POSTPONE"

"${GHOST_BIN}" \
  --host="${DB_HOST}" --port="${DB_PORT}" \
  --user="${DB_USER}" --password="${DB_PASS}" \
  --database="${DB_NAME}" --table="${DB_TABLE}" \
  --alter="${ALTER_STMT}" \
  --assume-rbr \
  --allow-on-master \
  --max-load="Threads_running=30,Threads_connected=1200" \
  --critical-load="Threads_running=60" \
  --critical-load-interval-millis=1000 \
  --chunk-size=1000 \
  --dml-batch-size=10 \
  --initially-drop-ghost-table --initially-drop-old-table \
  --postpone-cut-over-flag-file="${POSTPONE}" \
  --cut-over=default \
  --verbose \
  --skip-metadata-lock-check \
  ${MODE} 2>&1 | tee "ghost_${DB_TABLE}_$(date +%Y%m%d_%H%M%S).log"

几个关键参数:

  • --assume-rbr --allow-on-master:Aurora 无经典 replica 拓扑,直接连 writer;
  • --max-load=Threads_running=30:活跃线程超 30 就自动暂停拷贝(保护业务);
  • --critical-load=Threads_running=60:超 60 则自动中止(保命兜底);
  • --postpone-cut-over-flag-file:数据追平后不自动 cut-over,等你手动删这个文件才切——把毫秒级锁放到你盯着的低负载时刻;
  • --skip-metadata-lock-check:Aurora 默认没开 performance_schema 的 MDL instrument,加这个跳过检查(否则 gh-ost 会 bail out)。

Step 2:先 dry-run 校验

DB_PASS='xxx' ./run_ghost.sh my_big_table \
  "MODIFY COLUMN col_a varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, \
   MODIFY COLUMN col_b varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL"

不加 --execute 就是 dry-run。重点看输出里:binary logs validated(binlog OK)、账号权限校验通过、影子表建/改/删成功、无 FK/触发器报错、退出码 0。

Step 3:正式执行

命令末尾加 --execute。gh-ost 会建影子表 → binlog 增量同步 + 逐块回填 → 追平后因 postpone 文件停下等待。日志类似:

Copy: 21312000/64747231 32.9%; Applied: 154014; Backlog: 1/1000;
Lag: 0.02s; State: migrating; ETA: 1h10m

Step 4:全程监控

另开终端,通过 socket 实时看状态、控节流:

echo status      | nc -U /tmp/gh-ost.<db>.<table>.sock   # 查状态
echo throttle    | nc -U /tmp/gh-ost.<db>.<table>.sock   # 手动暂停拷贝
echo no-throttle | nc -U /tmp/gh-ost.<db>.<table>.sock   # 恢复

盯三个指标:

  • Lag:复制延迟。<1s 健康,持续 >30s 且爬升才危险;
  • Backlog:binlog 事件积压队列(如 1/1000)。只要 Lag 低,Backlog 满也不危险;
  • 数据库 Writer CPU:这才是要保护的对象。

Step 5:cut-over(挑低负载时刻)

数据拷到 100%、Lag 低时,删 postpone 文件触发切换:

rm /tmp/ghost.<db>.<table>.postpone

注意:不用死等 Backlog 恰好为 0。删文件只是「授权 cut-over」,gh-ost 会自己追平剩余增量、再在毫秒级窗口原子换表。

cut-over 成功的日志:

Lock & rename duration: 958ms. During mgtr time, queries were blocked
Tables renamed
Done migrating

整个 cut-over 只阻塞了不到 1 秒——对在线业务几乎无感。

避坑

1. 生产 CPU 被拷贝顶高,怎么调节

实战中,全速拷贝叠加业务高峰,把 Writer CPU 顶到了 88% 并持续了近 10 分钟。处置手段有三层,全部运行时生效、不用重启

手段 效果 对总耗时
echo throttle 完全暂停拷贝,CPU 立即降 暂停时长直接叠加
max-load=Threads_running=15(调低阈值) 业务稍忙就让路 变慢
nice-ratio=0.5(拷贝间加延迟) CPU 平缓降,拷贝不停 增约 1/3

实战中我们用了 nice-ratio=0.5(比完全 throttle 温和——拷一批就 sleep 半批时间),CPU 从 88% 降到 71–74%;等进入业务低谷窗口后再 nice-ratio=0 恢复全速。

echo "nice-ratio=0.5" | nc -U /tmp/gh-ost.<db>.<table>.sock   # 温和降速
echo "nice-ratio=0"   | nc -U /tmp/gh-ost.<db>.<table>.sock   # 恢复全速

理解区别throttle 是「停车等」(完全不动,时长叠加);nice-ratio 是「开慢车」(一直在开,只是慢)。想降 CPU 又不想完全停,用 nice-ratio

2. chunk-size 不能运行时改

chunk-size(每批拷多少行)影响 CPU 冲击强度和速度,但它是启动参数,运行中改不了。想调只能重启 gh-ost。所以启动时就要设合理值(默认 1000 比较稳;对超大表想温和点可设 500)。

3. 密码含特殊字符导致 Access denied

如果 gh-ost 报 Access denied 但手动 mysql -p 能连,多半是密码含 $ ` 等特殊字符,在 shell 里被解析了。解法:用 --conf 配置文件传密码,或用环境变量(<<'EOF' 写入配置文件,单引号 heredoc 防解析)。

4. cut-over 后旧表不会自动删

gh-ost 默认保留旧表(重命名为 _<table>_del),保守起见留个回滚余地。确认新表没问题、观察几天后再手动删回收空间:

DROP TABLE `<db>`.`_<table>_del`;

5. 先在克隆实例演练

上生产前,强烈建议用生产快照建一个克隆实例,把完整流程(dry-run → execute → cut-over → 验证)跑一遍。克隆实例没有在线业务,速度是「理想值」——生产因为 throttle 会慢 2–4 倍,但流程、结果正确性、cut-over 机制都能提前验证。

验证

cut-over 后确认结构和数据:

-- 列定义生效
SELECT column_name, column_type, character_maximum_length, collation_name, is_nullable
FROM information_schema.columns
WHERE table_schema='<db>' AND table_name='<table>'
  AND column_name IN ('col_a','col_b');
-- 行数一致
SELECT COUNT(*) FROM `<db>`.`<table>`;

总结

维度 原生 ALTER gh-ost
适用表大小 小表(几百 MB 内) 大表(几十 GB+)
锁表 全程 MDL,数小时 仅 cut-over 毫秒级
业务影响 阻塞,可能雪崩 几乎无感
可控性 运行时可 throttle / nice-ratio / 中止
复杂度 一条 SQL 需前置检查 + 监控

核心结论:

  • 几十 GB 的生产大表改结构,一律用 gh-ost(或 pt-osc),严禁直接 ALTER——尤其涉及 COLLATE/字符集变更时,原生 DDL 必然全表重建、锁表雪崩。
  • Aurora 上先确认 writer 的 binlog 已开为 ROW(集群参数组静态参数,改完要重启),这是 gh-ost 的命根子。
  • --postpone-cut-over-flag-file 把毫秒级 cut-over 放到你盯着的低负载窗口,别让它自动切。
  • CPU 高时用 nice-ratio 温和降速,比完全 throttle 更实用;chunk-size 要在启动时设好。
  • 务必先在克隆实例演练,生产耗时按克隆的 2–4 倍预估。

一次 37GB / 6000 万行的表结构变更,用 gh-ost 后台跑了约 2 小时,cut-over 只阻塞了 0.96 秒,在线业务全程无感——这就是在线 DDL 工具的价值。


本文基于一次真实的 AWS Aurora MySQL 生产 DDL 实战整理。

您还没有登录,请登录后发表评论。