第二十六章 · 路过¶
十月十六日,星期五,上午九点二十。
实时对账灰度跑完了第一批。一万二千四百条结算事件,比对结果里标出三百七十条异常。陈稳把这三百七十条导出来,抽了二十条手工核对,二十条全是对的。
不是账不对。是他的程序说账不对。
他在记事本上写了一行:误报 370/12400,抽检 20/20 均为误报。写完,把这行删了,重新写:误报率 2.98%。原因未知。
九点半,纪衡的消息进来:「灰度数据出来了吗。」
陈稳看着输入框。他可以写「出来了,异常率百分之二点九八」。这个数字是真的,纪衡也只要一个数字。
他打了七个字,删掉,重新打:「跑完了。有误报。原因还没定位。周一之前给结论。」
纪衡回了两个字:「收到。」
他把那三百七十条按商户号排序,又按对账类型排序,又按时间戳排序。三种排法,三种分布,看不出规律。他把误报的原始数据和视图数据放在两个窗口里对照,字段一个一个比。商户 ID 一致,结算金额一致,对账时间戳一致。
只有状态字段不一致。
原始数据里状态是「已结算」,视图里是「3」。四十二条字段对齐规则的第十九条写着:状态字段统一为数值型,已结算映射为 1,待结算 2,异常 3。
已结算应该是 1。视图里是 3。
他去查视图定义。视图里的映射写的是 1。
他把这个视图跑了一遍,出来的是 1。
他把实时对账的比对流程跑了一遍,出来的是 3。
同一段逻辑,两个结果。
十点十分。他打开三十七个测试用例里的第十九个,改了一行断言,重跑。绿的。他把整个测试类跑了一遍,三十七个全绿。
绿的。但线上是红的。
他往后靠了一下,椅背嘎了一声。
¶
十点四十,蒋辉端着杯子过来,停在他工位边上。
「消息服务那边我加了一个消费延迟的监控项,超过五秒告警。你看一下要不要接。」
「要。」
「你在看什么。」
「对账误报。」
「多少。」
「三百七十条。一万二千四百里的三百七十条。」
蒋辉往屏幕上凑了凑。
「都是误报?」
「抽了二十条,二十条都是误报。」
「那不是挺好的。你的程序太紧张了,比松了好。松了就是漏。」
「纪衡下周要看数据。」
「你就说误报率百分之三,正在优化。」蒋辉喝了一口,「你上次不是也这么讲的,纪衡还不是让你按计划执行。」
「他上次说我最重要的东西一个字都没讲。」
蒋辉笑了一下。
「行。那你查。」他往回走了两步,又停下,「你那个字段对齐规则四十二条,你背下来了?」
「背下来了。」
「那你比我强。我的配置项十七个,我到现在还得翻文档。」
十一点,陈稳把比对流程的日志级别调到最细。日志刷出来两万三千行。他从中间往两头找,找到十九点四十一分的那一段:
从本地缓存加载,四十二条。
他不记得自己在这里加过缓存。
他去翻提交记录。九月九日,perf: 字段映射配置加本地缓存,减少数据库查询。提交人是他自己。那天他跑了一次压测,字段映射每比对一条就查一次库,十二万次查询,四十分钟。他加了缓存,四分钟。
他盯着那行日志。缓存里是四十二条,视图里也是四十二条,数量是对的。数量对,内容就一定对吗。
十一点二十,他写了一段代码,把缓存里的四十二条打出来,和视图里的四十二条逐条比。
跑出来:四十二条全一样。
他把这段代码删了。
¶
下午两点,他重跑了一次灰度。误报四百一十二条。
比上午多了四十二条。
他把两次的误报取交集,两百八十一条重合,八十九条只在第一次出现,一百三十一条只在第二次出现。
不稳定。同一批数据,同一段代码,两次跑出两个结果。
三点,他把并发数从八调到一。误报两百九十八条。
他把并发数调到十六。误报五百六十条。
并发越高,误报越多。
他在记事本上写:并发相关。缓存相关?未证实。
他打开易文的聊天窗口,打了一句话:「实时对账误报,并发越高误报越多,怀疑缓存,但缓存内容比对是一致的。」
打完了,光标停在句末。
他想起纪衡在三楼小会议室里说的那句:你学的东西,最后是你自己的,不是她的。
他把这句话全部删掉,关掉窗口。
四点,他给自己列了一张表,四行:
一、缓存内容错了——已排除,逐条比过。 二、比对逻辑错了——三十七个测试全绿。 三、数据源错了——原始数据抽检二十条全对。 四、时序错了——待验证。
第四行他不知道怎么验证。
五点半,办公区的人陆续走了。他把并发调到三十二,跑第四次。误报一千一百零七条。
误报率百分之八点九三。
他把这个数字看了很久,然后把窗口关掉。
¶
晚上八点四十,他到家。何晚在客厅改试卷,红笔在纸上划,划得很快。
「饭在锅里。」
「嗯。」
他吃了两口,放下筷子。
「你今天不顺。」何晚说,没有抬头。
「你怎么知道。」
「你吃饭没有看手机。你顺的时候会一边吃一边刷那个日志。」
他把筷子拿起来,又夹了一口。
「有个东西查不出来。」
「多久了。」
「今天一天。」
「那才一天。」何晚把一张试卷翻过来,「我上次有个学生,一道题错了半年,我讲了三遍才知道他错在哪。不是他不会算,是他把题目里的'至少'看成了'最多'。」
「后来呢。」
「后来我发现,我讲三遍都是在讲怎么算。他的问题不在算。」
陈稳把最后一口饭咽下去。
「我明天去公司。」
「明天星期六。」
「我知道。」
何晚把红笔放下,抬起头。
「你以前不会跟我说'我明天去公司'。你以前是直接走。」
¶
星期六,公司只有值班的人。他坐了一整天,写了一个复现脚本,两百一十行,专门制造并发冲突。跑了十七次,十七次都有误报,误报数从八十九到六百三十四,跳。
星期天他没去。何晚要去她妈家,他跟着去了,坐了一下午,回来在地铁上把那张四行的表在手机上又抄了一遍。第四行还是空的。
星期一,十月十九日。他把整个比对流程拆成六段,每一段都打日志,跑一次三十二并发。日志四十一万行。他从早上九点看到下午四点,找到一段东西:
十九点四十一分零三秒,第七号线程加载字段映射,四十二条。 十九点四十一分零三秒,第十一号线程加载字段映射,四十二条。 十九点四十一分零三秒,第十一号线程写入字段映射,四十二条。
写入。
它不只是读缓存。它还在写。
他去翻代码。那段缓存的实现是他九月九日写的,十九行。第十四行有一个判断:如果缓存过期,重新从视图加载,然后覆盖。过期时间他写的是三十秒。
三十秒。并发三十二的时候,一秒钟有几百次比对。三十秒里,总有一个瞬间,几十个线程同时发现缓存过期,同时去加载,同时往里写。
写的过程中,另一批线程正在读。
读到的是一个正在被覆盖的、半旧半新的东西。
他坐在那里,把这段话在心里过了一遍。
然后他给缓存的写入加了一把锁。跑三十二并发。
误报八十九条。
降了,没有归零。
他把锁加到读上。跑。误报十二条。
十二条。
他把日志翻出来,找那十二条。十二条全部发生在同一秒——十九点四十七分零一秒。那一秒缓存刚好过期。
他把过期时间从三十秒改成三百秒。跑。误报三条。
改成三千秒。跑。误报零条。
零。他重跑一次,零。再跑一次,零。
他在记事本上写:过期时间 3000 秒,误报归零。
写完,他看着这行字,没有保存。
三千秒是五十分钟。字段映射配置改了以后,要五十分钟才生效。
这不是修好了。这是把问题往后挪了五十分钟。
他把过期时间改回三十秒。误报回到十二条。
时间是晚上十点四十。他把三十七个测试又跑了一遍,全绿。他把复现脚本跑了一遍,红的。
¶
星期二,十月二十日。他试了四种写法。
第一种,缓存加读写锁——十二条。 第二种,缓存改成不可变对象,每次整体替换——四条。 第三种,加双缓冲,一份在用一份在更新——一条。 第四种,缓存做分段,按业务线分四段——七条。
第三种最好。一条。他把这一条追出来,是在双缓冲切换的那一个指令的间隙里。
一条。一万二千四百条里的一条。
他在会议室里坐了半小时,把这四种写法画在白板上,擦掉,又画。画到第三遍的时候他发现,四种写法都在解决同一个问题:怎么让一个正在被改的东西,看起来像没被改。
十一点,他给自己订了个盒饭,边吃边看第三种写法的实现,一百七十行。他九月九日写的那个缓存是十九行。
他把一百七十行提交到自己的分支,没有合。commit message 写:wip: 字段映射缓存改双缓冲——误报 12→1,仍有残留。
下午三点,纪衡的消息进来:「周三上午周会。你讲对账进度和灰度数据。十分钟。」
他看了一下日期。明天。
他打开方案,翻到第二页,字段对齐原则,四十二条。他背了一遍,十一分钟。他把纪衡上周说的那句话在记事本上抄了一遍:重点讲数据字典的结构、字段对齐的原则、视图创建的方式。
抄完他想,如果明天纪衡问误报率,他要说一。还是要说十二。
第三种写法还没合。合了才是一。没合就是十二。
他把这个问题放着,去写周会的三张纸。第一张:数据字典结构,六个表,四十二个字段,每个字段的来源、类型、空值策略。第二张:字段对齐原则,四条原则,四十二条规则从这四条推出来。第三张:视图创建方式,三个视图,为什么是三个不是一个。
写完是晚上九点二十。他读了一遍,读了七分四十秒。
超时两分半。
他把第一张纸上的十四个字段例子删到四个。再读,六分五十秒。
他把三张纸打印出来,装进包里。
十点,他在记事本最后一行写:误报 1/12400。残留原因:双缓冲切换间隙。
¶
星期三,十月二十一日,上午八点五十。
他到得早,办公区还没什么人。他把复现脚本又跑了一次。一条。他把日志翻到那一条,那一条还在同一个位置。
九点零五分,走廊那头有人过来,脚步很慢,一步一停。
顾源。他手里拿着一个纸箱,不大,抱在胸前。
顾源在他工位边上停下来,往屏幕上瞟了一下。屏幕上是那一百七十行双缓冲的实现,右边是日志窗口,那一条误报的记录停在最上面。
顾源什么也没说,看了大概五秒。
「你试试把这个缓存去掉。」
陈稳把手从键盘上拿开。
「去掉?」
「嗯。」
「不加缓存的话,一次比对一次查库,十二万次。上次压测四十分钟。」
顾源把纸箱换了一只手抱。
「你的字段映射,一天改几次。」
「不改。上线之后就没改过。」
「那你缓存它干什么。」
顾源说完,往前走了。走了四五步,回头补了一句,声音不大:
「不改的东西,不叫缓存。叫常量。」
然后他进了电梯。电梯门合上之前,陈稳看到纸箱侧面用马克笔写了一行字,字很小,看不清写的什么。
¶
九点二十。陈稳把一百七十行双缓冲全部删掉。
他把字段映射从缓存改成服务启动时一次性加载,加载完就不动了。要改配置,重启服务。四十二条字段映射,服务启动多了零点三秒。
他跑三十二并发。
误报零条。
他重跑。零。再跑。零。跑第四次,零。第五次,零。
他把复现脚本跑了十七次。十七次全零。
他在记事本上写了一行,写完删了,重新写:
字段映射改为启动时加载,误报 12400 中 0 条。删掉缓存 19 行 + 双缓冲 170 行。净减 189 行。
他提交 MR。commit message 写:
fix: 字段映射改为启动时加载——删除运行时缓存。配置变更需重启生效。顾源指出。
九点四十,他把周会那三张纸从包里拿出来,在第三张纸的最后加了两行:
灰度数据:一万二千四百条,误报零条,误报率零。 配置变更方式:重启生效。原因:字段映射为静态配置,上线后无变更。
加完,他去追顾源。
架构组在五楼。他到的时候,顾源的工位是空的。桌面上什么都没有,只有一圈白色的印子,比杯底大一点。
旁边一个人抬起头。
「找顾老师?他今天不在。」
「他刚才还在四楼。」
「他去搬东西。他下个月退。」
陈稳站在那里。
「退休?」
「你不知道?」那人把椅子转回去,「他没跟人说。我也是上周才知道的。」
¶
十点半,三楼会议室。
陈稳讲了九分十七秒。数据字典结构,六个表四十二个字段,讲了三分半。字段对齐原则,四条原则,讲了三分钟。视图创建方式,为什么是三个不是一个,讲了两分钟。最后半分钟讲灰度数据:一万二千四百条,误报零条。
纪衡把笔记本合上。
「上周你讲了三分十一秒。今天九分十七秒。你多讲的六分钟,是什么。」
「数据字典、字段对齐原则、视图创建方式。」
「误报零条,是这周解决的?」
「是。」
「怎么解决的。」
「删掉了一段缓存。」
纪衡看着他。
「就这样?」
「我先加了锁,误报从三百七十降到十二。又改了双缓冲,降到一。后来把缓存整个删掉,降到零。」
「谁告诉你删掉的。」
「顾源。今天早上路过我工位,看了一眼屏幕。」
纪衡在笔记本上记了一下。
「顾源看了一眼,你的问题解决了。你自己查了几天。」
「四天。」
「下次遇到这种事,第一天就去问他。」纪衡把笔合上,「他下个月退休。你还有一个月。」
¶
下午两点,陈稳给研发部发了一封邮件,申请调取 DHCP 池的全量操作日志,时间范围八月一日至十月十五日,用途一栏写:「结算系统 settle_reconciliation 表索引异常删除事件溯源」。
三点二十,研发部回了:「全量日志涉及多部门测试环境,需部门负责人审批后提供。请提供审批单号。」
他把这封邮件转发给纪衡,正文写了一行:「八月一日的索引删除,需要研发部全量日志确认操作人。需要审批。」
纪衡没有回。
四点,他打开记事本,在张毅那一行下面加了一行:10.14.1.88 全量日志:审批未批,日志未拿到。
¶
晚上八点,他到家。何晚在厨房,锅里在响。
「今天呢。」
「解决了。」
「你查了四天的那个?」
「嗯。」
「怎么解决的。」
「有人路过我工位,说了一句话。」
何晚把火关小。
「一句话。」
「一句话。」
「那你查那四天,白查了?」
陈稳把外套挂上。
「他说的那句话,我三天前听不懂。」
何晚把菜盛出来,端到桌上。他坐下,夹了一口。
「他要退休了。下个月。」
「你认识他多久。」
「今年七月见过一次。一共说过四次话。」
「四次话。」何晚把碗放在他面前,「那你下个月之前,还能说几次。」
他没有算。
饭吃完,他洗了碗,回房间打开电脑。字段映射的 MR 已经被合了,reviewer 是那个资深工程师,回复两个字:「可以。」
他打开记事本,写了三行:
十月二十一日。误报零条。删了一百八十九行代码。 顾源下个月退休。他没有跟人说。 周会九分十七秒。纪衡说,下次第一天就去问他。
写完他保存,关掉。
他打开抽屉,找了半天,找到一张纸,是七月十六日顾源在食堂餐盘边上给他画的那个跨天时间窗的示意图,两条横线,一个箭头。纸角有一块油印子。
他把这张纸夹进方案的第一页。
窗外有人在楼下停车,车灯扫过天花板,扫到那道裂缝上,停了一下,灭了。
对面那扇窗的灯,还是黑的。
← 第二十五章 三分钟 · 目录 · 第二十七章 84% →