迁移复杂度评估
| 场景 | 复杂度 | 主要工作 | 典型周期 |
|---|---|---|---|
| MySQL → RDS MySQL | 低 | DMS 全量 + CDC,参数对齐 | 1-2 周 |
| PostgreSQL → RDS PostgreSQL | 低 | 同上,注意扩展兼容性 | 1-2 周 |
| SQL Server → RDS SQL Server | 低 | 备份还原或 DMS,许可确认 | 1-3 周 |
| Oracle → RDS Oracle | 中 | BYOL 许可、参数与特性对齐 | 2-4 周 |
| SQL Server → PostgreSQL | 高 | T-SQL 转 PL/pgSQL、类型映射、应用改造 | 8-16 周 |
| Oracle → PostgreSQL | 很高 | PL/SQL 转换、包与触发器重写、性能回归 | 12-24 周 |
异构迁移的周期主要由存储过程数量和业务逻辑复杂度决定。SCT 能自动转换 60%-80%,剩下的必须人工处理。
异构迁移六阶段
-
01
评估与转换分析
用 SCT 扫描源库,生成转换报告,统计自动转换率与需人工处理的对象清单。
交付物:SCT 评估报告、人工改造工作量清单、风险项列表
1-2 周
-
02
Schema 转换
自动转换 + 人工修正表结构、索引、约束、序列。注意数据类型精度差异(如 NUMBER 到 NUMERIC)。
交付物:目标 Schema DDL、类型映射表、差异说明文档
2-4 周
-
03
代码改写
存储过程、函数、触发器逐个转换。这是最耗时的部分,需要开发深度参与。
交付物:转换后的 PL/pgSQL 代码、单元测试用例
4-12 周
-
04
应用适配
改数据库驱动、连接串、方言相关的 SQL、分页语法、序列获取方式。
交付物:应用改造代码、配置变更清单
2-6 周
-
05
数据迁移与校验
DMS 全量 + CDC 增量。校验分三层:行数、校验和、业务关键字段抽样比对。
交付物:数据一致性报告、校验脚本、差异处理记录
2-4 周
-
06
性能回归与切换
对比关键 SQL 的执行计划与耗时,补索引、调参数,达标后按演练流程切换。
交付物:性能对比报告、优化记录、切换 Runbook
2-4 周
数据一致性校验脚本
"""迁移数据一致性校验:行数 → 校验和 → 抽样比对,三层逐步收紧。"""
from __future__ import annotations
import hashlib
from dataclasses import dataclass, field
from typing import Any, Sequence
@dataclass
class TableCheck:
table: str
key_column: str
compare_columns: Sequence[str]
source_rows: int = 0
target_rows: int = 0
checksum_match: bool = False
sample_diffs: list[dict[str, Any]] = field(default_factory=list)
@property
def passed(self) -> bool:
return (
self.source_rows == self.target_rows
and self.checksum_match
and not self.sample_diffs
)
def count_rows(conn, table: str) -> int:
with conn.cursor() as cur:
cur.execute(f"SELECT COUNT(*) FROM {table}")
return cur.fetchone()[0]
def table_checksum(conn, table: str, columns: Sequence[str], key: str) -> str:
"""按主键排序后计算整表内容的 MD5,用于快速判断是否完全一致。
注意:跨引擎的类型格式化差异会导致校验和不同,
所以要统一转成文本并处理 NULL 与数值精度。
"""
col_expr = ", ".join(f"COALESCE(CAST({c} AS CHAR), '~NULL~')" for c in columns)
with conn.cursor() as cur:
cur.execute(f"SELECT {col_expr} FROM {table} ORDER BY {key}")
h = hashlib.md5()
for row in cur:
h.update("|".join(row).encode("utf-8"))
return h.hexdigest()
def sample_compare(
src_conn, tgt_conn, table: str, key: str,
columns: Sequence[str], sample_size: int = 1000,
) -> list[dict[str, Any]]:
"""随机抽样逐行比对,捕捉校验和无法暴露的精度类问题。"""
col_list = ", ".join(columns)
with src_conn.cursor() as cur:
cur.execute(
f"SELECT {key}, {col_list} FROM {table} "
f"ORDER BY RAND() LIMIT {sample_size}"
)
src_rows = {r[0]: r[1:] for r in cur.fetchall()}
if not src_rows:
return []
keys = ", ".join(repr(k) for k in src_rows)
with tgt_conn.cursor() as cur:
cur.execute(
f"SELECT {key}, {col_list} FROM {table} WHERE {key} IN ({keys})"
)
tgt_rows = {r[0]: r[1:] for r in cur.fetchall()}
diffs: list[dict[str, Any]] = []
for k, src_val in src_rows.items():
tgt_val = tgt_rows.get(k)
if tgt_val is None:
diffs.append({"key": k, "issue": "目标端缺失"})
elif tuple(map(str, src_val)) != tuple(map(str, tgt_val)):
diffs.append({
"key": k, "issue": "值不一致",
"source": src_val, "target": tgt_val,
})
return diffs
def run_checks(src_conn, tgt_conn, checks: list[TableCheck]) -> tuple[bool, list[TableCheck]]:
for chk in checks:
chk.source_rows = count_rows(src_conn, chk.table)
chk.target_rows = count_rows(tgt_conn, chk.table)
if chk.source_rows != chk.target_rows:
continue # 行数就不对,没必要往下算
src_sum = table_checksum(src_conn, chk.table, chk.compare_columns, chk.key_column)
tgt_sum = table_checksum(tgt_conn, chk.table, chk.compare_columns, chk.key_column)
chk.checksum_match = src_sum == tgt_sum
if not chk.checksum_match:
chk.sample_diffs = sample_compare(
src_conn, tgt_conn, chk.table, chk.key_column, chk.compare_columns
)
all_passed = all(c.passed for c in checks)
return all_passed, checks
校验必须在切换窗口内跑完,所以大表要提前测好耗时。超过 20 分钟的校验建议改成分区并行。