ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

xxx10版本收口实战:模块边界、数据兼容与回归验证

xxx10版本收口实战:模块边界、数据兼容与回归验证

1. 先把“xxx10”这个编号说清楚

“xxx10”这个编号第一次被写进排期表的时候,团队里没几个人觉得它特殊——无非是把上一个版本号加了个一。真到了要切分支那天,才发现事情不对劲:前面九个大版本攒下来的东西,全部压在这一个版本上结算。老接口的兼容承诺、被绕过去三轮的模块边界、三套并存的构建脚本、还有一堆“下个版本再清理”的临时开关,都得在这一版里给出交代。所以这篇东西不打算讲某个具体功能怎么实现,而是把 xxx10 当作一次长期迭代项目的版本收口来拆:它到底是什么、这一版在解决什么问题、哪些人适合照着做。

先把定义说死。xxx 是一个持续维护的内部项目代号,10 是它的大版本序号,不是补丁号,也不是小版本迭代。这种编号方式在很多长期项目里都能见到,好处是直观,坏处是它掩盖了版本之间的“性质差异”——第 4 版和第 5 版的改动量可能天差地别,但编号上只差一个一。xxx10 的定位很清楚:前九个版本负责把功能长出来,第十版负责把结构理顺,让后面能继续长。换句话说,它是一个“承重版本”。

它能做什么?具体讲,xxx10 处理的是四件事:模块边界重新划分、数据层的前后兼容、构建与发布链路统一、回归验证流程固化。这四件事听起来都不像“功能”,但它们决定了下一次改需求时,你是花两天还是花两周。解决的痛点也很典型:改一个字段要动五个文件、本地跑得通上了环境就崩、每次发版前靠人肉点一遍、老数据不敢碰因为没人知道谁在读它。

适合谁看?三类人。第一类是正在维护一个两三年以上老项目的开发者,你可能正处在“想重构又不敢动”的阶段;第二类是刚接手别人代码库的人,需要一套判断“哪些债该先还”的思路;第三类是做技术排期的人,想知道一个收口版本的工作量到底该按什么维度估。前置知识不需要很深,能看懂版本管理、接口约定、流水线这几个概念就够了。下面所有的步骤和参数,都是基于这类项目最常见的实践补全的,你照着自己的项目改数字就行。

2. 版本骨架是怎么从 1 长到 10 的

2.1 前三个版本只做加法,是刻意的

回头看,xxx 的前三个版本几乎没做过任何架构层面的决策,就是堆功能。第一个版本只有一个入口页加一张表;第二个版本加了权限和几个查询条件;第三个版本开始有人提“要不要拆模块”,被压下去了。当时压下去的理由现在看依然成立:在需求边界还没稳定的时候拆模块,拆出来的边界八成是错的。你以为是按业务拆,实际上三个月后业务本身合并了。

这个阶段的代价也是明摆着的。所有逻辑挤在少数几个文件里,一个请求进来要穿过七八层判断,改一处牵动一片。但这种“脏”是有价值的——它把真实的调用关系暴露出来了。等到第三个版本结束时,团队手里已经有了一张事实上的依赖图:哪些功能总是一起改、哪些数据总是被同一个人读、哪些接口半年没人碰。这张图不是画出来的,是改 bug 改出来的,比任何架构图都准。

我个人的经验是,前三个版本不要急着谈架构,但要强制做一件事:每次改需求时记录“这次动了哪几个文件”。这份记录攒到第三次,模块该怎么切基本就自己浮出来了。很多团队跳过这一步,直接按想象画架构图,结果第四版就开始返工。xxx 没跳过,所以第四版的拆分方案几乎是照着一份文件改动频次表抄下来的。

2.2 中间四个版本集中还债,节奏要控住

第四到第七个版本,是还债期。这段时间的工作有个共同特征:做完之后用户看不出任何变化。模块拆了、接口收了口、日志规范统一了、配置从代码里挪出来了。这类工作最容易在排期会上被砍,因为它们不产出可见价值。xxx 能推下去,靠的是一个很土的办法——每个还债任务都必须绑定一个“下一次改需求能省多少时间”的预估,估不出来就不做。

举个例子。第五版把配置从代码里抽出来,当时估的是“以后改环境相关参数不用重新打包”,实际收益在第六版体现出来了:一次线上参数调整从“改代码、走发布、等窗口”压缩到“改配置、重启进程”,耗时从半天降到十分钟。这个收益被写进了第六版的复盘,成了后面继续还债的依据。还债这件事,光讲道理没用,得有账本。

