CHAPTER 050 / 100
第五十章:企业项目压力倍增
听这一章
微软云希 · AI 合成朗读 · 17:39
周一早上九点零三分,产品抱着电脑走进会议室,开口第一句话就是:
“客户昨晚又调整了一版需求。”
小马哥刚拧开的矿泉水停在半空。
伟源抬头看了产品一眼,又低头在测试本上翻到新的一页。
我坐在最里面,手边那杯咖啡还没来得及喝,心里已经先苦了一下。
企业项目上线才几天,我们本来准备把剩下的历史数据巡检和监控补完。结果客户真正把系统接进自己的流程以后,很快发现原来的方案不够用。他们内部已经有一套业务系统,不想让操作人员每天登录我们的后台手动提交任务,而是希望直接通过接口批量创建,生成完成后再由我们主动通知。
除此之外,他们还提了几项调整:创建任务时要带自己的业务单号,查询接口要返回更细的失败原因,回调里要补模型版本和结果时长,同一批任务还希望支持新的风格参数。
每一条单独看,都像是在现有接口上多加两个字段。
合在一起看,就不是加字段了。
是把创建、查询、回调、数据库和模型适配一起拎出来,问它们能不能在不影响线上账号的前提下,集体换个姿势继续跑。
产品把客户发来的流程图投到屏幕上。
“他们接下来要扩大使用范围,所以希望这几个调整尽快上。尤其是业务单号和结果回调,这两个不解决,他们那边没办法把生成结果跟自己的记录对应起来。”
这话听着急,但并不离谱。
客户不是坐在办公室里突然灵感爆发,想拿程序员练手。他们之前只用页面试跑,很多问题藏在人工操作后面。现在真要接系统,哪一笔请求是谁发的、有没有重复、结果回来了没有,自然都得说清楚。
我最怕的也不是客户改需求。
我怕的是所有人都把改动当成一句“顺手支持一下”,然后今天改创建接口,明天补数据库,后天发现模型参数没存,第四天回调重试又把同一条结果推了三遍。到最后每个人都很忙,系统也确实改了,只是没人能讲清楚它现在到底按哪一版规则活着。
我把客户流程图从头走了一遍,没有马上报时间。
“先别按字段拆。”我说,“按链路拆。客户系统提交一笔请求,我们怎么认出这是同一笔;任务创建和额度处理能不能一起成功;模型用的是哪版参数;结果落库以后什么时候回调;回调失败又怎么补。这个顺序先定下来,再看接口改多少。”
小马哥点了点屏幕上的业务单号。
“这个我来跟客户确认唯一范围。是整个企业唯一,还是他们不同项目下各自唯一,不能靠我们猜。”
伟源接了一句:“我先补重复提交、回调超时和新旧参数混用的测试场景。接口文档出来一版,我跟一版。”
产品问:“那客户今天还能继续提调整吗?”
“能提。”我说,“但得进同一个变更记录。谁提的、为什么改、影响哪条链路、这次做不做,都写清楚。口头消息不直接进开发。”
我说完以后,自己先在心里停了一下。
以前碰到这种会,我最容易说的是“问题不大,我先改”。这句话当场特别好用,能让会议迅速结束,也能让所有人对我露出一种“这人靠谱”的表情。
代价是两个小时以后,我会同时开着五个窗口,靠聊天记录考古需求,再怀疑刚才那个自信点头的人是不是被什么东西附了身。
现在我终于知道,负责人真正危险的时候,不是他说“这个我不会”,而是他什么都还没确认,就先让所有人觉得自己都会。
早会结束前,我们新建了一张变更表。
需求来源、业务原因、接口版本、数据影响、兼容方式、负责人、验证人和计划时间,一项都没省。紧急线上问题随时处理,影响外部协议的调整必须先评审,普通体验优化进下一批,不再谁在群里声音大谁就先插队。
十点之前,小马哥跟客户确认了业务单号的规则:同一家企业内唯一,客户超时重试时必须沿用原单号。我们决定不直接改掉原来的页面接口,而是给系统对接单独留出明确入口,旧账号照常使用,新接口按客户业务单号做幂等。
方案刚定下来不到半小时,第一个紧急问题就到了。
客户联调人员在群里发了两笔一模一样的任务截图。
“我们第一次调用超时,重试后返回成功,但后台出现了两个批次。”
小马哥把两次请求记录拉出来,业务参数完全一致,间隔十二秒。第一次其实已经创建成功,只是客户那边在超时时间内没有收到响应,于是按正常策略又发了一次。
我们之前在页面提交上做过防重复,同一个提交令牌不会创建两次。可这次是外部系统重试,每次 HTTP 请求都能拿到新的服务端令牌。站在原接口的视角,两次请求都很新;站在客户的视角,这明明就是同一笔业务。
幂等没有失效。
是我们把“同一件事”的边界认错了。
我盯着日志,后背一下绷紧。重复创建的不只是两行任务记录,后面还会跟着额度占用、模型调用和结果回调。现在只多了两个批次还算发现得早,真让客户批量接入以后再碰上,数据库里很快就能长出一片双胞胎。
“先暂停外部批量联调。”我说,“页面端不动。小马哥确认受影响的请求范围,伟源补连续重试和并发重试,我来改业务单号这层。”
修复方案没有只放一把缓存锁。
缓存能挡住大部分同时进来的请求,但服务重启、锁过期或者两台机器时序撞在一起,仍然可能漏。最后真正兜底的是企业标识和客户业务单号的唯一约束。第一次请求创建内部批次,后面的重复请求不再生成新任务,而是返回第一次已经创建的批次号和当前状态。
更关键的是,任务批次、任务明细和额度记录必须在同一个事务里完成。中间任何一步失败,前面的数据一起撤回,不能让客户拿到批次号以后才发现额度没记,或者额度扣了,任务却没落下来。
十一点二十,伟源连续压了几十次重复请求,批次始终只有一份。
小马哥把新的调用规则和返回示例同步给客户,第一处紧急问题暂时收住。
我端起早上那杯咖啡喝了一口。
已经凉了。
味道很像一个 Bug 修完以后才发现还有半天要过。
中午十二点四十,我们几个人刚把外卖盒掀开,群里又跳出一条消息。
客户收到“生成成功”的回调后,立刻调用查询接口,任务状态却还是“生成中”。过了两三秒再查,结果才正常出现。
第二个紧急问题来了。
这次没有重复数据,也没有接口报错。回调成功,查询成功,数据库最终状态也是对的。四个环节各自拿出成绩单,都能证明自己没挂科。
客户连起来一用,顺序却反了。
我把那条任务的时间线拉出来,很快找到了问题。模型结果回来以后,我们先更新任务状态,再在同一段处理逻辑里调用客户的回调地址,最后数据库事务才提交。正常情况下这几步很快,肉眼看不出差别。客户系统一旦收到回调就立刻反查,查询请求可能正好抢在事务提交前面,于是看见的还是旧状态。
相当于我们在饭还没出锅的时候,先给客人发消息说可以开吃了。
小马哥看着我:“回调放到提交以后?”
“对,而且不能让外部回调绑住本地事务。”我说,“先把任务状态和结果稳稳落下来,提交成功以后再产生回调事件。客户接口慢或者暂时失败,只重试回调,不回头把已经成功的模型结果一起折腾一遍。”
伟源在测试环境里故意给事务提交加了延迟,很快复现了同样的现象。他把查询请求的时间点和数据库状态截在一张图上,连解释都省了。
我们重新调整处理顺序:模型结果先完整落库,任务状态与结果记录保持一致;事务提交成功后,回调任务再进入独立队列。每次回调都有固定事件编号,客户返回失败就按规则重试,成功记录下来以后不重复通知。
这部分改动比第一处更让人紧张。
重复提交最多是在入口多造一份任务,事务顺序如果处理错,可能出现数据库已经成功、回调没发,也可能回调发了、数据库又回滚。前者还能补,后者会让两个系统各自坚信一套事实,然后把联调群变成分布式争论现场。
下午两点半,伟源又跑了一轮。
他先让客户回调地址故意超时,再恢复正常。任务结果没有被重复写入,客户回调按原事件编号重试,查询接口每次拿到的都是已经提交的数据。
“顺序对了。”他说,“失败重试也没带出新任务。”
我这才把筷子重新伸进饭盒。
米饭已经结成一小块,夹起来很完整,比我们上午的事务边界还清楚。
本来以为下午能安静一点,模型侧又发来一版参数说明。
客户希望同一类任务生成出来的风格更稳定,模型同学为企业场景做了新版本,增加了一个风格强度参数。旧接口里我们用的是百分比整数,新版本接收的却是零到一之间的小数。模型同学在对齐会上明确讲过转换规则,我们也在新适配层里处理了。
问题出在批量任务的重试链路。
傍晚五点零七分,报警群第三次亮了。
某一类任务的模型调用失败率突然升高。正常提交能跑,失败任务自动重试以后却连续报参数不合法。更麻烦的是,一部分上线前创建的旧任务没有记录具体模型版本,重试时会直接拿当前默认版本,导致同一批任务前后走了不同的参数规则。
那天我的心跳跟报警群差不多,上午、下午、傍晚,各响一次。
第三次响的时候,身体已经不像第一次那么利索了。我盯着那几条失败记录,脑子里先闪过一个很熟悉的念头:我来查接口,我来补数据,我来跟模型同学对,我再顺手把重试也改了。
这个念头刚跑到一半,小马哥已经在群里把受影响的模板暂停了,避免新任务继续进入错误链路。
“客户那边我去解释,先告诉他们影响范围和恢复时间。”他说,“你别一边改代码一边回十个人消息。”
伟源也把复现条件整理了出来。
“只有旧任务加自动重试会触发。新任务正常,手动重新提交也正常。我继续测版本切换和失败补偿。”
模型同学确认了服务本身没有异常,并把新旧版本的参数差异重新列了一份。问题最终落在我们自己的任务记录和适配逻辑上:任务创建时只保存了业务参数,没有把模型版本和参数协议一起固定下来。第一次调用靠默认配置还能跑,后续只要默认版本升级,旧任务重试就可能被塞进一套它从没见过的规则里。
模型优化本来是为了让结果更稳定。
我们却差点因为没有保存版本,让“优化”变成每次重试都重新开盲盒。
我没有再把所有事情收回自己手里。
小马哥继续负责客户同步和受影响任务清单,伟源守着回归矩阵,模型同学确认版本兼容范围。我只处理后端这条主线:新任务创建时明确保存模型版本和参数快照,旧任务按原规则补齐可确认的版本,无法确认的暂停自动重试,交给人工判断,不拿线上数据赌默认值。
晚上七点四十,暂停的那批任务恢复。
新任务按新版本转换参数,旧任务重试仍沿用原版本。伟源把正常提交、自动重试、版本切换和回调补偿连起来跑了两轮,没有再出现参数错误,也没有因为重试多生成一份结果。
小马哥在客户群里发完恢复通知,往椅背上一靠。
“一天三个紧急 Bug。”他说,“咱们这个项目现在不是压力测试,是压力包月。”
我揉了揉太阳穴。
“至少今天的套餐已经用完了。”
伟源抬头看我:“这句话最好也列入上线期间禁用词。”
我们三个人都笑了一下。
笑完,谁也没急着走。
当天的问题虽然都收住了,可如果只停在“修好了”,明天同样的混乱还会换个字段重新来一遍。我们把早上的变更表重新打开,一项项补齐。
小马哥负责外部接口协议和客户确认,所有字段调整必须带版本、示例和生效时间;伟源负责变更对应的测试矩阵,重复请求、延迟、回调失败、事务回滚和模型版本切换都变成固定用例;我负责数据库变更、幂等规则、事务边界和模型版本记录,不再让这些关键约束只存在某个人脑子里。
产品也把今天新收到的需求全部补进记录,标清哪些是本轮必须,哪些进入下一版。晚上以后再来的普通调整,先记录,第二天评估,不再半夜一句“客户觉得这里可以再改一下”,大家就集体打开电脑。
最后,我们又更新了接口文档、数据库字段说明、回调重试规则和故障处理记录。每一项变更后面都有负责人和验证人,已经完成的贴上结果,暂缓的写明原因。
那张表看起来一点都不高级。
没有酷炫架构图,也没有什么能让领导听完立刻鼓掌的新名词。它只是把今天从会议室、报警群和聊天记录里飞出来的东西,一件件钉回该在的位置。
可对当时的我们来说,这比再写一层漂亮的封装有用。
需求会继续变,接口还会增加,模型也不可能永远停在同一版。真正能让项目活下去的,不是提前猜中客户以后所有想法,而是每次变化进来时,团队都知道先确认什么、谁来处理、怎么验证,又把结论留在哪里。
晚上九点多,我把最后一项任务分给自己,合上电脑。
小马哥在收客户确认记录,伟源在整理回归报告,产品把明早要继续确认的问题留在了变更表里。桌面还是乱,报警群也没因为我们辛苦一天就自动解散,但我没有那种所有口子都压在自己身上的窒息感。
压力确实比项目上线前更大了。
只是这一次,压力进来以后会被拆开,会有人接住,也会留下记录,不再全部堆进我一个人的待办队列。
我暗自想:“压力大,但比自己独自扛时好很多。”