跳转至

第九章 · 值班

八月三日,七点五十一分。值班第一天,第一通电话比签到早了七分钟。

"你好,结算组值班,陈稳。"

"运营部的。商户反馈对账数据对不上,差两千三。你们那边是不是有问题?"

"商户编号。"

"M202-4437-1。"

结算流水表里,这个商户最近一笔是凌晨三点四十七分,四万三千二百,状态已结算。运营那边说商户后台显示四万零九百,差两千三百。

再往下翻两行,还有一笔四万零九百的,结算时间三点四十九分,晚了两分钟,类型 MANUAL_SETTLE。人工补结。

"你们系统里有两笔。"陈稳说,"三点四十七分自动结算,三点四十九分人工补结。商户看到的是后一笔。"

"为什么会补两次。"

"第一笔金额四万三千二,第二笔四万零九百。差额两千三,等于一笔退款。你去问商户,结算日当天有没有退过货。"

"补结是谁做的。"

经办人字段里是 zhang.yi。

"技术支持中心的人。"陈稳说。

"名字。"

"张毅。"

"那我找他。"

"你找他之前先问商户。"陈稳说,"如果真有退款,就不是系统的问题。"

电话挂断。七点五十九分,值班系统弹签到确认,剩余时间二十三小时五十九分钟。

八点十四分,纪衡的邮件,昨晚十一点四十七分发的,标题"八月第一周工作安排"。正文四行:本周值班陈稳;三个转岗同事今日到岗,由陈稳带;每天下班前发一份带人进度;拦截器全量按计划推进。

第三行没有商量的余地,也没有工时预算。陈稳把这一行抄进记事本,后面画了个括号,写"每天 30 分钟"。他自己名下现在十五个工单,其中六个是老梁的。

九点三十分,工位区中间站了三个人,一男两女,背着包。男的戴圆框眼镜,二十三四岁,手里拿一份打印文件。

"请问结算组是在这里吗?"

"是。"

"我们是转岗过来的,今天报到。"

老梁的位置和旁边两个空位已经换了新显示器,键盘鼠标还没拆封。男的叫王磊,短发戴圆框眼镜的女生叫林晓,扎马尾的叫张悦。

"我在营销技术部做了三年后端。"林晓说,"主要做对账模块。"

"对账哪一块。"

"商户侧的差异明细,还有中间表的数据核对。"

"你们查生产数据用什么账号。"

"我们组有一个只读账号,配在测试环境的跳板机上。三年前配的,一直没换过密码。"林晓说,"怎么了?"

"没事。先装环境。JDK 十七,Maven,Git,IDE。有问题问我。"

张悦是今年六月的应届生,电脑上开着一个 Git 教程页面,停在第十步。

九点四十一分,值班手机又响。这次是运维组,问周一有没有人改营销结算适配器的配置。陈稳说没有。对方说日志里看到一次配置热加载。

"什么时间。"

"上周五晚上十一点多。"

"上周五我不在值班。"

"那就是别人。行,我记一下。"

十三点整,告警推送:结算队列积压超过阈值,当前 1,847 条,阈值 1,500 条。

积压线在缓慢上升,每分钟三十到四十条。十三点不是结算高峰。日志里请求来源是营销系统的促销活动结算,昨天上线的活动,今天开始产生结算请求,量超出预期两倍。

营销结算 adapter 的配置文件里有一行,被注释掉了:

# marketing.settle.throttle.enabled=false

注释符号后面的值是 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。"

一楼,顾源先出去,走了两步,回过头。

"这件事你先别写在报告里。"

"为什么。"

"因为你还不知道是谁。"顾源说,"写进去,看到报告的人里就有那个人。"

二十二点四十七分,书房。

CREATE TIME: 2026-07-28 16:32:17
UPDATE TIME: 2026-08-01 23:14:42

创建到变更之间隔了四天。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,反查连接来源。"

值班手机放在床头柜上,屏幕朝上,剩余时间七小时四十九分钟。他把手机往枕头这边挪了五厘米。


← 第八章 老梁走了 · 目录 · 第十章 胃 →