节奏上也有讲究。中间四个版本没有一次是纯还债,每个版本都是“七成功能 + 三成清理”。纯还债的版本风险太高——改动面大、没有新增功能做对照、出问题不好定位。掺着做的好处是,功能部分可以当作清理部分的验证载体:模块拆得对不对,看新功能接进去顺不顺就知道了。这个比例不是拍脑袋定的,试过五五开,结果版本周期拉长了近一倍;试过九一开,清理部分基本等于没做。

2.3 第八、第九版开始做减法,第十版收口

第八版是个转折点,因为这一版第一次删东西。删掉了两个半年没人调用的接口、一套并行的旧权限判断、还有一个被新方案完全覆盖的缓存层。删代码这件事的心理门槛比加代码高得多,谁都怕删出问题。xxx 的做法是先“标记弃用”,在日志里打点观察一个月,确认调用量为零再动手。这个流程后来固化成了规范,第九版又照此删掉了一批。

第九版做的是“统一”。三套构建脚本合成一套、两种日志格式合成一种、前后端的错误码对齐。这类工作听起来琐碎,但它是第十版能顺利推进的前提——如果构建还有三套,你根本没法保证回归验证跑的是同一份产物。

到了 xxx10,剩下的是最难啃的:模块边界要重新划一次(因为前四个版本拆出来的边界,随着业务变化又不准了)、数据层要做兼容改造、发布链路要收敛成单一入口。这三件事的共同点是都涉及“跨模块”,一个人关起门来做不完,必须有人协调。所以第十版的排期方式和前面九个版本完全不同,不是按功能模块排,而是按改造批次排,每一批都要前后端和数据侧同时对。这个变化本身就是信号:项目已经从“各自干活”进入了“协同收口”的阶段。

3. xxx10 里最硬的三个改造

3.1 模块边界重划:先量依赖,再动刀

模块边界这件事,最怕的是凭直觉。xxx10 的做法是先跑一遍依赖分析,把模块之间的引用关系导出来,按引用次数排序。分析结果和当初的设想差得挺远:原本以为最独立的模块,实际被七个地方引用;原本以为耦合最重的两个模块,之间只有三个调用点。这说明边界错位往往不是“拆得太粗”,而是“拆错了方向”。

具体做法分三步。第一步,导出依赖矩阵,行是调用方,列是被调用方,格子里填调用点数量。第二步,找“双向引用”——A 调 B 的同时 B 也调 A,这种地方一定是边界画错了,因为健康的模块依赖应该是单向的。第三步,按“改动同步率”验证:统计过去半年里,哪些模块总是被同一次提交改动,同步率高的应该合并,而不是继续拆。

xxx10 里这一步的产出是一张表,把原来的 11 个模块收敛成 7 个,同时新增了 1 个明确的“共享基础层”。共享层只放纯数据结构和工具函数,不允许有业务判断,这条规则写进了评审清单。没有这条规则,共享层三个月就会变成第二个垃圾桶。

注意:重划边界时不要把“未来可能用到”的东西放进共享层。判断标准很简单——如果删掉它,现有代码是否报错。不报错就不该在里面。

参数上有个经验值可以参考:单个模块的对外暴露接口数量控制在 20 个以内,超过就说明这个模块承担了太多职责。xxx10 改造前最大的一个模块暴露了 43 个接口,改造后拆成两个,分别是 17 和 12。这个数字不是硬指标,但它是个很好用的信号灯。

3.2 数据层兼容:让新旧代码能同时跑

数据层的改造是最容易出事的地方,因为它牵扯到“已经存在的数据”。xxx10 面对的情况是:字段命名不统一、部分字段类型不一致、有几张表存在冗余列,且没人敢确认这些列还有没有在读。直接改表结构风险太大,所以采用的是双写 + 影子读的过渡方案。

双写的意思是,新代码写入时同时写新旧两个字段,旧代码只读旧字段、新代码只读新字段。这样两边都能正常工作,互不干扰。影子读的意思是,在读取旧字段的地方额外读一次新字段,把两者的值做比对,不一致就记一条日志但不影响返回结果。这个阶段要跑够时间,xxx10 定的是两周,覆盖一个完整的业务周期。

比对日志是关键。跑完两周后统计,不一致率从最初的 0.7% 降到 0.02%,剩下的通过逐条排查确认是历史脏数据,才敢进入下一步——把读切换到新字段,旧字段进入只写不读状态,再观察一周,最后停写。整个流程走完大概三到四周,比“直接改表”慢,但它把不可控的停机风险换成了可控的时间成本。

改造过程中有几个细节值得单独说:

  • 字段类型从“字符串存数字”改成“数字类型”时,不要一次性转。先加新列,双写时做转换,遇到无法转换的值记日志并保留原值,等脏数据清理干净再切。
  • 冗余列不要急着删。先停写、再停读、最后删列,每一步至少间隔一个发布周期。xxx10 里有一列从停读到删除隔了六周,中间还真发现一个定时任务在读它。
  • 索引调整要和字段改造分开做。同一批次里既改结构又改索引,出问题时根本分不清是哪边引起的。

