背景
最近发生了一起“批量发货阻塞”的线上问题,值得深思和记录。
批量发货的流程是:商家上传一个里面可能含有一万多的订单号及物流发货信息的发货文件,然后PHP后台接收和解析文件,提取出待发货的信息,每5000个订单号发货信息打包成一个消息后批量推送到NSQ消息队列中。后台PHP脚本从NSQ消息队列中取出打包的发货消息,打散成单个的发货消息,然后循环调用Java发货服务化接口完成批量发货。
这里采用5000个订单号发货信息打包一个消息批量推送,是考虑到前端超时限制,采用其他数量总有偶发的前端超时,导致无法给出正确的提示。而为了简单起见,PHP采用了同步串行的方式。
事件
昨天,有客满同学报商家批量发货发不了货了。我意识到消息可能积压了。马上到NSQ平台查看,果然消息有堆积的现象。紧急联系NSQ同学帮忙查看,NSQ同学的答复是:消费客户端超时。
开始并不清楚究竟是什么原因,先采用加发货服务的机器、启动更多PHP进程来缓解问题,但是没有效果。 后来直接与NSQ交流知道,NSQ会对消费端进行计时,如果在指定时间内没有消费完数据,就会超时断链然后重发消息。5000个发货消息,每个消息处理100ms,也要 500s = 8min+,因此NSQ会反复断链然后重试,而PHP进程有限,导致消费端始终堆积。
最后采取的措施:先停掉NSQchannel,备份其中的消息数据;然后清空channel,手动对NSQchannel中的指定数据进行消费。
对NSQchannel中的指定数据进行分析后发现,里面有1700+订单连续重复发货了360+ 多次。联系商家说是,周期购发货需要一次性把这么多期全部发货完毕,客服点得手都累了。囧~~ 至此,原因基本清楚了: 一位周期购商家短时间内发货了600000+,而后台PHP脚本是循环调用服务化接口,没有那么大的吞吐量及时处理这么多的发货请求,导致消息一直超时、重试、堆积,形成恶性循环。
教训
对NSQ消息消费的技术细节不深入,消息处理代码在大批量发货请求下有BUG,导致消息超时、重试、堆积; 后来采用每隔一段时间传一次心跳,保持NSQ连接。启示:深入技术细节,知其所以然。
后台采用PHP循环调用请求,吞吐量低,在超大量并发发货的极端情形下无法应对, 系统薄弱点。改进方法: 直接在Java端多线程消费发货消息,正确处理消息ACK。依次类推,我们应当居安思危,多多深挖系统的薄弱点:单点、单线程、吞吐量低、无业务幂等、弱安全性、循环多次调用同一接口、无线程池控制的独立线程等。