上云迁移验收-CVM健康校验用例与数据库比对报告

上云迁移验收实战:一次产出 CVM 健康校验用例 + 数据库迁移比对报告

分类:云计算 / 运维 / 数据库

标签:腾讯云、上云迁移、CVM、MySQL、验收、自动化运维、DTS

摘要:迁移做完不等于迁移成功。本文给出一套可复用的验收体系:4 层 CVM 健康校验用例、5 类数据库比对指标、一份可自动生成的验收报告、以及"什么情况下必须回滚"的决策树。所有脚本可直接用于生产。


一、为什么需要一个"验收体系"

我做过的迁移里,出问题的都不是迁移本身,而是验收标准太模糊

  • 运维说"机器通了"------但安全组没放开调用方
  • 开发说"接口通了"------但连的是测试库
  • DBA 说"数据同步了"------但只对了行数,金额精度错了
  • 项目经理说"迁移完成"------依据是"没人报错"

所以我把验收拆成 6 个层次、可执行的用例,每一条都有明确 PASS/FAIL 判定,且全部脚本化。

text 复制代码
L0  基础设施    实例状态、镜像、磁盘、标签
L1  系统层      时区、依赖、内核参数、时钟
L2  进程层      systemd 状态、重启次数、resource 限制
L3  网络层      端口监听、安全组可达、下游依赖连通
L4  业务层      接口探针、日志写入、关键链路
L5  数据层      行数、金额、主键校验和、抽样、外键一致性

L0~L4 属于 CVM 侧,L5 属于数据库侧。只做 L0~L3 的验收是假验收


二、CVM 侧:4 层健康校验脚本

2.1 完整脚本 accept_cvm.sh

bash 复制代码
#!/bin/bash
###############################################################################
# accept_cvm.sh ------ CVM 上云验收脚本
# 用法: accept_cvm.sh -e <dev|test|prod> [-o report.md]
# 输出: 控制台结果 + Markdown 报告
###############################################################################
set -uo pipefail

APP_NAME="${APP_NAME:-biz-web}"
PORT="${PORT:-8080}"
ENV=""
REPORT=""
TOTAL=0
PASSED=0
declare -a RESULTS=()

while getopts "e:o:" opt; do
  case $opt in
    e) ENV="$OPTARG" ;;
    o) REPORT="$OPTARG" ;;
    *) echo "usage: $0 -e <env> [-o report.md]"; exit 1 ;;
  esac
done
[ -n "$ENV" ] || { echo "FATAL: -e 必填"; exit 1; }

# ---------- 通用断言 ----------
check() {
  local layer="$1" name="$2" ; shift 2
  TOTAL=$((TOTAL + 1))
  local out
  if out=$("$@" 2>&1); then
    PASSED=$((PASSED + 1))
    printf '  [PASS] %s\n' "$name"
    RESULTS+=("| ${layer} | ${name} | PASS | ${out:-ok} |")
  else
    printf '  [FAIL] %s => %s\n' "$name" "$out"
    RESULTS+=("| ${layer} | ${name} | **FAIL** | ${out:-failed} |")
  fi
}

# ================= L0 基础设施 =================
l0_instance() {
  local meta="http://metadata.tencentyun.com/latest/meta-data"
  local iid
  iid=$(curl -s -m 3 "${meta}/instance-id") || return 1
  echo "${iid}"
}

l0_tags() {
  local iid env_tag
  iid=$(curl -s -m 3 "http://metadata.tencentyun.com/latest/meta-data/instance-id")
  env_tag=$(tccli cvm DescribeInstances --InstanceIds "[\"${iid}\"]" \
    --query 'InstanceSet[0].Tags[?Key==`env`].Value | [0]' -o text 2>/dev/null)
  [ "${env_tag}" = "${ENV}" ] || { echo "标签 env=${env_tag},期望 ${ENV}"; return 1; }
  echo "标签一致: ${iid} env=${ENV}"
}

l0_disk() {
  local use
  use=$(df -P /data 2>/dev/null | awk 'NR==2{gsub("%","",$5); print $5}')
  [ -n "${use}" ] || { echo "无 /data 挂载点"; return 1; }
  [ "${use}" -lt 85 ] || { echo "/data 使用率 ${use}%"; return 1; }
  echo "/data 使用率 ${use}%"
}

