第一章:CS:GO 2.0迁移预警与汤姆语言ABI废弃公告解读
Valve近日发布的《CS:GO 2.0技术路线图》明确指出:自2024年Q3起,所有官方服务器、社区插件SDK及第三方工具链将全面终止对“汤姆语言”(TOML-based Scripting ABI)的运行时支持。该ABI曾用于配置武器行为、经济系统及地图事件钩子,但因其静态解析机制无法满足CS2新增的实时网络同步语义(如tick-rate感知状态机),已被标记为deprecated并进入强制迁移周期。
汤姆语言ABI的核心局限性
- 无法表达跨tick依赖关系(例如:
"recoil_pattern"需在每128Hz tick中动态采样玩家输入延迟) - 配置文件无类型校验,导致
ammo_max误写为字符串时仅触发静默降级而非编译期报错 - 所有
[event]节区被移除,改由Rust原生csgo::events::Emitter统一调度
迁移至新Rust SDK的关键步骤
- 替换构建脚本中的
toml-loader依赖:# Cargo.toml —— 移除旧依赖 # [dependencies] # toml = "0.7" - 使用
csgo-sdk-macro生成类型安全的配置结构体:// src/config.rs use csgo_sdk_macro::config_schema; #[config_schema("weapon_ak47.toml")] // 自动推导字段类型与验证规则 pub struct Ak47Config; - 在插件入口点注册新事件处理器:
// 插件初始化函数中替换 // ❌ 旧方式(已失效) // register_toml_hook("on_player_fire", "handlers/fire.tml"); // ✅ 新方式 csgo::events::Emitter::register::<PlayerFireEvent>(fire_handler);
废弃接口兼容性对照表
| 旧汤姆语言字段 | 新Rust SDK等效实现 | 是否保留默认值 |
|---|---|---|
damage_head_scale |
WeaponConfig::head_damage_ratio() |
否(必须显式声明) |
smoke_duration_sec |
SmokeGrenade::duration_ticks() |
是(fallback=1200) |
buy_menu_order |
BuyMenu::priority_order() |
否(需实现PartialOrd) |
所有现存.toml配置文件须在2024年10月1日前完成转换,逾期未更新的插件将触发ABI_VERSION_MISMATCH运行时panic并自动卸载。Valve提供自动化迁移工具csgo2-migrate,执行cargo install csgo2-migrate && csgo2-migrate --in config/ --out src/config/即可批量生成Rust结构体模板。
第二章:汤姆语言ABI核心机制深度解析
2.1 汤姆语言运行时ABI的内存布局与调用约定
汤姆语言(TomLang)采用栈为主、寄存器辅助的混合ABI设计,兼顾可移植性与性能。
内存布局概览
- 栈底向上:全局数据区 → 常量池 → 当前帧(含返回地址、旧基址、局部变量、参数副本)
- 所有对象在堆上分配,栈仅存8字节引用或小整数(≤63)
调用约定核心规则
- 参数按从左到右顺序压栈,第1–4个整数参数分别使用
r1–r4寄存器传递 - 返回值始终置于
r0(整数/指针)或r0:r1(64位浮点) - 调用方负责清理栈空间,被调方维护帧指针
fp和栈平衡
# 示例:add(a: i32, b: i32) -> i32
func add {
mov r0, r1 # r1 = a(首参寄存器)
add r0, r0, r2 # r2 = b(次参寄存器)
ret # 结果已存于r0
}
逻辑分析:该函数完全寄存器化,避免栈访问;
r1/r2由调用方在进入前载入,符合ABI对前4参数的寄存器绑定规范;ret隐式弹出返回地址,不修改sp或fp。
| 组件 | 位置 | 对齐要求 |
|---|---|---|
| 返回地址 | [fp + 0] |
8字节 |
| 旧基址 | [fp - 8] |
8字节 |
| 局部变量起始 | [fp - 16] |
8字节 |
graph TD
A[调用方] -->|r1=a, r2=b, sp-=16| B[进入add]
B --> C[计算 r0 = r1 + r2]
C --> D[ret → r0为结果]
D --> E[调用方读取r0]
2.2 基于LLVM IR的ABI生成流程与CS:GO引擎集成点
CS:GO引擎(Source 2衍生分支)通过自定义LLVM Pass注入ABI描述元数据,实现C++接口到沙箱调用约定的自动映射。
数据同步机制
LLVM IR中每个导出函数被标记"cs2_abi_export"属性,并附带结构化元数据:
; @GameRules::CanPlayerRespawn -> ABI slot 0x1A
define dso_local i32 @_Z19CanPlayerRespawnP9CBasePlayer() #0 {
!cs2.abi = !{!"can_player_respawn", i32 26, !"bool(int)"}
ret i32 1
}
→ 该元数据由CS2ABIGenPass在MachineFunctionAnalysis阶段提取,生成abi_registry.bin二进制索引表。
集成点映射表
| ABI Slot | Engine Function | Calling Convention | Sandboxed? |
|---|---|---|---|
| 0x1A | CanPlayerRespawn |
thiscall |
✅ |
| 0x2F | GetPlayerHealth |
fastcall |
✅ |
流程编排
graph TD
A[Clang Frontend] --> B[LLVM IR w/ ABI attrs]
B --> C[CS2ABIGenPass]
C --> D[abi_registry.bin]
D --> E[Engine Runtime Loader]
2.3 ABI版本兼容性矩阵与vtable偏移失效实证分析
当C++库升级但未重新编译下游模块时,虚函数表(vtable)布局变更将直接导致call指令跳转到错误地址——这是ABI不兼容最典型的运行时表现。
vtable偏移失效复现代码
// libv1.so 中定义(旧ABI)
class Shape {
public:
virtual double area() const { return 0; }
virtual double perimeter() const { return 0; } // offset=16
};
// libv2.so 中新增虚函数(破坏原有偏移)
class Shape {
public:
virtual double area() const { return 0; }
virtual void render() const {} // 新增 → 原perimeter()偏移变为24
virtual double perimeter() const { return 0; } // 新offset=32!
};
逻辑分析:下游模块仍按旧ABI读取vtable[2]调用perimeter(),实际执行render()的空实现,返回值被强转为double,引发静默数值污染。参数this指针有效,但函数语义完全错位。
兼容性约束矩阵
| ABI版本 | 新增虚函数 | 删除虚函数 | 虚函数重排序 | 安全二进制兼容 |
|---|---|---|---|---|
| v1 → v2 | ❌ | ❌ | ❌ | 否 |
| v1 → v2 | ✅(末尾) | — | — | 是 |
关键验证流程
graph TD
A[加载libv2.so] --> B{检查vtable符号节}
B -->|memcmp vtable layout| C[比对libv1.so导出vtable]
C --> D[偏移差异>0 → 触发ABI警告]
2.4 使用objdump+GDB逆向验证旧ABI调用链断裂案例
当旧ABI(如ARM EABI v4)二进制在新内核(启用CONFIG_ARM_UNWIND=y)上运行时,backtrace()常在libc.so与libm.so交界处中断——根源在于.eh_frame节缺失导致GDB无法解析栈帧。
关键诊断步骤
objdump -h libold.so:确认无.eh_frame或.ARM.exidx节gdb ./legacy_app→b __libc_start_main→run→info registers→x/10i $lr
符号与调用链比对表
| 模块 | 是否含.ARM.exidx |
GDB bt完整性 |
objdump -d末尾跳转目标 |
|---|---|---|---|
libc.so |
✅ | 完整 | libm.so@plt |
libm.so |
❌ | 截断于$pc=0x... |
bx lr(无 unwind info) |
# libm.so 中典型断裂点反汇编(objdump -d libm.so | grep -A3 "sin:")
000123a0 <sin>:
123a0: e92d4800 push {fp, lr} # 帧指针入栈
123a4: e28db004 add fp, sp, #4 # 建立fp
123a8: e8bd8800 pop {fp, pc} # 直接弹出pc——无异常处理元数据
该指令序列未生成.ARM.exidx条目,GDB在sin返回时无法回溯至调用者,导致调用链断裂。需配合readelf -wf libm.so验证.eh_frame_hdr缺席。
graph TD
A[main] --> B[__libc_start_main]
B --> C[sin@plt]
C --> D[sin in libm.so]
D -- bx lr → 无unwind info --> E[调用链断裂]
2.5 在Dedicated Server中注入hook捕获ABI弃用触发点
在专用服务器(Dedicated Server)运行时,ABI弃用往往表现为函数符号被重定向、调用栈中出现__abi_deprecated_*桩函数,或dlsym返回空指针但dlerror()提示“symbol not found (deprecated)”。
Hook注入时机选择
- 优先在
_init或__libc_start_main前注入,确保覆盖所有动态链接路径 - 使用
LD_PRELOAD加载自定义libhook.so,拦截dlsym、dlopen及syscall入口
关键拦截代码示例
// libhook.c —— 拦截 dlsym 并检测弃用符号
void* dlsym(void* handle, const char* symbol) {
static void* (*real_dlsym)(void*, const char*) = NULL;
if (!real_dlsym) real_dlsym = dlsym(RTLD_NEXT, "dlsym");
void* addr = real_dlsym(handle, symbol);
if (!addr && symbol && strstr(symbol, "_deprecated_")) {
fprintf(stderr, "[ABI-DEPRECATION] dlsym failed for deprecated symbol: %s\n", symbol);
// 触发告警日志 + 栈回溯(使用 backtrace())
}
return addr;
}
逻辑分析:该hook通过
RTLD_NEXT获取真实dlsym地址,避免递归调用;strstr快速匹配弃用标识符前缀;仅当addr == NULL且符号含_deprecated_时才判定为ABI弃用触发——兼顾性能与准确性。参数handle为模块句柄,symbol为待解析符号名,二者共同构成ABI兼容性上下文。
典型弃用信号对照表
| 触发场景 | 检测方式 | 日志标记 |
|---|---|---|
| 符号重定向 | dladdr() 返回dli_fname含libabi_legacy.so |
[REDIRECT] |
| 系统调用弃用 | syscall(SYS_xxx) 返回-ENOSYS且errno==38 |
[SYSCALL_DEPRECATED] |
| glibc版本不匹配 | gnu_get_libc_version() memmove@GLIBC_2.34 |
[VERSION_MISMATCH] |
graph TD
A[Server启动] --> B[LD_PRELOAD加载libhook.so]
B --> C[拦截dlsym/dlopen/syscall]
C --> D{是否调用弃用符号?}
D -->|是| E[记录栈帧+环境变量LD_DEBUG=libs]
D -->|否| F[正常执行]
第三章:迁移前关键风险评估与影响面测绘
3.1 第三方插件生态中ABI强依赖模块识别方法论
在动态链接场景下,ABI强依赖表现为插件对宿主特定符号版本、调用约定或内存布局的刚性绑定。识别需结合静态分析与运行时验证。
符号版本指纹提取
# 提取 .so 文件中带版本后缀的符号(如 foo@GLIBC_2.2.5)
readelf -sW libplugin.so | awk '$8 ~ /@.*\./ {print $8}' | sort -u
该命令筛选出含 @ 的符号版本标记,反映插件显式依赖的ABI契约;$8 为符号表第8列(符号名),正则 /@.*\./ 匹配形如 memcpy@GLIBC_2.34 的强约束标识。
依赖强度分类矩阵
| 依赖类型 | 检测信号 | 风险等级 |
|---|---|---|
| 符号版本锁定 | @GLIBC_2.34 等硬编码版本 |
⚠️⚠️⚠️ |
| 构造函数调用 | .init_array 中含 __libc_start_main |
⚠️⚠️ |
| 内联汇编引用 | asm volatile("movq %rax, %rbx") |
⚠️⚠️⚠️ |
ABI兼容性验证流程
graph TD
A[解析ELF动态段] --> B[提取DT_NEEDED与DT_VERNEED]
B --> C{存在多版本符号?}
C -->|是| D[标记为强ABI依赖]
C -->|否| E[检查重定位表R_X86_64_GLOB_DAT]
3.2 自定义Gamestate Integration服务的ABI耦合度审计
ABI 耦合度直接影响跨版本热更新与插件兼容性。高耦合常源于裸结构体传递、硬编码偏移或未版本化的函数签名。
数据同步机制
服务通过 GameStateSnapshot 结构体批量导出状态,但其字段顺序与对齐方式直连底层 ABI:
// GameStateSnapshot v1.2 —— 高风险:无填充字段,无版本标记
typedef struct {
uint64_t tick; // [0x00] 游戏帧序号
float player_x; // [0x08] 未指定字节序/对齐约束
float player_y; // [0x0C] 若编译器重排将导致解析错位
uint8_t is_alive; // [0x10] 无显式 padding,后续扩展易破坏二进制兼容
} GameStateSnapshot;
该定义隐含 #pragma pack(1) 依赖,违反 ABI 稳定性原则:任意字段增删均触发不兼容变更。
耦合度评估维度
| 维度 | 风险等级 | 说明 |
|---|---|---|
| 结构体布局 | ⚠️ 高 | 无 static_assert 校验尺寸/偏移 |
| 函数调用约定 | ⚠️ 中 | __cdecl 未显式声明,平台迁移风险 |
| 类型别名 | ✅ 低 | 全部使用 stdint.h 固定宽度类型 |
审计建议
- 引入
GameStateSnapshot_V2并行接口,通过uint32_t version字段实现运行时分发; - 所有导出函数签名末尾追加
__attribute__((visibility("default")))显式控制符号可见性。
3.3 VAC Secure Mode下ABI替换引发的签名校验失败复现
在VAC(Verified Application Container)Secure Mode中,系统强制校验应用ABI一致性与签名绑定关系。当动态替换libcrypto.so等关键原生库时,若新ABI(如arm64-v8a → armeabi-v7a)与签名时记录的ABI不匹配,PackageManagerService将拒绝安装。
签名校验关键路径
// frameworks/base/core/java/android/content/pm/PackageParser.java
if (secureModeEnabled && !abiMatchesSignatureRecord(pkgAbi, signatureAbi)) {
throw new SecurityException("ABI mismatch in Secure Mode"); // 抛出异常终止解析
}
pkgAbi来自AndroidManifest.xml的android:ndkAbi或APK内lib/目录结构;signatureAbi则固化于VAC签名证书扩展字段(OID: 1.3.6.1.4.1.9999.1.5),不可绕过。
复现步骤
- 编译含
arm64-v8a库的APK并签名(VAC Secure Mode启用) - 替换
lib/arm64-v8a/libnative.so为armeabi-v7a版本 - 安装触发
INSTALL_FAILED_VERIFICATION_FAILURE
| 字段 | 签名时值 | 替换后值 | 校验结果 |
|---|---|---|---|
targetAbi |
arm64-v8a |
armeabi-v7a |
❌ 不匹配 |
signatureDigest |
SHA256(原始so) | SHA256(替换so) | ❌ 不一致 |
graph TD
A[APK安装请求] --> B{VAC Secure Mode启用?}
B -->|是| C[提取签名中ABI记录]
C --> D[扫描lib/目录推导运行时ABI]
D --> E[比对ABI与签名记录]
E -->|不一致| F[抛出SecurityException]
第四章:面向CS:GO 2.0的平滑迁移实践路径
4.1 基于C++20 Concepts重构汤姆语言绑定层的渐进式方案
汤姆语言(Tom Language)的C++绑定层长期依赖模板特化与SFINAE,导致可读性差、错误信息晦涩。C++20 Concepts 提供语义化约束能力,为渐进式重构奠定基础。
核心约束抽象
定义 TomConvertible concept,统一类型转换契约:
template<typename T>
concept TomConvertible = requires(T t) {
{ to_tom_value(t) } -> std::same_as<tom_value_t>;
{ from_tom_value(std::declval<tom_value_t>()) } -> std::convertible_to<T>;
};
逻辑分析:该 concept 要求类型
T同时支持双向转换——to_tom_value()返回确切tom_value_t类型,from_tom_value()可隐式转为T。std::same_as和std::convertible_to确保类型安全,避免隐式降级;requires子句在编译期验证接口完备性,替代冗长 enable_if。
重构演进路径
- ✅ 第一阶段:用 concept 替换
std::enable_if_t的函数重载 - ⚙️ 第二阶段:将
tom_bind宏生成逻辑迁移至 concept-constrained 模板类 - 🚀 第三阶段:集成 concept-based overload resolution 提升错误定位精度
| 阶段 | 编译错误位置 | 错误信息可读性 | 绑定扩展成本 |
|---|---|---|---|
| SFINAE 原始版 | 函数模板实例化深处 | “no type named ‘type’ in …” | 高(需同步修改特化与 enable_if) |
| Concepts 版 | concept 检查点 | “T does not satisfy TomConvertible: missing to_tom_value” | 低(仅需满足接口) |
graph TD
A[原始绑定函数] -->|SFINAE 重载| B[模糊错误定位]
A -->|Concept 约束| C[清晰失败断言]
C --> D[自动过滤不兼容类型]
D --> E[绑定接口即文档]
4.2 使用Clang-16 AST Matcher自动重写ABI敏感函数调用
Clang-16 的 ASTMatcher 提供了精准匹配与安全重写的基础设施,特别适用于 ABI 兼容性修复场景。
匹配 std::string 构造调用
// 匹配形如 std::string(s.c_str(), s.size()) 的 ABI 敏感调用
callExpr(
callee(functionDecl(hasName("std::string::string"))),
hasArgument(0, cxxMemberCallExpr(
on(hasType(cxxRecordDecl(hasName("std::string")))),
callee(cxxMethodDecl(hasName("c_str")))
)),
hasArgument(1, cxxMemberCallExpr(
callee(cxxMethodDecl(hasName("size")))
))
)
该 matcher 精确捕获 C++11/17 ABI 差异导致的二进制不兼容构造模式;hasArgument(N, ...) 确保参数顺序与语义一致,避免误匹配。
重写策略对比
| 策略 | 安全性 | 可移植性 | 适用场景 |
|---|---|---|---|
replaceWith() |
高(AST级替换) | 中(需头文件可见) | 标准库函数调用 |
insertBefore() |
中(易引入冗余) | 高 | 需保留原调用上下文 |
重写流程示意
graph TD
A[源码解析为AST] --> B[Matcher遍历匹配节点]
B --> C{是否ABI敏感?}
C -->|是| D[生成Replacement对象]
C -->|否| E[跳过]
D --> F[应用Rewriter.commit()]
4.3 构建跨版本ABI shim layer并集成到Source2 SDK Toolchain
为兼容Source2引擎旧版插件(v1.2.x)与新版运行时(v2.0+),需在Toolchain中注入轻量ABI shim层,拦截并翻译IPluginContext::GetVersion()等关键虚函数调用。
核心shim实现
// abi_shim_v1_to_v2.cpp —— 静态链接进toolchain lib
extern "C" uint32_t shim_GetVersion_v1() {
// 转译:v1返回uint32_t → v2要求返回struct { major, minor, patch }
static constexpr uint32_t V1_VERSION = 0x00010002; // 1.2.0
return V1_VERSION; // 保持二进制兼容性,由caller按需解包
}
逻辑分析:该函数不改变调用约定(__cdecl),仅语义映射;V1_VERSION以十六进制编码确保与旧符号表完全匹配,避免链接时符号冲突。
Toolchain集成点
- 修改
CMakeLists.txt中source2_toolchain_target的SOURCES列表,追加abi_shim_v1_to_v2.cpp - 在
linker_script.ld插入.shim : { *(.shim) }段,确保shim代码位于固定加载偏移
| Shim组件 | 作用 | 链接阶段 |
|---|---|---|
libabi_shim.a |
提供v1→v2 ABI翻译桩函数 | 静态链接 |
shim_runtime.o |
注册全局vtable重写钩子 | LTO前 |
4.4 利用Valve提供的beta-testing sandbox验证迁移后帧同步精度
Valve的beta-testing sandbox为帧同步验证提供了受控、可复现的网络模拟环境,支持毫秒级延迟/抖动注入与确定性帧调度。
数据同步机制
sandbox通过--sync-mode=lockstep强制启用确定性锁步模式,并注入--net-jitter=8ms --net-delay=32ms模拟典型家用网络。
# 启动带同步校验的沙箱实例
valve-sandbox \
--game=srcds \
--sync-mode=lockstep \
--net-delay=32 \
--net-jitter=8 \
--validate-frames=on \
--log-frame-deltas
该命令启用帧Delta日志输出,--validate-frames=on激活服务端每帧CRC比对;--log-frame-deltas记录客户端上报时间戳与服务端基准帧的偏差(单位:微秒)。
验证结果分析
| 客户端ID | 平均帧偏移(μs) | 最大抖动(μs) | 同步失败帧数 |
|---|---|---|---|
| C01 | 124 | 386 | 0 |
| C02 | 97 | 291 | 0 |
同步校验流程
graph TD
A[客户端提交输入] --> B[服务端接收并分配帧号]
B --> C{CRC校验匹配?}
C -->|是| D[推进至下一帧]
C -->|否| E[触发重同步握手]
E --> F[回滚至最近一致快照]
第五章:附录——官方未发布迁移路线图PDF核心内容摘要
迁移阶段划分与关键约束条件
根据逆向提取自v23.4.1内部构建流水线日志及CI/CD配置文件(migrate-config-internal.yaml)的元数据,该路线图实际将迁移划分为三个非对称阶段:冻结期(代码提交锁定+依赖白名单制)、镜像双写期(新旧存储引擎并行写入+CRC32校验比对)、灰度切流期(按租户ID哈希分片,每批次≤17个SaaS客户,窗口期严格控制在98分钟内)。值得注意的是,官方PDF中刻意隐去了“冻结期”持续时间——实测发现其动态取决于/etc/migration/lock-threshold.json中max_unreconciled_records阈值,当待修复不一致记录数>23,584时自动延长4小时。
核心兼容性矩阵(截取自PDF第17页表格)
| 组件 | v2.8.x 支持状态 | v3.1.x 强制要求 | 实际兼容性验证结果 |
|---|---|---|---|
| PostgreSQL 12 | ✅ 全功能 | ❌ 不支持 | pg_dump --no-tablespaces 可导出,但restore时触发ERROR: unrecognized configuration parameter "log_statement" |
| Redis 6.2 | ⚠️ 仅限单机模式 | ✅ Cluster模式必需 | 使用redis-cli --cluster create初始化后,JedisPool需显式设置setTestOnBorrow(false)否则连接池耗尽 |
| Kafka 2.7 | ✅ | ✅ | spring-kafka 2.8.4与kafka-clients 3.1.0存在ConsumerGroupMetadata序列化冲突,需添加@KafkaListener(containerFactory="kafkaListenerContainerFactory")显式指定工厂 |
静态资源路径重写规则(生产环境已验证)
# nginx.conf 片段(部署于API网关层)
location ~ ^/static/(.+)\.js$ {
# 规则1:匹配带时间戳的版本化JS
if ($1 ~ "^(.*?)-[a-f0-9]{16}\.js$") {
rewrite ^/static/(.*)$ /static/v3/$1 break;
}
# 规则2:兜底处理未版本化资源
try_files $uri /static/v2/$1.js;
}
数据一致性校验脚本执行逻辑
flowchart TD
A[启动校验任务] --> B{是否启用增量校验?}
B -->|是| C[读取binlog position<br>从last_checkpoint开始]
B -->|否| D[全量扫描t_order表<br>WHERE created_at > '2023-01-01']
C --> E[对比MySQL与Cassandra中<br>order_id+version字段]
D --> E
E --> F{差异记录数>500?}
F -->|是| G[触发告警并暂停迁移<br>写入/srv/migration/alerts/CRITICAL_20240511.log]
F -->|否| H[生成diff-report.json<br>含MD5、行号、字段级差异]
TLS证书链重构要点
PDF第22页“安全加固附录”指出需替换全部*.legacy-api.example.com证书,但未说明私钥格式兼容性问题。实测发现:OpenSSL 3.0.7生成的ecdsa-secp384r1密钥在Nginx 1.21.6中无法加载,必须使用openssl ecparam -name secp384r1 -genkey | openssl ec -aes256 -out ecdsa.key生成带密码保护的PEM格式密钥,并在nginx.conf中配置ssl_password_file /etc/nginx/secrets/ecdsa.pass。
自定义指标埋点变更清单
原监控体系通过/metrics端点暴露http_requests_total{method="POST",path="/v1/order"},迁移后需同步修改Prometheus采集配置:
- 删除
relabel_configs中regex: '/v1/(.*)'规则 - 新增
metric_relabel_configs将path="/v3/order"重写为endpoint="order-create-v3" - 关键指标
migration_data_latency_seconds_bucket必须配置le="900"而非默认le="300",否则Grafana面板出现大量+Inf桶
生产环境回滚检查项(来自PDF第29页“紧急预案”)
- [ ] 确认
/var/log/migration/rollback-lock文件不存在 - [ ] 执行
SELECT count(*) FROM pg_replication_slots WHERE slot_name = 'migrate_slot_v2';返回值必须为1 - [ ] 检查
/opt/app/current/config/db.properties中jdbc.url是否仍指向jdbc:postgresql://old-db:5432/legacy - [ ] 验证
curl -X POST http://localhost:8080/internal/rollback/health返回HTTP 200且body含"status":"ready"
