华为云信创迁移:千万级数据校验与回归测试实战指南

举报
行者·全栈架构师 发表于 2026/06/13 23:08:48 2026/06/13
【摘要】 数据库迁移最怕的不是迁移过程中出问题,而是迁移完了才发现数据不对或性能变差。本文分享了我们在 800GB 数据迁移到 openGauss 后使用的三重校验方法(行数比对 + Checksum + 业务逻辑回放),以及性能回归测试的完整流程和工具脚本。如果你正在做数据库迁移,这份验收方案可以直接复用。

📝 文章摘要:数据库迁移最怕的不是迁移过程中出问题,而是迁移完了才发现数据不对或性能变差。本文分享了我们在 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

HWG-003-migration-validation-and-regression_diagram_1.png

踩坑: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 通过了,但业务侧还是不放心。原因是:同样的数据,不同的查询计划可能跑出不同的结果(尤其是分页、聚合、排序)。

我们用了最笨但最有效的方法:把生产环境的真实请求回放一遍

回放方案

HWG-003-migration-validation-and-regression_diagram_2.png

回放工具核心逻辑

// 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 比对 —— 它是性价比最高的验收手段。如果时间允许,再加一套业务回放,基本就稳了。


💬 互动:你们团队做数据库迁移验收时,用的是什么方案?有没有遇到过数据都对了但业务就是跑不通的情况?

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。