3.3 构建与发布链路:从三套到一套

xxx10 之前,项目里有三套构建脚本,分别对应“本地调试”“测试环境”“生产发布”。三套脚本各写各的,参数默认值不一样,导致过一个很经典的问题:本地构建出来是好的,测试环境构建出来少了一个配置项。排查了半天,发现是两套脚本对同一个环境变量的默认值定义相反。

合并的方式是只保留一套脚本,环境差异全部通过外部配置注入。脚本本身不含任何环境判断,只接收参数。这样做的直接好处是“构建产物一致”——同一个提交,无论在哪构建,产物完全相同,差别只在运行时读到的配置。这一点对回归验证极其重要,因为你可以确信测的就是要发的。

脚本参数化的设计上,有几个坑要避开:

参数类型建议做法不建议做法
环境标识必填,无默认值给个默认值图省事
构建模式显式声明靠环境变量隐式推断
版本号从提交信息生成手工填写
缓存目录按提交哈希隔离全局共享一个目录

版本号从提交信息生成这一条,看起来是小事,实际影响很大。手工填版本号必然会出现重复或者跳号,一旦出现,回滚的时候就找不到“到底哪个产物对应哪次提交”。改成自动生成后,产物和提交一一对应,回滚变成了“重新发布某个哈希对应的产物”,干净利落。

发布链路的收敛还包括一个动作:把发布步骤从“人工执行清单”变成“流水线阶段”。xxx10 把发布拆成四个阶段——构建、静态检查、自动化回归、灰度放量,前三个阶段任何一步不通过就中断,第四阶段需要人工确认。人工确认这一环保留着,是因为灰度放量的判断需要看业务指标,机器不好替。但除了这一环,其他全部自动,这样发布过程本身不会因为“某个人忘了某一步”而失败。

4. 一次完整的回归验证怎么跑

4.1 环境和数据的准备

回归验证最容易翻车的地方不是用例写得不好,而是环境不干净。xxx10 定了个硬规矩:每次回归必须从全新的环境开始,不复用上一轮的环境。原因是上一轮跑完会留下各种状态——缓存、临时文件、被改过的配置,这些残留会让下一轮的结果不可信。

环境准备分成四步,顺序不能乱:

  1. 拉取指定提交的代码,确认提交哈希与待发布版本一致。这一步要用脚本校验,不能靠人眼看。
  2. 构建产物,校验产物哈希。同一个提交构建两次,哈希应该一致,不一致说明构建过程引入了不确定性(比如打包了时间戳),要查。
  3. 初始化数据。基础数据从脚本导入,业务数据从生产环境的脱敏快照导入。脱敏必须在导入前完成,不要在测试环境里做脱敏,容易漏。
  4. 启动依赖服务,跑一遍健康检查。健康检查不通过的,不要急着跑用例,先解决环境问题。

数据这块有个具体建议:准备三套数据,分别对应“空数据”“正常量”“边界量”。空数据用来验证没有记录时的表现(很多项目在这里会报空指针),正常量用来跑主流程,边界量用来测分页、排序、超长字段这些容易出问题的地方。xxx10 里就有一批用例是专门跑空数据的,之前一直没测,一测发现三个地方直接报错。

4.2 分阶段执行与观察点

回归不是一次性跑完就完事,要分阶段,每个阶段有明确的观察点。

第一阶段跑冒烟用例,数量控制在 30 条以内,覆盖最核心的几条链路。这一阶段的目标不是“测全”,而是“快速确认环境可用”。跑完不超过十分钟,失败就立刻停下查环境,不要继续往下跑。

第二阶段跑全量功能用例。这一阶段耗时最长,建议并行执行但要控制并发数——并发太高会让被测服务本身扛不住,出现假失败。xxx10 用的是 4 并发,单轮大概 40 分钟。并发数怎么定?简单办法是逐步加,加到失败率开始上升就退回上一档。

第三阶段跑兼容性验证。这一阶段专门针对 xxx10 的改造点:老代码读新数据、新代码读老数据、新旧代码混合部署。混合部署这个场景最容易被忽略,但它在灰度期间真实存在,必须测。

第四阶段做数据一致性校验。跑完所有用例后,比对关键表的记录数和几个核心字段的聚合值,确认没有因为双写逻辑导致数据错乱。这一步用脚本做,输出一份差异报告,差异为零才放行。

提示:回归过程中产生的所有日志要保留,不要跑完就清。出问题时这些日志是唯一的线索,尤其是并行执行时,日志里的时间戳和线程标识能帮你还原执行顺序。