# ================= L1 系统层 =================
l1_timezone() {
  local tz
  tz=$(timedatectl show -p Timezone --value)
  [ "${tz}" = "Asia/Shanghai" ] || { echo "时区为 ${tz}"; return 1; }
  echo "时区 ${tz}"
}

l1_clock_offset() {
  local off
  off=$(chronyc tracking 2>/dev/null | awk '/System time/{print $4}')
  [ -n "${off}" ] || { echo "chrony 未运行"; return 1; }
  # 偏移绝对值小于 1 秒
  awk -v o="${off}" 'BEGIN{ if ((o<0?-o:o) < 1.0) exit 0; exit 1 }' \
    || { echo "时钟偏移 ${off}s"; return 1; }
  echo "时钟偏移 ${off}s"
}

l1_deps() {
  local missing=()
  for c in java systemctl curl mysql; do
    command -v "$c" >/dev/null 2>&1 || missing+=("$c")
  done
  [ ${#missing[@]} -eq 0 ] || { echo "缺少: ${missing[*]}"; return 1; }
  echo "依赖齐全"
}

l1_ulimit() {
  local nofile
  nofile=$(systemctl show "${APP_NAME}" -p LimitNOFILE --value)
  [ -n "${nofile}" ] && [ "${nofile}" != "infinity" ] && [ "${nofile}" -ge 65535 ] \
    || { echo "LimitNOFILE=${nofile}"; return 1; }
  echo "LimitNOFILE=${nofile}"
}

# ================= L2 进程层 =================
l2_service_active() {
  systemctl is-active --quiet "${APP_NAME}" || { echo "非 active"; return 1; }
  echo "active"
}

l2_restarts() {
  local n
  n=$(systemctl show "${APP_NAME}" -p NRestarts --value)
  [ "${n:-0}" -le 3 ] || { echo "重启 ${n} 次"; return 1; }
  echo "NRestarts=${n:-0}"
}

l2_uptime() {
  local since now secs
  since=$(systemctl show "${APP_NAME}" -p ActiveEnterTimestampMonotonic --value)
  now=$(cut -d' ' -f1 /proc/uptime | tr -d '.')
  secs=$(( (${now:0:8} - ${since:0:8}) / 1000000 ))
  [ "${secs}" -ge 30 ] || { echo "启动仅 ${secs}s,可能仍在重启循环"; return 1; }
  echo "已稳定运行 ${secs}s"
}

# ================= L3 网络层 =================
l3_listen() {
  ss -lntp 2>/dev/null | grep -q ":${PORT} " || { echo "端口 ${PORT} 未监听"; return 1; }
  echo "监听 ${PORT}"
}

l3_bind_all() {
  local ip
  ip=$(hostname -I | awk '{print $1}')
  curl -s -o /dev/null -m 5 "http://${ip}:${PORT}/actuator/health" \
    || { echo "未绑定内网 IP ${ip}"; return 1; }
  echo "内网 IP ${ip} 可达"
}

l3_downstream() {
  local dep host port
  for dep in "${DOWNSTREAM:-}"; do
    [ -z "${dep}" ] && continue
    host="${dep%:*}"; port="${dep#*:}"
    timeout 3 bash -c "echo > /dev/tcp/${host}/${port}" 2>/dev/null \
      || { echo "不可达 ${dep}"; return 1; }
  done
  echo "下游依赖全部可达"
}

# ================= L4 业务层 =================
l4_health_endpoint() {
  local code
  code=$(curl -s -o /dev/null -m 5 -w '%{http_code}' \
    "http://127.0.0.1:${PORT}/actuator/health")
  [ "${code}" = "200" ] || { echo "health 返回 ${code}"; return 1; }
  echo "HTTP 200"
}

l4_business_endpoint() {
  local code
  code=$(curl -s -o /dev/null -m 8 -w '%{http_code}' \
    "http://127.0.0.1:${PORT}/api/v1/order/query?orderNo=SELFTEST&env=${ENV}")
  # 允许 200(查到)或 404(查不到但链路通),不允许 5xx
  case "${code}" in
    200|404) echo "业务接口 HTTP ${code}" ;;
    *) echo "业务接口返回 ${code}"; return 1 ;;
  esac
}

