华为云信创迁移:千万级数据校验与回归测试实战指南
📝 文章摘要:数据库迁移最怕的不是迁移过程中出问题,而是迁移完了才发现数据不对或性能变差。本文分享了我们在 800GB 数据迁移到 openGauss 后使用的三重校验方法(行数比对 + Checksum + 业务逻辑回放),以及性能回归测试的完整流程和工具脚本。如果你正在做数据库迁移,这份验收方案可以直接复用。
⏱ 预计阅读时间:14 分钟
🎯 背景:为什么需要三重校验?
迁移结束后,我们的 QA 团队提出了三个灵魂拷问:
QA: 你怎么证明两边的数据一模一样?
我:行数对过了,一样的。
QA: 行数一样不代表数据一样。同一条记录里某个字段值不同呢?
我:……好问题。
QA: 就算数据一样,性能一样吗?原来 50ms 的接口,可别变成 500ms。
我:……
于是我们设计了一套三重校验 + 性能回归的方案,在双跑期间每天自动跑一遍。
🔍 第一重:行数比对(快速兜底)
最基础的校验,跑得最快,能发现明显的多数据或少数据问题。
#!/bin/bash
# check_row_count.sh — 每天凌晨 3 点执行
DB_HOST_OG="192.168.2.100"
DB_HOST_MYSQL="192.168.1.100"
DB_NAME="order_db"
TABLES="orders order_items users products shipments"
for table in $TABLES; do
mysql_count=$(mysql -h $DB_HOST_MYSQL -u monitor -p -D $DB_NAME \
-e "SELECT COUNT(*) FROM $table" 2>/dev/null | tail -1)
og_count=$(gsql -h $DB_HOST_OG -U monitor -d $DB_NAME \
-t -c "SELECT COUNT(*) FROM $table" 2>/dev/null | tr -d ' ')
if [ "$mysql_count" != "$og_count" ]; then
echo "[ALERT] $table: MySQL=$mysql_count, openGauss=$og_count"
else
echo "[OK] $table: $mysql_count rows"
fi
done

踩坑:COUNT(*) 的性能问题
orders 表有 1.8 亿行,每次 COUNT(*) 跑 20 多秒,在 InnoDB 和 openGauss 上都是全表扫描。我们改为每 10 分钟通过增量同步心跳判断一致,每天只在凌晨低峰期跑一次全量 COUNT。
🔒 第二重:Checksum 校验(数据级精确比对)
行数一样不代表数据一样。我们需要的是一行一行地对比所有字段。
MySQL 端
-- 生成每一行数据的 CRC32 校验和
SELECT table_name,
SUM(CRC32(CONCAT_WS('|',
COALESCE(id, ''),
COALESCE(user_id, ''),
COALESCE(total_amount, 0),
COALESCE(status, ''),
COALESCE(created_at, '')
))) AS checksum
FROM orders;
openGauss 端
openGauss 没有 CRC32 函数,用 MD5 转二进制来代替:
-- openGauss 等效:把每行转成 MD5 再按位累加
SELECT 'orders' AS table_name,
SUM(('x' || MD5(CONCAT_WS('|',
COALESCE(id::text, ''),
COALESCE(user_id::text, ''),
COALESCE(total_amount::text, '0'),
COALESCE(status, ''),
COALESCE(created_at::text, '')
))) ::BIT (32) ::BIGINT) AS checksum
FROM orders;
自动化对比脚本
#!/usr/bin/env python3
"""checksum_compare.py — 每天凌晨 4:00 执行"""
import subprocess
import json
TABLES = ['orders', 'order_items', 'users', 'products', 'shipments']
def get_checksum(db_type, table):
"""获取单表的 checksum 值"""
if db_type == 'mysql':
cmd = f'mysql -h 192.168.1.100 -u monitor -p -D order_db -e "YOUR_SQL_HERE"'
else: # opengauss
cmd = f'gsql -h 192.168.2.100 -U monitor -d order_db -t -c "YOUR_SQL_HERE"'
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return int(result.stdout.strip())
for table in TABLES:
mysql_cs = get_checksum('mysql', table)
og_cs = get_checksum('opengauss', table)
if mysql_cs == og_cs:
print(f"[PASS] {table}: checksum OK")
else:
print(f"[FAIL] {table}: MySQL={mysql_cs}, openGauss={og_cs}")
# 触发详细行级别对比
subprocess.run([f'./compare_rows.sh {table}'], shell=True)
💥 踩坑:浮点数精度差异
MySQL DECIMAL(10,2) 迁移到 openGauss NUMERIC(10,2),逻辑上等价但实际上因为浮点数序列化方式不同,Checksum 对不上。排查了 2 个小时,发现是 CONCAT_WS 对数值类型的 COALESCE 行为不同。
-- MySQL 中 COALESCE(total_amount, 0) 返回 DECIMAL
SELECT CRC32(123456.78);
-- 结果: 2473910856
-- openGauss 中 COALESCE(total_amount, 0) 也返回 NUMERIC
SELECT MD5(123456.78::text); -- 需要显式转 text
修复:所有字段都显式 CAST 成 text 再拼接。
🧪 第三重:业务逻辑回放(最接近真实场景)
Checksum 通过了,但业务侧还是不放心。原因是:同样的数据,不同的查询计划可能跑出不同的结果(尤其是分页、聚合、排序)。
我们用了最笨但最有效的方法:把生产环境的真实请求回放一遍。
回放方案

