数据库迁移零停机实战:用 gh-ost + 腾讯云 DTS 把 500 万行数据平滑迁到云上,用户毫无感知

阿里云上云采购季 2核2G云服务器99元/年 续费同价

凌晨 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 句话讲清楚)

  1. 它在原表上创建一个幽灵表(ghost table,_your_table_gho)
  2. 把原表数据分批拷贝到幽灵表,同时通过 binlog 实时同步增量变化
  3. 拷贝完成后,用一个原子操作把幽灵表 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

发表评论