重构老系统前,我先把那些看不见的依赖画成了地图

重构老系统前,我先把那些看不见的依赖画成了地图

2026 年春天,我们决定对 FulfillCore 动手。它是一家区域零售企业的订单履约核心系统,承担下单、锁库、拆单、配送分配和门店结算,已经连续运行十二年。代码是 Java 8 单体,数据库是 Oracle,外围还有六十多个 cron 作业、一批 shell 脚本、Windows 计划任务、SFTP 文件交换和大量 Excel 手工环节。业务不能停,重构又不能只靠勇气,所以我们先做了一件看起来“不产出功能”的事:把系统真实存在的依赖画成一张地图。

背景与问题:代码只是冰山露出水面的部分

立项时,团队拿到的架构图只有三层:Web 应用、应用服务、数据库。图上干净得像样板间。但值班表不会骗人:每次发版,总有人问“那个跑批会不会受影响”;每次改字段,总有人提醒“门店结算文件可能对不上”;每次数据库慢,追到最后发现是某个没人认领的 shell 脚本在凌晨全表扫描。

我们做过一次粗略统计:仓库里的代码和配置只能解释约四成运行时行为。剩下的六成,散落在服务器 crontab、运维手工脚本、数据库存储过程、共享表、文件目录、邮件审批和几位老员工的记忆里。它们是“看不见的依赖”——不写进架构图,却真实决定一次重构会不会把生产环境掀翻。

更麻烦的是,这些依赖不是均匀分布的。订单主流程相对清楚,真正危险的是边缘链路:门店日结、逆向退款、承运商对账、库存同步。它们调用频率低,出问题却直接变成财务差异。于是我们定下原则:先画依赖地图,再谈拆分;先找接缝,再动代码。

方法:用五层地图把“看不见”变成“可证据”

我们没有从画漂亮的架构图开始,而是从“证据”开始。地图分五层:代码与构建依赖、运行时调用、数据血缘、文件与消息、人与流程。每一层都要求留下可复核的证据,而不是“我记得应该是这样”。

第一层,静态扫描。我们用 ASM 解析 jar 包字节码,提取类、方法、注解和 Spring 配置;解析 MyBatis XML 和 JPA 注解,拿到 SQL 与表名的初步映射;用 tree-sitter 解析 shell 脚本,识别其中的数据库连接、文件路径和外部命令;再解析 crontab 与 Windows 计划任务 XML,把所有调度入口登记成节点。

第二层,运行时追踪。APM 的链路数据给出服务间调用,Nginx 日志给出外部入口,Oracle 审计和 AWR 给出数据库访问,旁路流量分析补齐没有埋点的老接口。我们对低峰期也做了影子采样,避免只看到白天的高频调用,漏掉凌晨的批处理。

第三层,数据血缘。把 SQL 解析结果、存储过程、触发器和审计日志合并,标记每张表的读写方。共享表是重点:一张表被五个系统写,谁都不敢先动。第四层,文件与消息。SFTP 日志、文件头尾格式、MQ 主题和消费者清单,全部登记成“契约节点”。第五层,人与流程。我们访谈了十二位业务和运维同事,跟着值夜班观察“影子流程”,把 Excel 手工传递、邮件审批和口头约定也画进图里。

图上每个节点和边都带置信度:已确认、推断、疑似。推断和疑似必须经过责任人签字或运行时验证才能升级。这样做的代价是前期慢,但好处是后面没人能用一句“我感觉不对”推翻整张图。我们还用三个指标衡量地图质量:高风险链路覆盖率、边确认率、发版后的更新延迟。地图不是一次性报告,而是一份会呼吸的资产。

实施细节:六周,从清单到可执行接缝

第一周做资产盘点:应用、脚本、任务、表、文件目录、接口、负责人。第二周补运行时:把 APM、日志和审计数据灌进图数据库。第三周做数据血缘和文件契约。第四周做人物访谈与现场观察。第五周生成可视化地图和风险清单。第六周开依赖评审会,逐条确认高风险的边。

技术上,我们用 Neo4j 存图,节点包括服务、作业、脚本、表、文件、主题、接口、人员和业务能力;边包括调用、读写、触发、投递、消费、拥有和“手工交接”。前端用 Graphviz 出静态图,查询用 Cypher。比如“找出所有写入结算表但不在订单服务代码库里的节点”,一条查询就能把隐藏写入方拉出来。

地图不是目的,接缝才是。我们给每条依赖打分:调用频率、故障影响、耦合类型、变更频率、是否有负责人、是否有契约测试。分数高的边先处理:能加契约测试的加测试,能改成事件的改事件,暂时动不了的标记为“重构禁区”。最终输出三样东西:依赖地图、接缝候选清单、风险登记册。重构方案从“拆成十四个服务”调整为“先拆八个边界清晰的模块”,剩下的等依赖解耦后再谈。

为了让地图进入日常流程,我们把高风险依赖接入了发布检查单:改到哪张表、哪个文件目录、哪个定时任务,系统会自动弹出受影响节点和责任人。灰度期间,每次变更前先跑一遍“依赖影响分析”,再决定回归范围。这一步看似增加流程,实际把“全量重测”变成了“按边测试”。

可衡量结果

六周后,地图上有 1847 个节点、3962 条边。我们发现了 218 个未登记在代码仓库里的调度作业、37 张被多系统共享的表、19 个 SFTP 文件契约、11 个没有文档的接口,以及 6 处关键 Excel 手工交接。最意外的是一条“门店日结”链路:一个很多年前留下的 shell 脚本直接写结算表,连运维交接文档里都没有。如果没有地图,第一次拆分几乎必然踩中它。

结果也可量化:重构范围从 14 个服务收敛到 8 个;回归用例从 1240 条降到 730 条,因为依赖边界清楚了,不需要全量重测;变更失败率从 18% 降到 7%;平均故障恢复时间从 95 分钟降到 42 分钟;第一次灰度回滚只用了 12 分钟。我们还把三条高风险依赖提前改成契约测试,节省了大约 6 人月的返工估算。地图本身没有写一行业务代码,但它改变了重构的顺序。

经验教训

第一,地图不是架构图。架构图描述“应该怎样”,依赖地图记录“实际怎样”。第二,静态扫描会骗人,动态追踪也会漏人,必须交叉验证。第三,最危险的依赖往往在人身上:一个老员工的“我一般先导 Excel 再传 FTP”,比十个接口都难替代。第四,置信度标签非常重要,它让讨论从立场之争变成证据之争。第五,地图会过期,必须指定维护人,并在每次发版后更新高风险边。第六,不要试图一次画全,先覆盖“变更会碰到”和“故障会扩散”的区域。第七,接缝不是越细越好,能独立演进的边界才有价值。

什么时候适用

这套方法适合:运行多年、技术栈异构、存在大量脚本和手工流程、变更成本高、故障影响大的系统;也适合受监管、需要审计追踪的场景。它不适合全新系统,也不适合两周就能重写的小系统。启动前最好确认三件事:能拿到运行时数据,业务和运维愿意接受访谈,团队愿意用四到八周做“不产出功能”的工作。如果这三件事做不到,地图会变成墙上的装饰画。

重构老系统,最贵的不是写新代码,而是不知道旧系统还在靠什么活着。先把看不见的依赖画成地图,再决定从哪里下刀,顺序反了,勇气就会变成事故。