回放工具核心逻辑
// replay.go — 简化版回放逻辑
package main
import (
"database/sql"
"crypto/md5"
"fmt"
_ "github.com/go-sql-driver/mysql"
_ "gitee.com/opengauss/openGauss-connector-go-pq"
)
type SQLRecord struct {
ID string
SQLText string
Params []interface{}
}
func replayAndCompare() {
mysqlDB, _ := sql.Open("mysql", dsn1)
ogDB, _ := sql.Open("opengauss", dsn2)
rows, _ := mysqlDB.Query("SELECT id, sql_text, params FROM replay_logs LIMIT 1000")
defer rows.Close()
for rows.Next() {
var rec SQLRecord
rows.Scan(&rec.ID, &rec.SQLText, &rec.Params)
// 分别在两个库执行
mysqlResult := executeAndHash(mysqlDB, rec.SQLText, rec.Params...)
ogResult := executeAndHash(ogDB, rec.SQLText, rec.Params...)
if mysqlResult.Hash != ogResult.Hash {
fmt.Printf("[DIFF] SQL ID=%s\n", rec.ID)
fmt.Printf(" MySQL: rows=%d, hash=%x\n", mysqlResult.Rows, mysqlResult.Hash)
fmt.Printf(" openGauss: rows=%d, hash=%x\n", ogResult.Rows, ogResult.Hash)
}
}
}
💥 踩坑:回放顺序导致的数据不一致
回放请求是按时间顺序逐条执行的,但如果某条 INSERT 后紧跟着一条依赖它的 SELECT,在单线程回放下没问题。但我们为了提高速度用了 16 并发回放,时序乱了 —— SELECT 可能比 INSERT 先执行,导致结果对不上。
// 修复:按事务分组 + 保证单事务内串行
type TxGroup struct {
TxID string
Queries []SQLRecord // 事务内的所有 SQL,保证顺序
}
// 不同事务可以并发,同一事务必须串行
📊 性能回归测试
数据校验通过后,还要保证性能不降级。我们用 Sysbench 和 JMeter 做了两套压测。
Sysbench OLTP 基准测试
# MySQL 基线
sysbench --db-driver=mysql \
--mysql-host=192.168.1.100 \
--mysql-db=order_db \
--tables=10 --table-size=10000000 \
--threads=32 --time=300 \
oltp_read_write run
# openGauss 对比
sysbench --db-driver=pgsql \
--pgsql-host=192.168.2.100 \
--pgsql-db=order_db \
--tables=10 --table-size=10000000 \
--threads=32 --time=300 \
oltp_read_write run
业务接口压测(JMeter)
我们选取了 12 个核心接口,用生产环境的请求参数模板做压测:
| 接口 | 场景 | MySQL P99 | openGauss 默认 | openGauss 调优后 | 结论 |
|---|---|---|---|---|---|
| 订单列表(分页) | 高频读 | 32ms | 45ms ↑ 41% | 28ms ↓ 13% | 调优后通过 |
| 创建订单(含事务) | 高频写 | 48ms | 62ms ↑ 29% | 41ms ↓ 15% | 调优后通过 |
| 订单详情(主键查) | 高频读 | 8ms | 9ms | 7ms | ✅ 通过 |
| 月度对账(聚合) | 低频批 | 4.2s | 3.1s ↓ 26% | 2.8s ↓ 33% | ✅ 通过 |
| 用户订单统计 | 分析型 | 1.8s | 1.2s ↓ 33% | 0.9s ↓ 50% | ✅ 通过 |
💥 踩坑:默认参数下写入性能倒挂
第一次压测结果让我们很紧张 —— 创建订单接口 P99 从 MySQL 的 48ms 变成了 openGauss 的 62ms。原因是 openGauss 默认 fsync 配置比较保守,且 shared_buffers 只有 256MB。
# postgresql.conf 调优(鲲鹏服务器)
shared_buffers = 8GB # 默认 32MB → 物理内存 25%
wal_buffers = 64MB # 默认 512KB → 大事务写入优化
checkpoint_timeout = 15min # 默认 5min → 减少 checkpoint 频率
max_wal_size = 64GB # 默认 1GB → 避免频繁 checkpoint
commit_delay = 100 # 默认 0 → 批量提交微优化
调完再跑,P99 降到了 41ms,比 MySQL 还快 15%。
📋 验收报告模板
每次校验完成后,自动生成以下报告:
# 数据校验报告 — 2025-03-18
## 一、行数比对
| 表名 | MySQL | openGauss | 结果 |
|:----|:-----:|:---------:|:----:|
| orders | 182,456,789 | 182,456,789 | ✅ |
| order_items | 687,234,567 | 687,234,567 | ✅ |
| users | 5,678,123 | 5,678,123 | ✅ |
## 二、Checksum 比对
| 表名 | MySQL Checksum | openGauss Checksum | 结果 |
|:----|:--------------:|:------------------:|:----:|
| orders | 0x8A3F2B1C | 0x8A3F2B1C | ✅ |
| order_items | 0x7D1E4A3B | 0x7D1E4A3B | ✅ |
## 三、业务回放
| 批次 | SQL 总数 | 差异数 | 结果 |
|:----|:--------:|:------:|:----:|
| Batch-001 | 10,000 | 0 | ✅ |
| Batch-002 | 10,000 | 0 | ✅ |
## 四、性能回归
| 接口 | MySQL P99 | openGauss P99 | 结论 |
|:----|:---------:|:-------------:|:----:|
| 订单列表 | 32ms | 28ms | ✅ 通过 |
| 创建订单 | 48ms | 41ms | ✅ 通过 |
## 结论:✅ 全部通过,可进行流量切换
❓ 常见问题
Q1:Checksum 对比和行数对比哪个重要?
Checksum 更重要。行数一样但数据不一样的情况我们遇到过:增量同步过程中某条记录的部分字段没有更新到。
Q2:业务回放需要保留生产数据多久?
建议保留 至少 24 小时的完整访问日志。我们保留 7 天滚动,双跑期间每天回放前一天的流量。
Q3:性能回归跑多长时间比较合理?
写场景 30 分钟以上(覆盖 checkpoint 触发周期),读场景 15 分钟(缓存预热后稳定即可)。
Q4:两边的压测结果相差多大算不通过?
我们的标准:核心接口 P99 偏差超过 20% 视为不通过,需要调优后再跑一轮。非核心接口放宽到 30%。
📝 总结
数据库迁移验收这件事,信别人不如信脚本。三重校验方案虽然搭建起来要花 2-3 天,但后续每天自动跑,双跑期间帮我们抓住了 7 处数据不一致问题,避免了上线后的事故。
如果只选一件事做,我建议优先做 Checksum 比对 —— 它是性价比最高的验收手段。如果时间允许,再加一套业务回放,基本就稳了。
💬 互动:你们团队做数据库迁移验收时,用的是什么方案?有没有遇到过数据都对了但业务就是跑不通的情况?
- 点赞
- 收藏
- 关注作者
评论(0)