“被遗忘”的杀手
在阅读本文前,读者不妨猜想,这“被遗忘的杀手” 是指什么呢? _
问题背景
在做周期购发货的时候,遇到过一个“诡异的”问题。
通常的普通发货,是一次性完成,将订单内的所有商品发货完毕后,将订单更新为已发货。而周期购订单,需要多期次的发货。只有当所有期次都发货成功后,才会将订单更新为已发货。 举例说,一个 N 期次的周期购订单, 当第 1,2,..., N-1 期次发货成功后, 订单状态仍然为 “待发货”, 仅当第 N 期发货成功后,订单状态才更新为已发货。
问题的诡异在于, 当在 QA 环境测试的时候, 一个 N (N>1) 期次的周期购订单,在第一期次成功发货完成后,就将订单状态更新为已发货。
排查过程
代码与日志
出了问题,第一想到的就是代码问题。 由于是采用了继承发货抽象类, PeriodExpress extends AbstractExpress ,覆写了其中一些方法。 我首先想到,是不是方法没有成功覆写。于是,专门将设置和更改订单状态的代码抽离出来,并在前后及覆写的方法都打印了日志,然后部署到QA环境测试,发现覆写方法的代码都执行到了, 排除了方法覆写的问题。
开发环境调试
排除方法覆写的问题后,有点困惑,难以想到究竟是哪里的代码出了问题。于是我在开发环境单步调试,毕竟在QA环境打日志分析代码执行路径太耗费时间。 可是,在开发环境下, 第一期次成功发货后,订单状态仍然是待发货,是正确的。 那很可能是QA环境的问题了。可是,究竟是哪里有问题呢?
很想直接到预发环境验证,可是,一则项目代码尚未成熟能够部署到预发环境(还要新建DDL),二则万一不是环境的问题,那么仍然是悬而未决的,况且,如果不找到根本原因,QA测试也是无法通过的。
排除干扰
想到了之前遇到过的问题 《记排查一个奇怪的数据库记录插入的问题的过程》,会不会是QA部署了另外一个发货服务工程或数据访问服务工程,它们接收到请求暗地里修改了订单状态呢? 于是,我联系测试同学,请求他们帮忙将QA环境其他的所有发货服务和数据访问服务停掉,也登陆到之前记得的机器上去查看服务状态确实停掉了。 然后,再次测试, 第一期次成功发货后,订单状态被更新为已发货。
这下, 可真是百思不得其解了。 就好像有一个幽灵般的力量, 不屈不挠地阻拦着你前进。 可是你看不到它,甚至不知道它究竟位于何处。
连续调试了近5个小时,已过22点。实在想不出还有什么原因,那就回家吧。
愿神托梦助我。
直连确认
回家后,也不太安心,还想着问题。 突然想到了,既然不能完全确认是不是另外部署的服务干扰了,那么何不采用“直连服务+注释掉那段更新为已发货的代码”的方式来彻底排除干扰?祭出最后的“杀手锏”了。 如果真是代码的问题,“注释掉那段更新为已发货的代码”,那我在指定机器上部署的发货服务工程就根本不可能更改订单状态为已发货了。突然很振奋,恨不能马上回去验证这个想法。 哼哼!这下你要原形毕露了! 不过, 还是先睡会吧!
第二天,我立马去验证昨天的想法,为了保险起见, 采用直连服务的方式,还故意将那段代码“更新为已发货”改为“更新为关闭”。如果真是我部署的发货服务更改的,那么订单在第一期次发货后,状态应该更改为“关闭”,而非“已发货”。否则,就绝对不是我部署的服务更改的。
说做就做。 我更改代码后部署到QA环境,然后重新下了个单再测试发货。 第一期次成功发货后,订单状态被更新为已发货。
懵了! 不会吧! 杀手锏都失效了!这下,真的有点没辙了! 冥冥中有一种神秘的力量,不依不饶地更改订单状态,可我就是不知道它藏在哪个角落。要是不及早找出原因解决,那么周期购项目可就要因为我这里延期啦!
灵光一现
沮丧中,我突然想到,会不会是任务脚本修改了订单状态?询问了下小组同学, 是有一个任务脚本, ExpressSubscribe, 似乎跟发货有点关系。很可能了! 欣喜地登陆QA环境的任务机,毫不迟疑地停掉了这个任务。 然后测试, 第一期次成功发货后,订单状态被更新为已发货。
要崩溃了! 还有没有其他任务会根据发货修改订单的状态?小组同学也不记得了。
地毯式搜索
快要崩溃的偶,开始对工程里更改订单状态的代码进行疯狂的地毯式搜索。在工程里 find in path , 搜索出所有的更改订单状态的代码,然后逐一查看。真是宁可错杀一千,不可放过一条漏网之鱼。 或许我早就该这么做了!
终于, 找到了另外一个重大嫌疑犯: ExpressMonitor , 订阅发货表并更新订单状态为已发货。 稍作改动并重新启动任务后, 然后测试, 第一期次成功发货后,订单状态没有被更新为已发货。 罪魁祸首确定。
如果找不到这个罪魁祸首并做相应修改,那么,即使部署到预发环境,也依然会产生错误的数据。 因此, QA 环境测试有问题,也尽量要在 QA 环境解决。
小结
现在,你明白什么是“被遗忘的”杀手了吧? _
任务脚本,通常是为了弥补系统流程不够恰当或者系统设计不够完善而做的patch。在开始运行的一段时间,它们勤勤恳恳地工作,悄悄地修补了很多本来应该通过系统设计和流程来解决的问题订单。然后,一旦存在时间足够长久,它们就逐渐被遗忘在某个角落,继续履行它们的使命。而当系统逐渐完善的过程中,它们所起的作用,就会逐渐倾向于负面影响,变成被遗忘的杀手。
当我们排查问题时,往往会忽略这些被遗忘的“劳动者”,绞尽脑汁,百思不得其解,这不就是消耗程序猿的时间和寿命的最强杀手么?
对于排查问题而言,又有什么启示呢? 代码 -> 环境 -> 任务脚本。 通常,我们第一关注的是代码问题; 其次,环境问题,也常常是排查问题的阻碍因素之一,甚至是非常隐蔽的阻碍; 而任务脚本,则是最容易被忽视的阻碍因素。 代码 -> 环境 -> 任务脚本 ,正好是一个排查范围逐渐扩大的过程,代码通常是关注所负责应用本身, 环境则是代码所运行的上下文,而任务脚本是环境中的隐藏干扰因素。 即使是代码问题, 业务代码 -> 框架代码 -> 系统底层代码,也是一个搜索范围逐渐扩大的过程。选择向哪个方向行进,有时是非常重要的,而依据则是问题的关注点。在本例中, 关注点是“更改订单状态为已发货”,如果从应用本身找不到原因,那么就从环境部署的服务寻找,如果环境服务的干扰也排除掉,那么就从所有可能更改订单状态的工程里查找和确认。
排查问题,有点像侦探办案的过程。 问题原因是隐藏的, 而你要通过一系列线索去找到它。不同的是,侦探办案通常采用的是广度优先搜索,先确定若干可能的嫌疑,再根据办案过程中的线索一个个地排除;而程序猿通常采用的是深度优先搜索,先想到最可能的嫌疑,深入探究下去,直到发现走不通,才考虑下一个出路。其实,程序猿也可以参考侦探办案的思路,结合广度优先搜索和深度优先搜索。此外,线索信息也是非常重要的。好的线索能够更快地指向目标,而坏的线索则会分散注意力。当线索非常少时,就要通过做对比、排查实验,创造更多有用的线索,从而在搜索空间中朝目标的方向前进。
May God Bless You !