本周一个故障,记录一下。
诸位知道,历史遗留代码,只要能运行,一般都不愿意去动的。 然而, 遗留问题就像雷区,排雷会炸,不排也会炸。 这不,本周一个电子卡券导出的雷,炸得我真是“面目全非”。
具体代码不必细讲。 看官只需懂得:电子卡券导出是一个PHP后台任务,接收消息队列的导出消息来处理任务。 在捕获了一个异常之后,走到了异常处理分支。异常分支有句代码 logger->warn 。 这有何错 ? logger 并没有 warn 方法 ! IDE 也提示的。 于是 PHP 报 fatal error ,直接退出了进程。 这样,导出任务进程无法启动,无法处理消息,队列阻塞,前端就一直显示报表正在生成中。
为什么 logger 没有 warn 方法 , 却还明显地写着 logger->warn ,甚至历经两年都没有发现 ? 想必部分读者已经了然:因为写在异常捕获中。异常测试,是很容易被忽略的分支。
值得提及的是,logger 有个 warning 的方法。 然而,开发者习惯用 warn 而不是 warning ,并且 warn 是动词,warning 是名词。 打日志本应该用 warn 而非 warning 。 底层 API 设计者希望用新的方式取代老的方式,也必须考虑到大多数开发者的惯用法。
另一个问题是: 电子卡券导出为什么处理这么慢?
进一步阅读代码可知,电子卡券导出代码,里层竟然对每一个订单循环进行处理:访问 DB 获取卡券信息,调用用户 API 接口,调用重量级的订单详情接口。环环相扣,真是令人“拍案叫绝”啊!
为什么会出现这种情况 ? 原来,为了方便,导出代码复用了列表代码。而列表代码只需要处理 20 个订单, 但导出代码却会处理数万订单。可想其性能可以低到地心里面去。推测, 可能当时修改列表代码的时候,并没有注意到导出复用了列表代码,只是在列表代码里修改了,但同时导出的处理也影响到了,而修改者浑然不觉。
调用重量级的订单详情接口,也值得商榷。 列表与导出要显示的信息不过七八个,却要调用订单详情接口,获取近百个字段。真是为了一个轮胎,拖了一辆卡车。须知,这事并不鲜见。注意到,导出外层批量调用了一次新的批量详情接口,却不料里层为了订单列表又循环对每个订单调用了老的订单详情接口,令人以泪洗面!
这里有三个启示:
- 对于大量数据,循环调用单个接口,乃开发之忌。列表如果这么做,一定会有一天跪掉;导出如果这么做,要么会拉跨被调用的业务方,要么自己慢到比蜗牛还慢。
- 心里要有全景图,能评估影响范围(极为重要!),不然,很容易因为一个改动影响到另一个地方。此是开发常理。
- 不要为了一个轮胎,拖一个卡车。不要轻率地复用一个大的业务模块。如果要复用,请通读这个业务模块,并保证复用的模块是轻量级的。
处理一个故障,可以折腾很多时间。比如 “趴坟”啊,开会啊,每一个细小的地方,一定要分析得水落石出,令人心服口服,无言以对,—— 哪怕这坨代码再也不会在线上运行。 有这时间,可以多去发现和思考系统中的潜伏着的问题,根治之。
PS: 无意看到历史上的今天,发了一篇“代码问题及对策” ,里面早已谈到了这些问题。 后人哀之而不鉴之,亦使后人复哀后人矣。