凌晨两点零六分,林海把插头从插座里拔了出来。
不是服务器电源,而是那台准备更新账务程序的旧电脑与外部调制解调器之间的电话线。灰色水晶头脱离接口时发出轻微一响,屏幕右下角仍闪着“连接中”。
“先隔离。”他说。
小郭站在旁边,手里还拿着升级说明:“银行批次已经收完,不会再进新数据。”
“你确定?”
“窗口早关了。”
“客户可以传真状态,值班人也可能录入。先断,别靠想当然。”
账务程序的新版本在零点上线。改动不大:支持银行软盘的统一状态码,自动把受理、待核、退回和到账对应到星辰的交易状态,减少人工录入。
前两个小时一切正常。零点清算时,左右账也相等。
两点,小郭按惯例抽查十笔,发现一笔被银行退回的指令在页面上显示“收款方已收到”。他以为看错编号,又查第二笔、第三笔。当天共有三十四笔使用新格式,其中六笔状态不对。
错误没有转走真钱。银行按自己的系统执行,星辰只显示状态。但对用户而言,“已收到”足以让付款人停止追问、让收款人开始发货,也可能让人错误地再次付款。
小郭第一反应是改映射表,把错的六笔重新计算。
林海拦住了他。
“先保留当前程序、数据库和输入文件。记发现时间。停止自动更新,页面进入状态维护。”
“修一行代码更快。”
“你还不知道错在哪一行。”
马飞被传呼叫到机房时,两点二十九分。他穿着毛衣外套,鞋带一只系着一只散着。出租车司机听说他半夜赶机房,一路问是不是网站被外国黑客攻击。马飞没心思解释,下车时把零钱全落在后座,车已经开远才发现。
机房里,林海把时间线写在白板上。零点零三分部署,零点十一分读取第一批新文件,一点五十八分小郭抽查发现。自动程序在此期间处理三十四笔,但只有六笔错误,说明不是整个版本完全失效。
“回滚。”马飞说。
小郭急了:“新版本产生的记录怎么办?直接恢复零点备份,会丢两个小时。”
“所以不是把整台机器退回零点。”林海说,“代码和状态映射回上一版;零点后的原始输入与操作日志保留,按旧规则重放。页面在重放完成前只显示最后经确认状态,不显示猜测。”
“要多久?”
“先演练一次。”
新版本上线前,他们做过数据库备份,却只在测试机验证了升级,没有完整演练带着两小时新数据回滚。现在真正需要退时,每一步都得重新确认。
马飞一度想亲自敲命令。林海把键盘推回小郭面前:“今晚他是当班操作,我复核。你批准回滚,不要同时又做执行人。”
“这是紧急情况。”
“紧急更要知道谁做了什么。”
马飞把手收回来。他坐到机柜旁的折叠椅上,看着两人复制程序目录、校验备份、冻结自动任务。曾经他认为掌握全部关键操作才是负责,如今最难的克制反而是在出事时不抢走别人的岗位。
两点五十一分,测试机完成第一次重放。三十四笔原始记录按旧规则导入,所有银行新状态暂列“待人工映射”,没有出现虚假到账。确认结果后,生产机回滚。
三点十七分,页面恢复。六名受影响参与人收到明确通知:星辰状态显示发生错误,银行实际处理不受影响,请以银行回执为准;相关记录正在逐笔核对,在完成前不要重复付款或发货。
错误原因仍没找到。
小郭把新旧文件逐字比较,终于在银行格式说明的附录看到一项差异。状态码字母“R”,在星辰旧测试表里代表Received,已收到;在银行正式文件里代表Returned,已退回。测试环境使用的是星辰自拟代码,正式上线却没有把银行代码表版本写进程序包。
同一个字母,在两套系统里说着相反的话。
“为什么只有六笔错?”马飞问。
“其余都是A和p,含义碰巧一致。”小郭说。
碰巧一致比全部报错更危险。程序成功处理了大多数记录,让人更容易相信剩下的也正确。
四点,六笔状态全部依据银行回执更正。没有客户已按错误信息发货,也没有重复付款。林海没有把事故标成“无损失”,而是“未观察到资金与货物损失,存在错误决策窗口一小时零六分”。
六名参与人必须逐一回访。前三个已经睡下,第四个是印刷耗材商的夜班仓管。他说自己看到“已收到”后,已经把两箱色带搬到出货区,只差司机没有到。若错误早一个小时发生,货可能已经离开仓库。第五名用户的家中电话由母亲接听,老人听见“付款错误”吓得立刻叫醒全家,直到小郭反复说明银行资金没有异常才安静下来。第六名留的是单位总机,夜里无人接,只能等早班。
小郭每打完一个电话,就在通知单上写下对方实际采取的动作。错误状态不再只是一行数据库值,它险些变成两箱发出的货,也变成一个家庭半夜围着电话的惊慌。
早上八点,最后一名参与人回电。他没有损失,却问星辰以后凭什么让人相信页面。马飞回答,不能靠一句“已经修复”,要给出银行回执对照、错误时间窗、受影响范围和复核入口。对方沉默一会儿,说那就把报告寄来,下一批是否继续等看完再决定。
“写得太吓人。”小郭说。
“下次才能知道窗口有没有缩短。”
天快亮时,机房空调吹得人手指发僵。林海从包里取出一台小录音机,磁带上贴着“六一节目”。女儿幼儿园提前排练,他答应昨晚值班时听,直到现在才想起来。
磁带放出来,先是老师报幕,随后一群孩子唱跑调的《小燕子》。林小满的声音分不出来,林海却听得很认真。唱到一半,磁带被旧录音机绞了一下,声音忽然拖长。
他马上按停,打开盖子,用铅笔一点点把磁带卷回去。
“不接着放?”小郭问。
“再转会断。”
“你怎么什么都先停?”
“坏过几次就会了。”
马飞看着那盘磁带,想到回滚也是同一种承认:继续并不总比停止勇敢。系统正在出错时,最宝贵的能力不是立刻向前,而是保住还没断的部分,回到一个被证明过的位置。
早上六点,三个人去机房外的小摊吃豆浆。摊主刚开火,油条还没炸好,只能一人拿两只昨夜剩下的馒头。林海把馒头掰碎泡进豆浆,小郭学他,结果碗里全是碎屑。
“回去睡。”马飞说。
“事故复盘呢?”林海问。
“下午再开。”
“下午是我休息。”
马飞看值班表。林海说得对。事故不能以复盘为名继续占用他下班后的时间。
“那明天上午。今天只交接必要事实。”
林海点头,把修好的磁带装回包里。他要赶第一班公交回家,在女儿起床前把录音机放到桌上,假装自己昨晚已经听完。
第二天复盘会上,星辰新增四条发布条件。外部状态码必须绑定来源机构和版本,不能只存一个字母;上线前使用银行提供的正式样本做黄金测试;备份不只验证能恢复,还要演练带新数据回滚;任何显示“已到账”的状态,必须能追到银行证据,而不是由内部代码自行推断。
小郭主动在事故记录上签名,没有被罚款或取消权限。他负责写了第一份回滚手册,把自己当晚想直接改映射表的冲动也记进去。
“这也写?”陈维明看报告时问。
“以后别人也会想这么做。”小郭说。
新版本一周后重新上线。这一次,R被明确写成“银行退回”,代码旁还有银行格式版本。团队在测试中故意放入相反含义的旧样本,程序整批拒绝,没有靠猜测转换。
凌晨两点的回滚没有成为对外宣传。没有客户愿意因为系统曾经把退回显示成到账而鼓掌。
可那一夜让星辰确认了一件比顺利升级更重要的事:当程序说错话时,团队能够停止、保留现场、退回旧路,再让每一笔新发生的事实重新走一遍,而不是用一行匆忙修改把错误和证据一起覆盖。
机器最终向前运行。
它向前的依据,不再只是某个人确信自己已经改对。
那盘差点被绞断的磁带,也在林海家的饭桌上完整放完。