4.3 放行标准怎么定

放行标准如果定成“全部用例通过”,基本等于没定,因为任何一轮回归都可能有几条一直失败的“僵尸用例”。xxx10 的做法是把标准拆成三条,全部满足才放行:

  • 核心链路的用例通过率 100%,这批用例单独标记,数量不多但每条都对应一个真实业务场景。
  • 全量用例通过率不低于 98%,且失败用例必须逐条有结论——是环境问题、是用例本身写错了、还是真的缺陷。没有结论的不算通过。
  • 数据一致性校验差异为零,性能指标不劣于上一版本(响应时间的 P95 波动在 10% 以内)。

第三条里的“不劣于上一版本”需要有个基线。xxx10 在版本开始时就把上一版的性能数据跑了一遍存档,后面每次对比都拿这份存档比。没有基线就没法判断“变慢了一点”到底是改坏了还是机器忙。

另外有个实操细节:放行决策要留记录。谁签的字、依据是哪份报告、有哪些已知问题被接受,都写下来。这不是流程洁癖,是因为灰度期间真出问题时,你需要快速知道“这个问题是不是当时就预见到了”。xxx10 里就有一条已知问题被接受——某个低频查询在数据量大时会慢两秒,评估后认为可以接受,灰度期间果然有用户反馈,直接拿出当时的评估记录,五分钟就对齐了,不用重新排查。

5. 常见问题与排查速查

5.1 现象、原因、处置对照表

下面这张表是 xxx10 期间实际遇到并记录下来的,按“现象”查就行。

现象大概率原因处置方式
本地通过、环境失败配置默认值不一致检查配置注入来源,确认无隐式默认值
双写后数据条数对不上旧代码仍在写旧字段查是否有未迁移的写入点,加日志定位
灰度期间部分用户报错新旧代码混合部署,路由未对齐检查灰度规则,确认按用户维度而非请求维度分流
回归用例随机失败并发过高或存在共享状态降并发复跑,隔离共享资源
构建产物哈希不稳定打包了时间戳或随机值排查构建配置,固定输出
删除旧字段后偶发报错有定时任务或离线任务在读全量搜索字段名,检查任务调度配置
接口响应变慢新增了同步校验逻辑对比基线,把非关键校验改成异步

这张表最有价值的是最后一条。xxx10 上线后有个接口慢了 300 毫秒,排查发现是新加的权限校验放在了主链路上。这个校验其实可以延后做,改成异步后恢复正常。新增的校验逻辑一定要问一句“能不能异步”,这一问能省掉很多性能问题。

5.2 几条踩出来的心得

第一条,改造类工作一定要有“开关”。xxx10 的每个改造点都配了一个配置开关,出问题能一键退回旧逻辑。开关本身也有成本——要多维护一条分支,但相比“出问题只能回滚整个版本”,这个成本值。开关要有清理计划,xxx10 定的是“稳定运行四周后清理”,写进了待办。

第二条,不要在同一次发布里同时改结构和改逻辑。结构改动(拆模块、改字段)和逻辑改动(新增判断、调整流程)混在一起,出问题时无法二分定位。xxx10 拆成了两次发布,第一次只改结构,功能行为完全不变,验证通过后再改逻辑。这样出问题时至少知道是哪一类。

第三条,回归用例要跟着改造点长。每做一次改造,就要新增对应用例,而不是指望原有用例覆盖到。原有用例是按旧结构写的,新结构下的边界它测不到。xxx10 新增了二十多条用例,全部是针对改造点的,跑完这些用例,对改造的信心才够。

第四条,文档要写“为什么”,不要只写“是什么”。xxx10 的模块说明里,每个模块都有一段“为什么这样划分”,写清楚当时基于什么数据做的决策。半年后有人想再拆的时候,看到这段就知道当时的约束条件是什么,不会重复踩坑。

第五条,收口版本的排期要留缓冲。这类版本的不确定性比功能版本高得多,因为你没法准确预估“会遇到什么”。xxx10 的实际耗时比排期多了三成,多出来的部分全花在数据层的问题上。留缓冲不是保守,是承认这类工作的不可预测性。

最后说个很实际的体会。做完 xxx10 之后,最明显的变化不是性能或者代码整洁度,而是改需求的心理成本降低了。以前接到一个改动,第一反应是“这个会不会牵连到别的地方”,现在能比较快地判断影响范围。这个变化没法量化,但它是所有收口工作的真正回报——你换来的不是更好看的代码,而是下一次改动时的确定性。至于 xxx10 之后要不要继续做 11、12,我的看法是:收口版本不该定期做,该在“改动开始变慢”的时候做。判断信号很朴素,当你发现一个两天的需求要做五天,就该停下来理一理了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进