凌晨 3 点挂维护公告迁移数据库的日子,你过够了吗?今天教你一套”在线 DDL + 跨云迁移”组合拳,迁移期间业务照跑、用户无感、DBA 睡整觉。
数据库迁移的三大噩梦
做过数据库迁移的人都知道,这活儿的恐怖程度和”线上改代码不测试直接上线”是一个级别。
噩梦 1:锁表导致服务不可用
传统 ALTER TABLE 在 MySQL 里经常需要锁表。你有个 500 万行的订单表,加个索引——整个交易系统开始 502,用户骂声一片,老板在群里 @ 你。
噩梦 2:数据不一致
你觉得在凌晨 2 点停机维护,dump 出来,导入新库,检查一下就可以。结果发现凌晨 1:59 分还有 3 个订单在写,导出来的数据少了 3 条。第二天客服电话被打爆。
噩梦 3:回滚困难
你没准备回滚方案,觉得”不可能失败”。结果真的失败了。新库数据脏了,老库已经停了写。你在凌晨 4 点,对着终端发呆。
今天这篇文章,我会用 gh-ost(GitHub 开源的在线 DDL 工具)和腾讯云 DTS(数据传输服务)来演示一套”零停机 + 可回滚”的完整迁移方案。
| 方案 | 停机时间 | 风险等级 | 回滚难度 |
|---|---|---|---|
| mysqldump + 停机导入 | 数小时 | 高 | 高 |
| 主从复制 + 切换 | 几分钟 | 中 | 中 |
| gh-ost + DTS 双重保障 | 0 | 低 | 低 |
第一招:gh-ost —— 在线改表不锁表
gh-ost 的原理(3 句话讲清楚)
- 它在原表上创建一个幽灵表(ghost table,
_your_table_gho) - 把原表数据分批拷贝到幽灵表,同时通过 binlog 实时同步增量变化
- 拷贝完成后,用一个原子操作把幽灵表 rename 成原表,整个切换时间 < 1 秒
实战:给 500 万行订单表加索引
# 安装 gh-ost(推荐 Docker)
docker pull ghcr.io/github/gh-ost:latest
# 假设你在腾讯云上有台 MySQL 实例
# 内网 IP: 10.0.1.100,端口: 3306
# 要去给 orders 表加一个复合索引
gh-ost \
--host="10.0.1.100" \
--port=3306 \
--user="migration" \
--password="your_password" \
--database="ecommerce" \
--table="orders" \
--alter="ADD INDEX idx_user_status (user_id, status)" \
--allow-on-master \
--execute \
--max-load='Threads_running=50' \
--critical-load='Threads_running=100' \
--chunk-size=2000 \
--cut-over=atomic \
--exact-rowcount \
--verbose
关键参数解释:
| 参数 | 作用 | 建议值 |
|---|---|---|
--max-load |
超过这个负载就暂停 | Threads_running=50 |
--critical-load |
超过就直接终止(保护业务) | Threads_running=100 |
--chunk-size |
每批拷贝行数 | 1000-5000(看服务器性能) |
--cut-over=atomic |
用原子 rename 切换 | 必须加! |
--allow-on-master |
允许在主库上执行 | 谨慎使用 |
执行过程中,业务代码正常读写 orders 表。gh-ost 会在后台默默地同步数据,你可以通过日志监控进度:
[INFO] Copy: 250000/5000000 5.0%; Applied: 150; Backlog: 2/100; Time: 3m15s(total), 3m15s(copy); ...
一条 500 万行的表,加个索引大约需要 20-30 分钟(取决于服务器性能和 binlog 写入速度)。期间业务完全不停。
gh-ost 的恢复机制
万一迁移中途挂了,或者你想取消:
# gh-ost 会在数据库中创建一个日志表 _orders_ghc
# 直接删除幽灵表即可恢复
mysql -h 10.0.1.100 -u root -e "DROP TABLE IF EXISTS _orders_gho; DROP TABLE IF EXISTS _orders_ghc;"
这就是 gh-ost 比 pt-online-schema-change 更安全的地方——gh-ost 不依赖触发器,不会有触发器导致的性能问题。
第二招:腾讯云 DTS —— 跨云 / 跨实例平滑迁移
gh-ost 搞定在线改表,但要把整个数据库”搬家”到云上,还得靠 DTS(数据传输服务)。
场景:把自建 MySQL 5.7 迁移到腾讯云 CDB MySQL 8.0
Step 1:在腾讯云控制台创建迁移任务
# 用 Python SDK 创建 DTS 迁移任务
# pip install tencentcloud-sdk-python-dts
from tencentcloud.common import credential
from tencentcloud.dts.v20211206 import dts_client, models
cred = credential.Credential("your-secret-id", "your-secret-key")
client = dts_client.DtsClient(cred, "ap-guangzhou")
# 1. 创建迁移任务
req = models.CreateMigrateJobRequest()
req.SrcDatabaseType = "mysql"
req.SrcRegion = "ap-guangzhou"
req.DstDatabaseType = "mysql"
req.DstRegion = "ap-guangzhou"
req.InstanceClass = "small" # 小规格即可,按需升级
req.JobName = "自建MySQL到腾讯云CDB迁移"
resp = client.CreateMigrateJob(req)
job_id = resp.JobId
print(f"迁移任务已创建,JobId: {job_id}")
Step 2:配置源和目标
# 2. 配置源实例信息
src_req = models.ConfigureMigrateJobRequest()
src_req.JobId = job_id
src_req.SrcInfo = models.SrcInfo()
src_req.SrcInfo.Region = "ap-guangzhou"
src_req.SrcInfo.AccessType = "cdb" # 或 extranet/dcg/vpncloud
src_req.SrcInfo.InstanceId = "cdb-xxxxxxxx"
# 目标实例
src_req.DstInfo = models.DstInfo()
src_req.DstInfo.Region = "ap-guangzhou"
src_req.DstInfo.InstanceId = "cdb-yyyyyyyy"
# 迁移选项:结构 + 全量 + 增量
src_req.MigrateOption = models.MigrateOption()
src_req.MigrateOption.DatabaseTable = models.DatabaseTable()
src_req.MigrateOption.DatabaseTable.ObjectMode = "partial"
# 指定要迁移的数据库
db_item = models.DBItem()
db_item.DbName = "ecommerce"
db_item.NewDbName = "ecommerce"
# 指定表(留空 = 全部)
db_item.Tables = [] # 空数组表示迁移所有表
src_req.MigrateOption.DatabaseTable.Databases = [db_item]
client.ConfigureMigrateJob(src_req)
print("配置完成")
Step 3:启动增量同步并验证
# 3. 启动迁移
start_req = models.StartMigrateJobRequest()
start_req.JobId = job_id
client.StartMigrateJob(start_req)
# 4. 监控迁移进度
import time
while True:
desc_req = models.DescribeMigrateJobsRequest()
desc_req.JobId = job_id
resp = client.DescribeMigrateJobs(desc_req)
job = resp.JobList[0]
status = job.Status
print(f"状态: {status}, 进度: {job.MigrateOption}")
if status == "complete":
print("全量迁移完成,增量同步运行中...")
break
elif status == "failed":
print("迁移失败,检查错误日志")
break
time.sleep(30)
print("""
你可以做以下验证:
1. 检查目标库行数:SELECT COUNT(*) FROM ecommerce.orders
2. 对比源库和目标库最后一条记录
3. 在源库插入一条测试数据,看目标库是否实时同步
""")
Step 4:切换流量(零停机)
确认增量同步稳定运行后,选一个业务低峰期切换:
# 5. 完成迁移(停止增量同步,收尾)
complete_req = models.CompleteMigrateJobRequest()
complete_req.JobId = job_id
client.CompleteMigrateJob(complete_req)
print("迁移完成,DTS 任务已关闭")
第三招:gh-ost + DTS 组合拳
实际生产环境中,往往需要两招一起用:
┌─────────────────────────────────────────────────────┐
│ 迁移流程(5 步) │
├─────────────────────────────────────────────────────┤
│ Step 1: DTS 全量迁移(源 → 腾讯云 CDB) │
│ ↓ 期间业务正常读写源库 │
│ Step 2: DTS 增量同步启动(追平 binlog 差异) │
│ ↓ 目标库和源库实时一致 │
│ Step 3: gh-ost 在目标库上执行 DDL 变更 │
│ ↓ 不受源库迁移影响 │
│ Step 4: 验证 + 切换应用连接地址 │
│ ↓ 秒级切换 │
│ Step 5: 保留源库一周作为回滚备份 │
└─────────────────────────────────────────────────────┘
自动化切换脚本
#!/bin/bash
# migrate_switch.sh —— 一键完成 DTS 切换 + gh-ost DDL
DTS_JOB_ID="dts-xxxxxxxx"
SOURCE_HOST="10.0.1.100"
TARGET_HOST="10.0.2.200"
DB_NAME="ecommerce"
echo "===== 1. 检查 DTS 增量同步延迟 ====="
# 用腾讯云 CLI 查询(需先安装 tccli)
DELAY=$(tccli dts DescribeMigrateJobs --JobId "$DTS_JOB_ID" \
| jq '.JobList[0].Detail.IncrementalInfo.MasterSlaveDistance')
echo "当前延迟: ${DELAY} 秒"
if [ "$DELAY" -gt 5 ]; then
echo "⚠️ 延迟 > 5s,等待追上..."
sleep 60
fi
echo "===== 2. 确认数据一致性 ====="
SOURCE_COUNT=$(mysql -h $SOURCE_HOST -u root -p"$PASS" -N -e "SELECT COUNT(*) FROM ${DB_NAME}.orders")
TARGET_COUNT=$(mysql -h $TARGET_HOST -u root -p"$PASS" -N -e "SELECT COUNT(*) FROM ${DB_NAME}.orders")
echo "源库: $SOURCE_COUNT 行, 目标库: $TARGET_COUNT 行"
if [ "$SOURCE_COUNT" != "$TARGET_COUNT" ]; then
echo "❌ 数据不一致,终止切换!"
exit 1
fi
echo "===== 3. gh-ost 在线 DDL(目标库上执行)====="
docker run --rm ghcr.io/github/gh-ost:latest \
--host="$TARGET_HOST" \
--user="root" --password="$PASS" \
--database="$DB_NAME" --table="orders" \
--alter="ADD INDEX idx_created (created_at)" \
--allow-on-master --execute --cut-over=atomic
echo "===== 4. 完成 DTS 任务 ====="
# Python SDK 或 tccli 完成
tccli dts CompleteMigrateJob --JobId "$DTS_JOB_ID"
echo "===== ✅ 迁移完成!请修改应用数据库连接地址 ====="
echo "旧库: $SOURCE_HOST"
echo "新库: $TARGET_HOST"
echo "建议保留旧库运行 7 天作为回滚备份"
避坑清单
| 坑 | 现象 | 解决方案 |
|---|---|---|
| gh-ost 主库压力大 | 业务响应变慢 | 降低 --chunk-size,提高 --max-load |
| DTS 不支持某些 DDL | 增量同步中断 | 先在源库停写,再 DTS 全量同步 |
| 自增 ID 冲突 | 新旧数据 ID 重复 | 源库自增步长改为 2,新旧库交错 |
| 字符集不一致 | 中文乱码 | 确认源和目标都是 utf8mb4 |
| 大事务导致延迟 | 增量同步追不上 | 拆分大事务,避免批量 DELETE |
总结
数据库迁移不是技术问题,是心态问题。大部分团队把迁移搞成”通宵 + 停机公告”模式,是因为没找到对的工具。
- gh-ost:在线 DDL,不锁表,随时可回滚
- 腾讯云 DTS:增量同步,边迁边用,验证完再切
- 两招组合:自建 MySQL → 腾讯云 CDB,零停机 + 零丢数据
下次老板说”我们迁移到云上吧”,你不用再默默掏出保温杯泡枸杞了。
👤 作者简介
一枚在大中原腹地(河南)卖公有云的从业者,主营腾讯云/阿里云/火山云,曾踩坑无数,现专注AI大模型应用落地。关注公众号「公有云cloud」,围观AI前沿动态~
博客:y1cloud.cn
