发行版的冻结日和内核的发布日打架时,改的应该是发布计划

一、冻结日是常量,上游发布日是变量
一次例行的版本安排出了个小变动:某个发行版把原计划采用的内核换成了更新的一版,理由很直白——那更新的一版还停在候选阶段,正式释出的窗口可能再往后挪一周,而发行版自己的功能冻结时间不动。表面看这只是上游的一个选择,实质是每一层交付都会撞上的结构冲突:有一端是不可移动的约束,就是冻结日;另一端是可移动的依赖,就是上游的正式发布。既然改不动冻结日,也催不熟上游,唯一能改的只有自己打算怎么在一个候选版上把活干完。
这里要先破一个常见直觉:等上游正式发布了再动手。听着稳妥,实际把留给集成与回归的时间压缩到了发布前很短的一段,所有问题恰好堆积在最不能改的时候暴露,风险被一路推到了链尾。更合理的做法是把候选版当成一个可工作的对象来对待——它不稳,但你可以在它上面把该做的适配、该建的基线、该留的记录先做完,等正式版一落地,你需要的只是把已在候选版上跑通的那套原样重跑一次,而不是从零开始追时间。
二、在候选版上工作的三条纪律
没有纪律的候选版适配会把问题带进正式版。我们内部固定三条。
- 候选版与正式版分别建基线:两套依赖清单、两套镜像、两套测试快照。别把候选版的结果记到正式版的账上,否则正式版一发布,你对不上哪些差异来自版本本身、哪些来自环境漂移。
- 针对候选版做的适配要能在正式版上原样跑:写适配时就要自问这段改动是候选版特有的临时绕行,还是正式版也成立的长期修。前者要单独隔离并标注回收条件,后者才允许进主干。
- 记录当前基于第几号候选:候选版是滚动的,一个候选编号背后是一组具体的补丁集合。不记号,等到线上要复现时你说不清当初跑的是哪一版,日志和现场对不上号。
第三条最容易被当成可有可无,但它是把“问题”和“版本”绑定的唯一线索。一个没有候选编号的失败用例,几周后再看只是一条噪音。
三、冻结到来之前的三类必做
冻结日往前推,三类动作缺一类都会在冻结后被动。
- 同一套回归集在候选版和正式版上各跑一遍,然后把两次结果比对。比对的价值不在看谁通过,在看差异:哪些用例只在候选版过、只在正式版过,这些差异点往往就是升级真正引入的行为变化。
- 把启动序列与关键路径上时序敏感的用例单独列成一档。这类用例对内核的调度、内存与设备初始化顺序最敏感,最容易被一次版本变动打乱;混在常规用例里跑会被稀释,单列出来才能盯住它到底变没变。
- 把已知不稳定项写成一份明确的豁免清单,逐项标注为什么跳过、谁批准、什么时候回收,而不是在测试脚本里无声地把某条注释掉。无声跳过是交付里最难复盘的一类,回头翻的时候没人知道当初为什么少了这一条。
这三件事的共同点是都必须发生在冻结日之前。冻结之后再补回归比对,等于在窗口关闭后才发现差异,那时候要么推迟发布,要么带着已知差异上线,两个选项都比冻结前排掉要贵。时序敏感用例尤其如此,它在候选版上暴露得越早,可回退的空间越大。
四、硬冻结与软冻结,以及企业内部同构的三处
把冻结这件事拆细,会发现它分两档,而多数团队把两档混成了一个词。硬冻结是不允许再引入任何新依赖,代码只出不进;软冻结是允许引入但要逐条登记,登记之后要有人评估它对发布窗口的影响。两档混用最典型的后果是“我以为不能动”:一侧以为已经硬冻结什么都不许改,另一侧按软冻结还在那里登记新依赖,等两边合到发布节点才发现基线根本不是一个。
同样的结构在企业内部至少出现在三个位置,而且这三个日期通常不重合:内部平台的基线镜像有一个冻结日,依赖的开源组件有它自己的发布节奏,自家产品又有一个对客承诺的上线日。三者不重合时不能强求谁迁就谁,能做的只有把每一层的冻结分别写清是哪一档,再按最紧的那个约束倒排其他两层的动作。洹拓给客户排这种多基线交叠的发布时,第一步不是排期,是把“哪个日期可移动、哪个不可移动”标在一张表上,后面所有取舍都从这里出发。
五、内核层的特有风险与四段灰度
内核这一层的特殊之处在于,它的风险面不在应用测试能覆盖的范围里。驱动兼容是最直接的一处,同一份应用在候选版起得来、正式版起不来,多半是某个驱动的行为变了;文件系统与网络栈的行为变化更隐蔽,一次读写或连接语义的调整不会报错,只在特定负载下现形;电源管理带来的性能抖动最难归因,跑分正常、上线变慢这种落差很难用一次应用回归解释清楚。这三类都不是纯应用层能测到的,必须在下面垫一层裸金属或者专有云的验证面,让内核的差异先在那一层被观察到,而不是等它冒到应用层再猜。这也解释了为什么把一次内核升级当成应用发版来排期会出问题——应用的回归集看不到这一层,你以为跑过了,其实只是没测到。
灰度顺序在这种场景里要写死,而不是临场决定:
| 灰度顺序 | 对象 | 观察要点 | 回退方式 |
|---|---|---|---|
| 第一段 | 非生产环境 | 差异比对是否收敛 | 直接回到旧基线重建 |
| 第二段 | 无状态服务 | 错误率与延迟曲线 | 切回旧版本实例 |
| 第三段 | 数据库类有状态服务 | 数据面一致性 | 预留回滚脚本与快照 |
| 第四段 | 消息类有状态服务 | 积压与重投行为 | 按队列粒度停投再切 |
有状态服务排在最后,是因为一旦它们在两版之间来回切,脏数据比性能回退还难收拾,观察时长要显著长于无状态那一段。
六、落到交付:验收表与方案措辞
有一条措辞替换值得写进方案模板:不要把“我们用的内核是更新的稳定版”当卖点,这句话只描述了版本新旧,不描述你有没有能力兜住差异;应改成“我们在候选版与正式版两个版本上都跑过同一套用例并通过”,把可验证性而非版本号作为承诺对象。客户要买的是对差异的处置能力,不是一款听起来更新的内核。
配套是一张验收表,缺任何一列都无法证明这套比对真的跑过。
| 版本对 | 用例集 | 通过时间 | 失败项处置人 |
|---|---|---|---|
| 候选版对正式版 | 归一后的回归集标识 | 精确到某一次构建 | 该失败项的责任人 |
四列里最容易省的是失败项处置人,但它恰恰决定了这张表能不能真正走完一轮。一份验收表记到“通过时间”就散场,下一次同样的失败还会在同一个位置冒出来,因为没有署名的失败等于没有人认领的失败。把候选版当可工作对象来对待,说的不是胆子大,是每一处差异都要有人接住。