l4_log_writing() {
  local logfile="/data/app/${APP_NAME}/shared/logs/stdout.log"
  [ -f "${logfile}" ] || { echo "日志文件不存在"; return 1; }
  [ "$(find "${logfile}" -mmin -5 | wc -l)" -ge 1 ] \
    || { echo "近 5 分钟无日志写入"; return 1; }
  echo "近 5 分钟有写入"
}

l4_no_error_spike() {
  local logfile="/data/app/${APP_NAME}/shared/logs/stderr.log"
  [ -f "${logfile}" ] || { echo "无 stderr(正常)"; return 0; }
  local n
  n=$(tail -n 500 "${logfile}" | grep -ciE "ERROR|Exception" || true)
  [ "${n}" -le 10 ] || { echo "近期 ERROR/Exception ${n} 条"; return 1; }
  echo "近期 ERROR ${n} 条"
}

# ================= 执行 =================
echo "###############################################"
echo "# CVM 上云验收  env=${ENV}  $(date '+%F %T')"
echo "###############################################"

echo "L0 基础设施层"
check L0 "实例元数据可获取"     l0_instance
check L0 "环境标签一致"         l0_tags
check L0 "磁盘水位 < 85%"       l0_disk

echo "L1 系统层"
check L1 "时区 Asia/Shanghai"   l1_timezone
check L1 "时钟偏移 < 1s"        l1_clock_offset
check L1 "依赖命令齐全"         l1_deps
check L1 "文件句柄 >= 65535"    l1_ulimit

echo "L2 进程层"
check L2 "systemd active"       l2_service_active
check L2 "重启次数 <= 3"         l2_restarts
check L2 "运行时长 >= 30s"       l2_uptime

echo "L3 网络层"
check L3 "端口监听"             l3_listen
check L3 "绑定内网 IP"          l3_bind_all
check L3 "下游依赖可达"         l3_downstream

echo "L4 业务层"
check L4 "存活探针 200"         l4_health_endpoint
check L4 "业务接口非 5xx"       l4_business_endpoint
check L4 "日志持续写入"         l4_log_writing
check L4 "无错误日志尖峰"       l4_no_error_spike

echo "###############################################"
echo "# 结果: ${PASSED}/${TOTAL} 通过"
echo "###############################################"

if [ -n "${REPORT}" ]; then
  {
    echo "## CVM 验收报告"
    echo ""
    echo "- 环境: ${ENV}"
    echo "- 时间: $(date '+%F %T')"
    echo "- 结果: **${PASSED}/${TOTAL} 通过**"
    echo ""
    echo "| 层级 | 检查项 | 结果 | 详情 |"
    echo "| --- | --- | --- | --- |"
    printf '%s\n' "${RESULTS[@]}"
  } > "${REPORT}"
  echo "报告已写入 ${REPORT}"
fi

[ "${PASSED}" -eq "${TOTAL}" ]

2.2 批量执行

bash 复制代码
#!/bin/bash
# accept_all.sh ------ 多台 CVM 并发验收并汇总报告
set -uo pipefail

HOSTS_FILE="${1:-hosts.txt}"
ENV="${2:-prod}"
OUT_DIR="report-$(date +%Y%m%d-%H%M%S)"
mkdir -p "${OUT_DIR}"

mapfile -t HOSTS < <(grep -vE '^#|^$' "${HOSTS_FILE}")

printf '%s\n' "${HOSTS[@]}" | xargs -P 8 -I{} bash -c '
  h="$1"; env="$2"; out="$3"
  ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 "root@${h}" \
    "APP_NAME=biz-web PORT=8080 DOWNSTREAM=biz-db.prod.internal:3306 \
     /opt/ops/accept_cvm.sh -e ${env} -o /tmp/acc.md" >/dev/null 2>&1
  scp -q "root@${h}:/tmp/acc.md" "${out}/${h}.md" 2>/dev/null \
    || echo "| ${h} | 无法连接或脚本失败 |" > "${out}/${h}.md"
' _ {} "${ENV}" "${OUT_DIR}"

