第九章 · 值班¶
八月三日,七点五十一分。值班第一天,第一通电话比签到早了七分钟。
"你好,结算组值班,陈稳。"
"运营部的。商户反馈对账数据对不上,差两千三。你们那边是不是有问题?"
"商户编号。"
"M202-4437-1。"
结算流水表里,这个商户最近一笔是凌晨三点四十七分,四万三千二百,状态已结算。运营那边说商户后台显示四万零九百,差两千三百。
再往下翻两行,还有一笔四万零九百的,结算时间三点四十九分,晚了两分钟,类型 MANUAL_SETTLE。人工补结。
"你们系统里有两笔。"陈稳说,"三点四十七分自动结算,三点四十九分人工补结。商户看到的是后一笔。"
"为什么会补两次。"
"第一笔金额四万三千二,第二笔四万零九百。差额两千三,等于一笔退款。你去问商户,结算日当天有没有退过货。"
"补结是谁做的。"
经办人字段里是 zhang.yi。
"技术支持中心的人。"陈稳说。
"名字。"
"张毅。"
"那我找他。"
"你找他之前先问商户。"陈稳说,"如果真有退款,就不是系统的问题。"
电话挂断。七点五十九分,值班系统弹签到确认,剩余时间二十三小时五十九分钟。
八点十四分,纪衡的邮件,昨晚十一点四十七分发的,标题"八月第一周工作安排"。正文四行:本周值班陈稳;三个转岗同事今日到岗,由陈稳带;每天下班前发一份带人进度;拦截器全量按计划推进。
第三行没有商量的余地,也没有工时预算。陈稳把这一行抄进记事本,后面画了个括号,写"每天 30 分钟"。他自己名下现在十五个工单,其中六个是老梁的。
九点三十分,工位区中间站了三个人,一男两女,背着包。男的戴圆框眼镜,二十三四岁,手里拿一份打印文件。
"请问结算组是在这里吗?"
"是。"
"我们是转岗过来的,今天报到。"
老梁的位置和旁边两个空位已经换了新显示器,键盘鼠标还没拆封。男的叫王磊,短发戴圆框眼镜的女生叫林晓,扎马尾的叫张悦。
"我在营销技术部做了三年后端。"林晓说,"主要做对账模块。"
"对账哪一块。"
"商户侧的差异明细,还有中间表的数据核对。"
"你们查生产数据用什么账号。"
"我们组有一个只读账号,配在测试环境的跳板机上。三年前配的,一直没换过密码。"林晓说,"怎么了?"
"没事。先装环境。JDK 十七,Maven,Git,IDE。有问题问我。"
张悦是今年六月的应届生,电脑上开着一个 Git 教程页面,停在第十步。
九点四十一分,值班手机又响。这次是运维组,问周一有没有人改营销结算适配器的配置。陈稳说没有。对方说日志里看到一次配置热加载。
"什么时间。"
"上周五晚上十一点多。"
"上周五我不在值班。"
"那就是别人。行,我记一下。"
十三点整,告警推送:结算队列积压超过阈值,当前 1,847 条,阈值 1,500 条。
积压线在缓慢上升,每分钟三十到四十条。十三点不是结算高峰。日志里请求来源是营销系统的促销活动结算,昨天上线的活动,今天开始产生结算请求,量超出预期两倍。
营销结算 adapter 的配置文件里有一行,被注释掉了:
注释符号后面的值是 false。也就是说,这一行本来是显式关闭限流的,有人把它注释了,注释掉之后走的是默认值——默认值也是 false。改了等于没改。
陈稳把限流打开,阈值一千二百条每分钟,保存,热加载。积压涨到一千九百条后开始掉,三分钟降到一千一百条。
记事本上加一行:"营销结算适配器限流已开启,阈值 1200/min。原配置被注释,注释前后行为一致。"
十五点四十分,王磊那边的显示器上是结算系统的配置类。林晓桌上放着两本书,《Java 并发编程实战》和《分布式系统设计》,书脊都开了。张悦的教程翻到第十四步。
十八点,外卖,炒饭,鸡蛋火腿青豆,上面一层油。吃到一半,值班手机响了,运维组确认限流是他开的。他说是。
"你开之前跟谁报备了。"
"没有。积压超阈值,值班有处置权。"
"流程上要有单子。"
"我补。"
十九点十二分,配置变更单补完,审批人一栏自动填的是纪衡。
补完单子,蒋辉的消息从客户现场发过来。
"听说你把营销的限流开了。"
"积压超阈值。"
"营销那边有人找我了,问是谁开的。"
"我。"
"我知道是你。"蒋辉说,"我跟他说是运维按预案处置的。"
"为什么。"
"因为他们组长上个月刚给纪总提过一次'结算侧配合度不高'。"蒋辉说,"你这周值班,别在这种事上留名字。"
"限流单子上有我的名字。"
"单子在流程系统里,没人看。"蒋辉说,"人的记性只记吵架。"
二十点五十分,纪衡的邮件:"拦截器已全量运行四十八小时,累计拦截 4,217 次,0 资损。周五前出报告。另,三个转岗同事的学习计划,每天下班前发一份进度。"
学习计划五行:周一环境搭建与架构概览,周二结算核心链路源码阅读,周三拦截器配置与测试,周四结算队列与限流机制,周五值班流程与告警处理。
发送时间二十一点零三分。抄送里没有加易文。数据组接下来要用林晓那边的对账口径,这件事陈稳知道,易文上周提过一次。他没有告诉易文有个做过三年对账的人转到了结算组。
二十三点十一分,家里。值班系统剩余时间十六小时四十八分钟。
零点零七分,何晚的消息:"睡了吗。"
他回了"值班"。何晚回了一个"哦",然后没有再发。
凌晨一点二十三分,告警音。
结算成功率降至 99.60%,持续下降中。十五分钟前还是 99.99%,现在还在往下走。
异常类型全部一样:数据库连接超时。
连接池使用率百分之九十七,活动连接一百九十四,最大二百。等待队列里十二个请求在排,最长的等了七秒三。
慢查询日志里有一条,执行时间从三十毫秒涨到三秒零四,每分钟调用两百一十次。查询语句里 WHERE 条件是两个字段:merchant_id 和 settle_date。
SELECT id, merchant_id, settle_date, diff_amount
FROM settle_reconciliation
WHERE merchant_id = ? AND settle_date = ?
EXPLAIN 出来是全表扫描,扫描行数一千一百四十七万。
settle_reconciliation。就是昨天那张工单里的表,两千三百万行,"对账中间表,临时使用"。
表结构里,两个查询字段上没有索引。
一点三十一分,ALTER TABLE settle_reconciliation ADD INDEX idx_recon_merchant_date (merchant_id, settle_date)。执行了三秒一。
连接池使用率从九十七掉到七十,成功率五分钟内回到 99.99%。
一点四十一分,记事本:
"2026-08-04 01:23 结算成功率降至 99.60%,根因:settle_reconciliation 表缺少索引,慢查询打满连接池。已添加联合索引 idx_recon_merchant_date,系统恢复。建议:DDL 审批流程增加索引检查。"
写完他又在下面补了半行:"建表人 张毅(工单 INC-20260728-0113)"。
补完他把这半行删了。删掉的理由不是不确定,是他还没看过 DDL 历史。
第二天八点三十分。王磊的显示器上是一张结算系统架构图,draw.io 画的,二十多个节点,每条线上标了方法名和参数。林晓在看代码,桌上一杯咖啡,杯沿有口红印。张悦的教程从第十步翻到了第十六步。
纪衡的邮件,早上七点十二分:"凌晨告警看到了。处理方式正确。DDL 审批流程改进建议,周五前提交书面方案。"
"处理方式正确"这四个字,是纪衡两个月来给他的第一句评价。陈稳把邮件标了星,然后取消了星标。
学习计划后面加一行:"周五,DDL 审批流程改进方案。"
九点,王磊拿着一份打印出来的代码过来,订书机订了两下,大概五十页。
"陈哥,这段状态机看不明白。结算状态从已提交到已确认,中间有个条件判断,调了另一个方法,那个方法又调了配置类。"
"状态机定义在 SettleStateMachine,转换规则在里面。条件判断读的是 SettleConfig,在 config 包。"
"配置项叫什么。"
"settle.state.transition.timeout。默认三千六百秒。"
"一个小时?"
"一个小时。"
"为什么这么长。"
"因为三年前有一次银行接口挂了四十分钟,超时短了会把正常单子判失败。"
王磊在纸上写了两行,回去了。
十四点,林晓过来问对账模块的数据源。
"settle_db 从库。延迟超过三十秒不处理。"
"你们从库的延迟一般多少。"
"三到八秒。上周四有一次到过四十二秒。"
"上周四……"林晓停了一下,"上周四我们那边跑了一次全量对账重算。"
"几点。"
"下午。具体时间我回去查。"
十六点,值班手机响。
"数据组,易文。你们系统凌晨那个告警,连接池打满,你修了。慢查询是哪个表。"
"settle_reconciliation。"
"谁创建的。"
"张毅。"
"老梁的交接人?"
"是。"
"这个表上周四建的,没有文档,没有设计评审,没有索引。"易文那边顿了一下,"张毅不会犯这种低级错误。他在运维组做过四年 DBA。"
陈稳的记事本上,删掉那半行的位置还留着一个空格。
"你这个索引加得及时。但问题是为什么没有索引就能上线。"
"DDL 审批流程没有检查索引。"
"那你打算怎么办。"
"周五前提交改进方案。"
"纪衡让你写的?"
"是。"
"写完给我看一下。"易文说,"我告诉你一件事。DDL 审批系统里,这张表的建表单我查了,是有的。索引也在建表单里写着。"
"写着什么。"
"idx_recon_merchant_date,merchant_id 加 settle_date。"易文说,"跟你凌晨加的那个一模一样。"
十九点,电梯里站着一个人,白衬衫,黑西裤,黑框眼镜。是顾源。
"你好。"
"你好。"
"值班?"
"第一天。"
"昨天凌晨那个告警我看到了。连接池打满,你加了索引,恢复了。"顾源说,"那个索引以前有。"
数字从七楼跳到六楼。
"易文刚跟我说了。建表单里写着。"
"建表单里写着,说明张毅建的时候是有的。"顾源说,"那你现在加的不是新索引。"
"是补回来的。"
"对。"顾源说,"补回来的东西,是被谁拿走的。"
电梯到四楼,停了一下,门开了,没有人上来,又关上。
"你回去查两个时间。表的创建时间,和最后一次结构变更时间。"
"然后。"
"然后查 binlog。"
一楼,顾源先出去,走了两步,回过头。
"这件事你先别写在报告里。"
"为什么。"
"因为你还不知道是谁。"顾源说,"写进去,看到报告的人里就有那个人。"
二十二点四十七分,书房。
创建到变更之间隔了四天。2026-08-01 是星期六,二十三点十四分是星期六晚上。
binlog 里那个时间点前后一分钟只有一条 DDL:
# at 4718293
#260801 23:14:42 server id 1 end_log_pos 4718451
SET TIMESTAMP=1785596082/*!*/;
ALTER TABLE `settle_reconciliation` DROP INDEX `idx_recon_merchant_date`
/*!*/;
执行账号:root@localhost。
陈稳把这一段读了三遍。第三遍他数了一下 end_log_pos 和 at 之间的差,一百五十八个字节。一条 DROP INDEX,一百五十八个字节,两千三百万行的表从此全表扫描,四十九小时以后打满连接池。
Git 日志里,2026-08-01 前后三天,没有任何涉及这张表的提交。整个仓库里搜 idx_recon_merchant_date,零个结果。DDL 审批系统里,八月一日当天没有单子,八月一日整个周末没有单子。
root@localhost 的意思是:这条语句是在数据库服务器本机上执行的,不是从应用连过来的。localhost 不带 IP。
能在数据库服务器本机上敲 root 的人,恒澜一共不多。运维组、DBA、还有极少数拿过临时授权没回收的人。今天上午林晓说过一句话:他们组有一个只读账号,配在测试环境的跳板机上,三年前配的,密码一直没换。
只读账号删不掉索引。
但跳板机能连到哪里,是另一回事。
他把 binlog 那一段导出成文件,存在本地,文件名 recon_20260801.log,放在一个叫 _notes 的目录里,这个目录不在任何仓库里。
二十三点五十二分,记事本:
"2026-08-04 23:52 settle_reconciliation 索引 idx_recon_merchant_date 被人为删除。时间 2026-08-01 23:14:42(周六)。执行账号 root@localhost。无 DDL 单,无 Git 提交。建表单里索引是有的,删的人不是建的人。需确认操作人身份。"
这一行他没有发给纪衡。周五要交的那份 DDL 审批流程改进方案里,他打算只写流程,不写这条记录。
对面那栋楼亮着灯的窗户不多。三楼亮着,五楼亮着,七楼亮着,八楼亮着,九楼亮着。他看了十一个月的那一扇,还是黑的。
零点十一分,备忘录,一行字:
"root@localhost 不带 IP。查 binlog 的 thread_id,反查连接来源。"
值班手机放在床头柜上,屏幕朝上,剩余时间七小时四十九分钟。他把手机往枕头这边挪了五厘米。
← 第八章 老梁走了 · 目录 · 第十章 胃 →