裴衍之沉默了一会儿。烤架那边传来滋滋的声音,是油脂滴在炭火上的声音,混著烟和肉香一起飘过来。有人在笑,有人在抢最后一根玉米,有人喊“给我留一口”。
“是你教我的。”他说。
程司白的手指在矿泉水瓶上紧了一下。瓶身的塑料在她手里发出一声轻微的声响,脆的,短促的,像一个被打断的思绪。
“三年前你说我不够理性。”裴衍之的声音很轻,轻到像是在自言自语,“你说我们不合适。我当时不知道你为什么这么说。后来我想了很久,想明白了一件事——你不喜欢的不是我,是我让你觉得你需要我。”
程司白没有说话。
“所以我让自己变得更理性。这样就算你不需要我,我也能自己处理好。”他把目光从她脸上移开,转向烤架的方向,“你说得对,我变了。变成了你以前说想要的那种人。”
程司白坐在长椅上,手里的矿泉水瓶被她握得有点变形了。阳光透过树叶照下来,光斑在她身上移动,从肩膀移到了手臂上,从手臂移到了手背上。她的手背上有一小片圆形的光斑,亮的,暖的,像一个小小的信号灯。
烤架那边的喧闹声还在继续。何悦端著一盘烤好的牛肉走过来,放在程司白面前。“程姐你吃点,我这次没有烤焦。”她看了一眼坐在旁边的裴衍之,又看了一眼程司白手里被捏变形的矿泉水瓶,什么都没有说,转身走了。
程司白低头看著那盘牛肉。烤得还不错,表面有一层均匀的焦褐色,撒了孜然和辣椒粉,闻起来很香。但她没有胃口。
“裴衍之。”她说。
“嗯。”
“但我不确定我喜欢理性的你。”
这句话说出来的时候,她的声音很平,平得像在念一段程式码。但裴衍之听到了这句台词后面的东西——就像她听到他说“我一直愿意”的时候,也听到了台词后面的东西一样。
裴衍之转头看著她。阳光照在她脸上,把她下巴的线条照得很柔和。她的表情很平静,但她的眼睛不是。她的眼睛里有什么东西在动,很慢,很小心,像一个在边界上试探的程序——不知道会不会崩溃,但它还在运行。
“那你想喜欢什么样的我?”他问。
程司白没有回答。她拿起一块牛肉放进嘴里,嚼了嚼,吞下去了。孜然的味道在舌尖上散开,有点咸,有点辣,还有一点她说不清楚的东西。
“我不知道。”她说。
这是实话。她真的不知道。她只知道她不确定。不确定自己喜欢理性的他,不确定自己还喜不喜欢三年前那个感性的他,不确定自己到底喜不喜欢他——这个问题她从来没有认真想过,因为她一直告诉自己不需要想。
但现在她需要想了。
因为他坐在她旁边,阳光在他脸上留下光斑,他的手放在膝盖上,手指微微张开,掌心朝上——那是一个开放的姿势,不设防,不防御,不保护自己。
他在等她。
不是等一个答案,是等她愿意开始想。
下午四点,团建结束。大巴车开回公司楼下,同事们陆续下车,各自散去。程司白站在公司门口等计程车,何悦跑过来说“程姐我先走了”,然后拉著陆维安快步离开了。她的脚步很快,快到程司白觉得她是在故意制造一个空档。
裴衍之从后面走过来,站在她旁边。
“我送你吧。顺路。”
程司白看了他一眼。她知道不顺路。他住在公司北边,她住在东边,完全相反的两个方向。
“好。”她说。
车子在路上行驶,窗外的路灯一盏一盏地往后退。程司白坐在副驾驶的位置上,手里拿著那瓶还没有喝完的矿泉水。车厢里很安静,没有开音乐,只有发动机的声音和风噪。
“裴衍之。”她说。
“嗯。”
“你刚才问我,想喜欢什么样的你。”
“嗯。”
“我不知道。”她看著车窗外的路灯,光线在她的脸上明灭交替,像一个在变化的信号,“但我觉得,你可能不需要为我变成另一个人。”
裴衍之的手指在方向盘上紧了一下。只有一下,然后就松开了。
“你三年前说我不够理性。”
“我三年前说了很多错的话。”程司白转头看著他,车厢里很暗,只有仪表盘的光照在他脸上,把他的表情照得很模糊,“但有一句话没有说错。”
“什么?”
“我们不合适。”
车子停下来了。不是因为到了目的地,是因为红灯。车厢里很安静,安静到她能听见自己的心跳声。一下一下的,不快,但很重。
“我们不合适。”她重复了一遍,“但如果我们都想试试,不合适也没关系。”
绿灯亮了。裴衍之没有踩油门。他坐在驾驶座上,手握著方向盘,看著前方的红绿灯变成了绿色,又变成了黄色,又变成了红色。
“程司白。”他说。
“嗯。”
“你刚才说的那句话,我可以存下来吗?”
程司白愣了一下。“存下来?”
“嗯。存下来,当作证据。以后你不承认的时候,我可以拿出来给你看。”
程司白看著他。仪表盘的光照在他脸上,她看到了他的表情——嘴角微微上扬,不是笑,是一种放松,一种只有在确信什么东西终于被确认之后才会出现的放松。
“你变了。”她说。
“哪里变了?”
“以前你不会说这种话。”
裴衍之踩下油门,车子缓缓启动。窗外的路灯又开始往后退了,一盏一盏的,连成一条线。
“你说你不确定喜欢理性的我。”他说,“所以我在试试,变回去一点。”
程司白转头看向车窗外。玻璃窗上倒映著她的脸——头发被风吹乱了,外套的领口竖起来,下巴埋在里面。她的嘴角有一点点上扬的弧度,很小,小到如果不是车窗上的倒影,她自己都不会发现。
她在笑。
她已经很久没有笑过了。
凌晨一点四十七分,程司白的手机在床头柜上震动了。不是来电,是监控系统的报警推送。她睁开眼睛的瞬间就清醒了,像被一个信号唤醒的节点——没有开机时间,没有加载过程,直接进入运行状态。她拿起手机,萤幕上的报警信息只有一行字:“灰度上线失败。数据不一致率超过阈值。”
她从床上坐起来,手指已经在解锁手机了。打开监控面板的瞬间,红色的报警标记像溃疡一样布满了整个萤幕。Raft方案的曲线在凌晨一点二十分开始出现剧烈波动,延迟从两百毫秒飙升到一千两百毫秒,然后直接断开了。Paxos方案还在运行,但数据不一致率在持续上升,从百分之零点零一涨到了百分之零点五,还在涨。
她没有犹豫,直接拨了何悦的电话。响了两声接了,何悦的声音听起来也是刚醒,带著浓重的鼻音。“程姐?”
“灰度失败了。Raft断了,Paxos数据不一致。通知所有人上线,我在路上了。”
她挂了电话,换衣服的时候手指在扣子上停了一下。不是因为困,是因为她在想一件事——Raft方案是她负责的,架构层是她设计的,测试是她跑的,数据是她确认的。现在它断了。不是别人的问题,是她的问题。
她把扣子扣好,拿起手机和钥匙,出门。
凌晨两点的街道很空,计程车开得很快,二十一分钟就到公司了。程司白冲进大楼的时候电梯正好在一楼,她按了十五楼,电梯上升的过程中她一直在看手机上的监控画面。Raft方案完全没有恢复的迹象,Paxos的数据不一致率已经涨到了百分之一点二。她的手指在萤幕上划来划去,查看每一条日志、每一个指标、每一段曲线。所有的数据都指向同一个结论——问题出在架构层。
电梯门打开的时候,走廊上的感应灯全亮了。办公区里只有几个人在,何悦坐在她的工位上,正在打电话,表情严肃。陆维安站在何悦旁边,手里拿著一杯咖啡,脸色很差。他看到程司白的时候点了点头,没有说话。
程司白走到自己的工位前,打开电脑。萤幕亮起来的瞬间,她看到了监控面板上那条彻底断开的蓝色曲线——Raft方案的最后一个数据点停留在凌晨一点二十三分十五秒,之后就是一片空白。她点开日志系统,开始从头排查。
何悦挂了电话走过来。“程姐,裴哥在路上了。运维那边说硬件没有问题,网络也没有问题。应该是软件层的故障。”
“我知道。”程司白的眼睛没有离开萤幕,“Raft的领导者节点在凌晨一点二十分的时候失去了与所有跟随者的心跳连接,然后开始重新选举。选举进行了三轮,每一轮都失败了。第四轮的时候系统直接崩溃了。”
“为什么会选举失败?”
“不知道。我在看日志。”
日志在萤幕上一行一行地滚动,每一行都是一个时间戳、一个节点ID、一条状态信息。程司白从凌晨一点二十分开始看,一条一条地看,像一个在迷宫里找出口的人——每一条走廊都看起来一样,每一扇门都可能是对的也可能是错的。她的眼睛越来越干涩,但她没有眨眼。
裴衍之到的时候是凌晨两点半。他穿著一件皱巴巴的T恤,头发没有整理,眼睛下面有两道很深的阴影。他走到程司白身后,没有说话,只是看著萤幕上的日志。
程司白感觉到他站在后面了。她没有转头,也没有说话。她的手指继续在键盘上敲,日志继续滚动,从一点二十分看到一点二十三分,又从一点二十三分看到一点二十分,来回看了三遍,没有找到异常。
“日志看起来一切正常。”她说。声音比她预期中要哑,像是嗓子被什么东西堵住了。
裴衍之拉了一把椅子坐在她旁边,打开自己的电脑,开始看Paxos方案的数据。“Paxos这边的数据不一致是Raft崩溃之后开始的。Raft崩溃的时候,一致性哈希把所有的流量都切到了Paxos上,流量瞬间翻倍,Paxos扛不住了。”
“所以根本问题还是在Raft。”
“根本问题是为什么Raft会崩。”裴衍之的声音很平静,平静得像在讨论一个与他无关的技术问题,“你继续看架构层的日志,我看业务层的。可能是业务层的输入触发了架构层的边缘条件。”
程司白没有回应。她的手指在键盘上继续敲,日志继续滚动,但她的大脑已经不在那个频道上了。她听到了一个声音——不是外界的声音,是她脑子里的声音。那个声音在说:是你。是你的问题。你的方案有Bug。你的架构不稳定。你不够好。
她用力咬了一下嘴唇内侧,痛感让她清醒了一点。她把注意力重新集中在萤幕上,从头开始看日志。一点二十分,节点三发送心跳。一点二十分零三秒,节点一没有回应。一点二十分零五秒,节点二没有回应。一点二十分零八秒,节点四没有回应。一点二十分十秒,节点三开始领导者选举。一点二十分十二秒,节点三发送选举请求。一点二十分十三秒,节点一拒绝。一点二十分十四秒,节点二拒绝。一点二十分十五秒,节点四拒绝。一点二十分十六秒,节点三重新发送选举请求。一点二十分十八秒,节点一拒绝。一点二十分十九秒,节点二拒绝。一点二十分二十秒,节点四拒绝。
她盯著这些拒绝记录,脑子里那个声音更大了。你不够好。你不够好。你不够好。她把目光从萤幕上移开,闭了一下眼睛。眼皮很重,重得像被什么东西压住了。不是因为困,是因为累。一种从骨头里渗出来的、怎么睡都补不回来的累。
“程司白。”裴衍之的声音从旁边传来,很近,近到她能感觉到他的呼吸,“你找到什么了吗?”
“没有。日志看起来一切正常。节点三发送选举请求,其他节点拒绝。没有原因,就是拒绝。”她的声音在最后两个字的时候抖了一下。只有一下,然后就恢复了平稳。
裴衍之沉默了几秒。办公区里很安静,只有键盘的声音和空调的嗡嗡声。何悦和陆维安在另一边讨论Paxos的数据不一致问题,声音压得很低,像在图书馆里说话。
“节点拒绝选举请求,只有两种可能。”裴衍之说,“要么是请求本身有问题,要么是节点状态有问题。你检查过节点的状态机了吗?”
“检查了。状态机正常,日志索引一致,任期号一致。”
“那请求本身呢?节点三发送的选举请求里,任期号是多少?”
程司白翻到日志的对应行。“任期号是七。”
“其他节点的任期号呢?”
“也是七。”
“那为什么拒绝?”
“我不知道。”
这三个字说出来的时候,程司白觉得自己的喉咙被什么东西卡住了。她是一个架构师,她的工作就是知道。知道系统怎么运行的,知道故障怎么发生的,知道问题怎么解决的。她不知道的时候,就没有人知道了。她是架构层的负责人,她是最后一道防线。如果她不知道,那道防线就破了。
她盯著萤幕上那几行拒绝记录,手指放在键盘上,没有敲。日志在她眼前变得模糊了,不是因为萤幕的问题,是因为她的眼睛里有什么东西在聚集。她用力睁大眼睛,不让那些东西落下来。
裴衍之站起来,走到她身后,弯下腰看著萤幕。他的肩膀离她很近,近到她能感觉到他的体温。那股柠檬和雪松的味道从他衣服上散发出来,很淡,淡到几乎闻不到,但她闻到了。
“从头推演一遍。”他说,“不要看日志,推演算法。”
程司白闭了一下眼睛,把那点模糊的东西压回去。她打开一个空白的编辑器,开始写推演的步骤。
“Raft算法的领导者选举规则:候选人发送选举请求,请求里包含自己的任期号和最后一条日志的索引。其他节点收到请求之后,比较请求中的任期号和自己的任期号。如果请求中的任期号更小,拒绝。如果更大,同意。如果相等,比较日志索引。如果请求中的日志索引大于等于自己的日志索引,同意。否则,拒绝。”
她一边说一边在编辑器里写,把规则一条一条地列出来,像在写技术文档。字迹工整,逻辑清晰,每一个条件分支都写得很清楚。
“节点三的任期号是七,其他节点的任期号也是七,所以任期号相等。接下来比较日志索引。节点三的最后一条日志索引是多少?”
她翻到日志里的那一行。“一万两千三百四十五。”
“其他节点的呢?”
“也是一万两千三百四十五。”
“日志索引相等,所以其他节点应该同意节点三的选举请求。但它们拒绝了。”裴衍之的声音还是很平静,但程司白听到了他声音底下那层东西——不是疑惑,是某种正在成型的确定。“所以问题不在规则里,在规则之外。”
程司白的手指在键盘上停了一下。“规则之外?”
“规则之外只有一种可能。”裴衍之站直身体,双手交叉在胸前,“节点三发送的选举请求里,日志索引不是一万两千三百四十五。”
“日志里写的是一万两千三百四十五。”
“日志里写的是节点三认为自己发送的值。但实际发送的值可能不一样。”
程司白愣住了。她想到了那个可能性——序列化错误。节点三在构造选举请求的时候,把日志索引从整数类型转换为字节流的时候出了错。整数一万两千三百四十五转换为字节流,再转换回来的时候,可能变成另一个数字。如果变成了一万两千三百四十四,其他节点比较的时候发现请求中的日志索引比自己的小,就会拒绝。
她开始检查序列化的代码。一行一行地看,一个函数一个函数地看。看到第三个文件的时候,她找到了。
一个边界条件没有处理。
整数转字节流的函数在处理大于一万的数字时,会丢失最低位元组。一万两千三百四十五转换之后再转换回来,变成一万两千三百四十四。差了一位。就差了一位。
她的手指停在那一行代码上。这行代码是她写的。三个月前,她在优化序列化性能的时候重写了这个函数,为了减少内存分配,她手动实现了字节转换。她测试了正数、负数、零、边界值,但她没有测试一万以上的数字。她以为那个范围是安全的。
她错了。
“是我写的。”她的声音很轻,轻到像是在自言自语。裴衍之没有说话。他站在她旁边,低头看著萤幕上那行代码。
“三个月前我重写了这个函数。我没有测试到位。这个Bug是我的。”她一个字一个字地说,每一个字都说得很清楚,清楚得像在念一份事故报告。
办公区里很安静。何悦和陆维安不知道什么时候停止了讨论,两个人站在不远处,看著程司白的方向。何悦的表情很担心,她想走过来,但陆维安拉住了她,摇了摇头。
程司白靠在椅背上,看著天花板。那盏旧灯管还是那样,光线不均匀,中间亮两边暗。她盯著那盏灯,觉得自己就像那根灯管——看起来还在亮,但里面已经坏了。有一段灯丝断了,电流过不去,光线不均匀,随时会彻底灭掉。
“我重写那个函数的时候,觉得自己很聪明。”她说,声音在空荡荡的办公区里回荡,像一段没有回声的录音,“手动实现字节转换,减少内存分配,提升性能。我觉得这才是架构师该做的事——优化,改进,追求极致。但我连基本的边界测试都没有做。”
她停了一下。喉咙里那个卡住的东西更大了,大到她几乎说不出话。
“我可能不适合做架构师。”
这句话说出来的时候,何悦忍不住了。她走过来,站在程司白面前。“程姐,你不要这么说。谁都会犯错——”
“你不懂。”程司白打断她,声音比之前更哑了,“这不是犯错。这是失职。我是架构组的负责人,我的代码跑在核心业务上。如果我连基本的测试都做不好,我不配坐在这个位置上。”
何悦张了张嘴,不知道该说什么。她转头看向裴衍之。
裴衍之坐在程司白旁边的椅子上,没有说话。他的表情很平静,平静得像一面没有波澜的湖。他看著程司白,看了大概十秒,然后打开自己的电脑,找到那段序列化的代码,开始看。
“这个函数是你三个月前写的。”他说。
“对。”
“当时你在做什么项目?”
“架构组的性能优化专案。目标是把序列化耗时降低百分之三十。”
“你做到了吗?”
程司白转头看著他。他的表情还是很平静,平静得像在问一个技术问题。但她的眼睛里有什么东西在动,不是眼泪,是某种被压了很久之后开始松动的东西。
“做到了。降低了百分之三十四。”
“那你没有失职。你完成了目标。这个Bug是边界条件没有覆盖到,不是逻辑错误。边界条件遗漏在复杂系统里是正常的,不是失职。”
“正常?”程司白的声音忽然提高了半度,在安静的办公区里显得很刺耳,“灰度上线失败了,数据不一致了,系统崩了。你告诉我这是正常的?”
“我说的是边界条件遗漏是正常的。不是说故障是正常的。”裴衍之的声音还是很平,平得像在解释一个技术概念,“你写了一个性能很好的函数,它跑了一万两千次都没有问题。第一万两千三百四十五次出问题了。这说明你的测试覆盖率不够,需要补充。不是你不配做架构师。”
程司白看著他。她的嘴唇在抖,不是冷,是某种她控制不住的东西。她想说什么,但喉咙里那个卡住的东西太大了,大到她一个字都说不出来。
裴衍之站起来,走到她面前,蹲下来。他的眼睛和她平视,很近,近到她能看清他瞳孔的颜色——深棕色的,在灯光下泛著一点点暖光。
“程司白,你不是神。你不需要完美。”
这句话说出来的时候,程司白觉得那个卡在喉咙里的东西碎了。不是消失了,是碎了,碎成很多小块,每一块都在扎她的喉咙,扎得她生疼。她的眼眶热了,视线模糊了,那些被她压了一整个晚上的东西终于压不住了。
她没有哭。她只是坐在那里,眼睛里有东西在闪,嘴唇在抖,手指攥著键盘边缘,指节发白。
“裴衍之。”她说。声音很哑,哑到不像她自己的声音。
“嗯。”
“我其实不是不相信共识算法。”
裴衍之蹲在她面前,没有动。他的手放在膝盖上,手指微微张开,掌心朝上。那是一个开放的姿势——不设防,不防御,不保护自己。
“我不相信的是——”她的声音断了一下,像一段被中断的传输,信号丢失了,数据不完整,“有人会一直选择我。”
办公区里很安静。何悦站在不远处,手摀住了嘴。陆维安低下了头。空调的嗡嗡声在这一刻变得特别响,像一段填满了所有空白频率的白噪音,没有旋律,没有节奏,只有持续的、不变的、压倒一切的声音。
裴衍之沉默了很久。
这很久里,程司白看著他的眼睛。那双深棕色的眼睛在灯光下很亮,亮得像萤幕上一个稳定的信号——没有波动,没有异常,一切正常。他在看她,没有移开目光,没有闪躲,没有逃避。他只是看著她,像一个在接收数据的节点——信号来了,他收到了,他在处理。
他伸出手,握住了她的手。
她的手很凉,他的手很暖。他的手指穿过她的指缝,扣住了她的手背。力度不重,但很稳,稳到像一个被确认了的连接——握手成功,协议协商完成,数据传输通道已建立。
“程司白。”他的声音很轻,轻到只有她能听见,“你知道共识算法的前提是什么吗?”
她摇了摇头。手指在他掌心里动了一下,没有抽出来。
“是节点之间要互相信任,愿意交换信息。”他看著她的眼睛,一个字一个字地说,“你从来不跟我交换信息。”
程司白觉得自己的心脏被什么东西击中了。不是痛,是一种她说不清楚的感觉——像一个被她关了很久的门被打开了,光从门缝里照进来,刺眼,但她不想把门关上。
她的手指在他掌心里收紧了。不是用力,是回应。像一个节点收到了另一个节点的确认信号之后,发回的那个ACK——收到,理解,同意。
裴衍之没有再说话。他蹲在她面前,握著她的手,掌心贴著掌心,手指扣著手指。两个人的心跳在不同的频率上运行,但通道是开的,数据在传输,共识在达成。
窗外的天开始亮了。不是那种突然的亮,是一种缓慢的、渐进的亮——从深蓝变成浅蓝,从浅蓝变成灰白,从灰白变成一层薄薄的橘色。太阳还没有出来,但光已经到了。光照在办公区的玻璃窗上,照在桌上那个白色的马克杯上,照在两个人交握的手上。
程司白看著窗外的那层橘色,觉得自己像一个刚刚从崩溃中恢复的系统——日志里还有警告,内存里还有碎片,但核心进程已经重新启动了,心跳恢复了,连接重建了,系统在慢慢地、一点一点地回到正常状态。
她没有说话。她也没有放手。
Bug修复之后,程司白没有走。
她把修复后的代码提交了,把测试用例补充了,把边界条件从一万扩大到十万,跑了一遍又一遍。绿色的通过标记在萤幕上跳出来的时候,窗外的天已经从深蓝变成了浅蓝。办公区里只剩她一个人——何悦在凌晨五点的时候被她劝回去了,陆维安也走了,裴衍之去了茶水间。
她靠在椅背上,看著萤幕上那一排绿色的“PASS”。眼睛很干,干到眨眼睛的时候会痛。但她不想闭上。闭上就会看到那行代码——三个月前她写的,她觉得很聪明的,她没有测试到位的那一行。一行代码,让整个系统崩了。一行代码,让她发现自己没有自己以为的那么好。
裴衍之端著两杯水走进来。一杯放在她桌上,一杯自己拿著。他没有说话,只是拉了一把椅子坐在她旁边,和她并排面对著萤幕。
两个人沉默了很久。