echo "=== 汇总 ==="
fail=0
for f in "${OUT_DIR}"/*.md; do
  host=$(basename "$f" .md)
  if grep -q "FAIL" "$f"; then
    echo "[FAIL] ${host}"
    fail=$((fail + 1))
  else
    echo "[PASS] ${host}"
  fi
done

{
  echo "# CVM 批量验收汇总"
  echo ""
  echo "主机数: ${#HOSTS[@]},失败: ${fail}"
  echo ""
  for f in "${OUT_DIR}"/*.md; do
    echo "## $(basename "$f" .md)"
    echo ""
    cat "$f"
    echo ""
  done
} > "${OUT_DIR}/SUMMARY.md"

echo "汇总报告: ${OUT_DIR}/SUMMARY.md"
exit "${fail}"

三、数据库侧:5 类比对指标

只对行数是不够的。下面是我们实际用的 5 类指标,覆盖"数量对、内容错"的隐蔽问题。

3.1 第 1 类:行数与主键范围

sql 复制代码
-- 在源库与目标库分别执行,比对结果
SELECT 'TABLE_COUNT' AS metric, COUNT(*) AS val FROM information_schema.TABLES
WHERE TABLE_SCHEMA='biz' AND TABLE_TYPE='BASE TABLE'
UNION ALL
SELECT CONCAT('COUNT_', 'biz_order'), COUNT(*) FROM biz_order
UNION ALL
SELECT CONCAT('COUNT_', 'sys_user'), COUNT(*) FROM sys_user
UNION ALL
SELECT CONCAT('COUNT_', 'biz_order_item'), COUNT(*) FROM biz_order_item;
sql 复制代码
-- 主键自增位点必须比源库大,否则迁移后新写入会主键冲突
SELECT TABLE_NAME, AUTO_INCREMENT FROM information_schema.TABLES
WHERE TABLE_SCHEMA='biz' AND AUTO_INCREMENT IS NOT NULL;

踩坑 :DTS 全量迁移后自增位点常常不同步,如果不检查,切换写流量后第一条 INSERT 就主键冲突。必须显式比对 AUTO_INCREMENT

3.2 第 2 类:金额/数值精度

sql 复制代码
-- 金额不允许用 FLOAT 比对,必须精确比对
SELECT
  COUNT(*)                      AS cnt,
  CAST(SUM(amount) AS DECIMAL(20,2))       AS sum_amount,
  CAST(SUM(ROUND(amount,2)) AS DECIMAL(20,2)) AS sum_rounded,
  MIN(amount) AS min_amount,
  MAX(amount) AS max_amount
FROM biz_order
WHERE created_at >= '2026-01-01';

-- 找出小数位异常的行(精度被截断的信号)
SELECT id, order_no, amount
FROM biz_order
WHERE amount <> ROUND(amount, 2)
LIMIT 20;

判定sum_amount 必须完全一致(DECIMAL 精确比较)。如果差几分钱,说明有字段类型被降级(DECIMAL(18,4)DECIMAL(18,2))。

3.3 第 3 类:主键校验和(最有效的一招)

行数相同但内容不同,是最难发现的。用 CRC32 + BIT_XOR 做全表内容指纹:

sql 复制代码
-- 全表内容指纹:任意一行内容变化都会导致结果不同
SELECT
  COUNT(*) AS cnt,
  BIT_XOR(CRC32(CONCAT_WS('#', id, order_no, user_id, amount, status,
                              DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s')))) AS checksum
FROM biz_order;

注意两个坑

  1. CONCAT_WS 需要用分隔符,避免 'ab' + 'c''a' + 'bc' 撞车
  2. NULLCONCAT_WS 中会被跳过,可能造成不同数据同指纹。对可空列用 IFNULL(col, '\\N') 显式标记:
sql 复制代码
SELECT BIT_XOR(CRC32(CONCAT_WS('#',
         id,
         order_no,
         IFNULL(remark, '\\N'),
         DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s')))) AS checksum
FROM biz_order;

大表分批校验(避免一次性扫全表造成压力):

sql 复制代码
-- 按主键区间分批,每批 100 万
SELECT
  FLOOR(id / 1000000) AS batch,
  COUNT(*) AS cnt,
  BIT_XOR(CRC32(CONCAT_WS('#', id, order_no, IFNULL(amount,0)))) AS checksum
FROM biz_order
GROUP BY batch
ORDER BY batch;

3.4 第 4 类:外键与引用完整性

sql 复制代码
-- 孤儿数据检测:明细表找不到主表的行
SELECT COUNT(*) AS orphan_count
FROM biz_order_item i
LEFT JOIN biz_order o ON i.order_id = o.id
WHERE o.id IS NULL;

-- 源库与目标库都执行,结果应都是 0(或都是同一个数)

DTS 迁移过程中如果表顺序不当,可能出现外键引用不到主表的情况。腾讯云 DTS 有"表关联迁移"选项,但仍建议迁移后主动校验一次。

3.5 第 5 类:业务语义抽样比对

统计指标对不上时,需要落到具体行上定位:

sql 复制代码
-- 随机抽样 1000 行做逐行比对(用固定种子保证两边取到同样的行)
-- 源库
SELECT id, order_no, user_id, amount, status, created_at
FROM biz_order
WHERE id IN (SELECT id FROM (
    SELECT id FROM biz_order ORDER BY CRC32(CONCAT(id, 'fixed-seed')) LIMIT 1000
  ) t)
ORDER BY id;

-- 目标库执行同样的 SQL,两边结果导出后 diff
bash 复制代码
# 导出并比对
mysql -h$SRC -N -B -e "$SQL" biz > sample_src.tsv
mysql -h$DST -N -B -e "$SQL" biz > sample_dst.tsv
diff sample_src.tsv sample_dst.tsv && echo "抽样 1000 行完全一致"

3.6 时间与时区陷阱(单独强调)

sql 复制代码
-- 迁移前后时区设置不一致会导致 DATETIME 值偏移 8 小时
SELECT @@global.time_zone, @@session.time_zone;
SHOW VARIABLES LIKE 'time_zone';

-- 抽样比对时,created_at 必须逐字节相同
-- 如果目标库值普遍 +8h 或 -8h,说明时区参数配置不一致
SELECT id, created_at FROM biz_order ORDER BY id DESC LIMIT 5;

这是最容易被忽略、后果最严重的比对项 。我们第一次对账时行数、金额全对,但订单时间整体偏移 8 小时------原因是源库参数组 time_zone=SYSTEM(CST),目标库实例新建时是 UTC。


四、自动生成验收报告

把 CVM 侧与数据库侧的结果合并成一份可交付的报告:

python 复制代码
#!/usr/bin/env python3
"""
gen_report.py ------ 汇总 CVM 验收 + 数据库比对,生成 Markdown 验收报告
用法: python3 gen_report.py --cvm-dir report-xxx --db-result db_result.json
"""
import argparse
import glob
import json
import os
import re
from datetime import datetime


def parse_cvm_report(path):
    """从 accept_cvm.sh 生成的 md 中提取结果"""
    txt = open(path, encoding='utf-8').read()
    m = re.search(r'结果: \*\*(\d+)/(\d+) 通过\*\*', txt)
    if m:
        return int(m.group(1)), int(m.group(2))
    return 0, 0


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--cvm-dir', required=True)
    ap.add_argument('--db-result', required=True)
    ap.add_argument('--env', default='prod')
    ap.add_argument('--out', default='ACCEPTANCE_REPORT.md')
    args = ap.parse_args()

    db = json.load(open(args.db_result, encoding='utf-8'))

    total_hosts = passed_hosts = 0
    host_rows = []
    for f in sorted(glob.glob(os.path.join(args.cvm_dir, '*.md'))):
        host = os.path.basename(f)[:-3]
        if host == 'SUMMARY':
            continue
        p, t = parse_cvm_report(f)
        total_hosts += 1
        if t and p == t:
            passed_hosts += 1
        host_rows.append((host, p, t, 'PASS' if t and p == t else '**FAIL**'))

    lines = [
        f"# 上云迁移验收报告",
        "",
        f"- 环境: **{args.env}**",
        f"- 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
        f"- 验收结论: "
        f"**{'通过' if passed_hosts == total_hosts and db.get('all_pass') else '未通过'}**",
        "",
        "## 一、CVM 验收汇总",
        "",
        f"主机数 {total_hosts},完全通过 {passed_hosts}",
        "",
        "| 主机 | 通过项 | 总数 | 结果 |",
        "| --- | --- | --- | --- |",
    ]
    for host, p, t, r in host_rows:
        lines.append(f"| {host} | {p} | {t} | {r} |")

    lines += [
        "",
        "## 二、数据库比对结果",
        "",
        "| 指标 | 源库 | 目标库 | 结果 |",
        "| --- | --- | --- | --- |",
    ]
    for item in db.get('checks', []):
        r = 'PASS' if item['same'] else '**FAIL**'
        lines.append(
            f"| {item['name']} | {item['src']} | {item['dst']} | {r} |")

    lines += [
        "",
        "## 三、结论与后续动作",
        "",
        f"- CVM 侧: {passed_hosts}/{total_hosts} 通过",
        f"- 数据库侧: "
        f"{'全部比对项一致' if db.get('all_pass') else '存在差异,需人工确认'}",
        "",
    ]
    if passed_hosts == total_hosts and db.get('all_pass'):
        lines += [
            "**验收通过。** 可以执行下一步:写流量切换。",
            "",
            "切换前请确认:",
            "1. 源库增量同步延迟 < 1s",
            "2. 回滚方案已演练过至少一次",
            "3. 切换窗口已通知业务方",
        ]
    else:
        lines += [
            "**验收未通过。** 请勿切换流量,按以下顺序处理:",
            "1. 定位失败项根因(见上方 FAIL 行)",
            "2. 修复后重新执行本报告生成流程",
            "3. 若为数据差异,先评估是否继续使用 DTS 追平",
        ]

    open(args.out, 'w', encoding='utf-8').write('\n'.join(lines))
    print(f"报告已生成: {args.out}")


if __name__ == '__main__':
    main()

报告样例

markdown 复制代码
# 上云迁移验收报告

- 环境: **prod**
- 生成时间: 2026-09-11 16:20:33
- 验收结论: **通过**

## 一、CVM 验收汇总

主机数 12,完全通过 12

| 主机 | 通过项 | 总数 | 结果 |
| --- | --- | --- | --- |
| 10.10.30.11 | 17 | 17 | PASS |
| 10.10.30.12 | 17 | 17 | PASS |
| 10.10.30.13 | 17 | 17 | PASS |

## 二、数据库比对结果

| 指标 | 源库 | 目标库 | 结果 |
| --- | --- | --- | --- |
| 表数量 | 214 | 214 | PASS |
| biz_order 行数 | 8743215 | 8743215 | PASS |
| biz_order 金额合计 | 983241442.77 | 983241442.77 | PASS |
| biz_order 内容指纹 | 3f2a91c4 | 3f2a91c4 | PASS |
| 孤儿明细行 | 0 | 0 | PASS |
| 抽样 1000 行 diff | 0 差异 | 0 差异 | PASS |
| AUTO_INCREMENT | 8743300 | 8743300 | PASS |

## 三、结论与后续动作

**验收通过。** 可以执行下一步:写流量切换。

五、回滚决策树

验收不通过怎么办?先决定回不回滚,再决定修不修

text 复制代码
验收失败
│
├─ 失败项是否影响业务正确性?
│   (数据不一致 / 金额错误 / 接口 5xx)
│   │
│   ├─ 是 ──► 【立即回滚】切回源库(读流量已切换的情况下)
│   │         回滚后:保留现场,导出日志与比对结果,再排查
│   │
│   └─ 否(如磁盘水位 86%、时区告警)
│          │
│          ├─ 能否在 30 分钟内修复且无需重启业务?
│          │   ├─ 能 ──► 修复后重跑验收,继续推进
│          │   └─ 不能 ──► 【回滚】不要赌,窗口比面子重要
│          │
│          └─ 是否可在切换后修复?
│              ├─ 能(如日志轮转配置)──► 记录为遗留项,带 todo 推进
│              └─ 不能 ──► 【回滚】
│
└─ 已切换写流量(不可逆点之后)
     └─ 回滚需反向 DTS,成本极高
        └─ 此时唯一选择:就地修复 + 快速止血
           (提前演练反向同步是唯一的保险)

最关键的一条经验在"不可逆点"之前,任何犹豫都选择回滚。 回滚的代价是几小时,硬扛的代价可能是几天的数据修复。

我们准备的"不可逆点"是:写流量切换 + 停止 DTS 同步。这两个动作必须由同一个人、在同一分钟内顺序确认执行,且有独立的第二人复核。


六、一页纸验收清单(可打印)

text 复制代码
上云迁移验收清单                                  环境: ________
执行人: ____________  时间: ____________

【T-1 天】
[ ] 回滚方案已演练(含反向 DTS 可行性确认)
[ ] 变更窗口已通知业务方与客服
[ ] 源库增量同步延迟基线已记录(当前: ____ ms)
[ ] 目标库参数组已与源库逐项比对确认
[ ] lower_case_table_names 一致(不可改,务必确认)

【T 时:切换前】
[ ] CVM 验收 17/17 全通过(批改报告已生成)
[ ] DB 比对 7 项全一致
[ ] AUTO_INCREMENT 位点已校验
[ ] 时区一致性已确认(时间字段无偏移)
[ ] 抽样 1000 行 diff 为 0

【切换中】
[ ] 读流量 5%  -> 观察 10 min(错误率、P99)
[ ] 读流量 50% -> 观察 30 min
[ ] 读流量 100% -> 观察 2 h(此时仍可秒级回滚)
[ ] 确认无误后,切换写流量
[ ] 确认无误后,停止 DTS

【T+1 天】
[ ] 慢查询日志与迁移前对比(新增慢查询数: ____)
[ ] 错误日志无新增异常类型
[ ] 数据库连接数、CPU 水位正常
[ ] 备份策略已生效并验证过恢复
[ ] 遗留 todo 已登记并指派

七、总结

从上云迁移项目里,我最大的体会是:验收不是最后一步,而应该是最先设计的一步。

我们这次的顺序是反过来的------先写验收脚本(accept_cvm.sh 和比对 SQL),再写部署脚本。这样带来的好处非常实在:

  1. 验收脚本定义了"什么叫成功",部署脚本的实现目标一下就清晰了,不需要反复讨论"要不要加个健康检查"。
  2. 验收脚本是回滚的触发器。部署脚本里的健康检查本质上就是验收脚本的子集,可以共用同一套逻辑,避免"部署时校验通过、验收时不通过"的矛盾。
  3. 验收报告是给业务方看的交付物。运维说的"迁移成功了"没人信,一份 17/17、7 项数据全一致的报告才是。

最后一句:敢不敢回滚,取决于有没有验收。 没有验收体系的迁移,本质上是在赌。


系列文章:

  1. 《传统物理机业务上云:腾讯云助手改写 Shell 脚本,一键生成 CVM 部署与业务校验脚本》
  2. 《从物理机到 CVM 全流程落地:部署脚本的 8 个云化改造点》
  3. 《深度复盘|数据库迁移实战(上):腾讯云助手解析慢查询日志,定位索引缺失与语法不兼容》
  4. 《深度复盘|数据库迁移实战(下):从自建 MySQL 5.7 到腾讯云 MySQL 8.0 的 SQL 兼容改造》
  5. 本篇
相关推荐
hsg772 小时前
简述:人工智能 + 软件实施方案
人工智能
人工智能时代 准备好了吗2 小时前
AI写错品牌一个字,如何判断是名称误差还是认错对象?
人工智能
电子制造自留地2 小时前
AI服务器PCB的超低损耗材料选型逻辑
运维·服务器·人工智能·科技·制造·科普·pcb工艺
张彦峰ZYF2 小时前
从“全量 OCR”到按页智能路由:pdf-inspector 与企业 PDF 解析架构的工程化重构
人工智能·pdf·ocr·ai agent·pdf-inspector
ocean21032 小时前
2025-2026年AI应用开发与Agent面试高频知识点洞察
人工智能·面试·职场和发展
数字新视界3 小时前
U位资产管理系统助力数字化资产监管与提升效率
大数据·人工智能·数据中心·微模块机房·模块化机房
染指11103 小时前
113.Agent-LangChain核心组件-大模型Short-term_memory短期记忆和PostgreSQL记忆存储
人工智能·langchain·agent·agents
砚底藏山河3 小时前
容错重试与指数退避:网络抖动手抖不再丢数据(魔码量化实战 #04)
java·数据库·python·金融
海盗12343 小时前
AI 新闻日报 2026-09-12:Agents API 公测、AI Agent 军团攻击事件、龙猫 2.0 万亿开源
人工智能·开源
仙魁XAN3 小时前
【WorkBuddy·基础入门】第五篇 :模型、权限和工作空间:让 WorkBuddy 做得好,也做得安全
人工智能·安全·workbuddy·workbuddy 基础入门