最近又遇到了一次慢查把db(mariadb10)几乎打挂的案例,作为一个核心支付系统的技术负责人,真是每日如履薄冰。因为之前支付系统经常出问题,现在各个BG对支付系统都盯得很紧。这次要不是我及时让DB给暴力清理数据,没准又提一个P2故障;
抱怨归抱怨,事后复盘,一丝都不能马虎。首先,描述一下故障的全过程。起因是我们支付系统有一个异步队列,这个队列使用的一张mysql表存储,异步回调业务线的任务(姑且表名称叫task),都会首先放这里。同时这个task表还有其他的异步任务,不同的任务使用task_type字段来进行区分。然后应用系统有一个定时任务,扫描这张表是否有待消费的任务,如果有,则会取出来进行消费;典型的生产者消费者模型;
这里的task说的再具体一点:
1、所有的异步任务都在这张表,有支付成功通知业务线消息,有给结算系统推送支付信息的任务;
2、消费者在任务处理成功后,则会把任务从task表删除。所以这张表经常是空的;
消费者根据不同的任务,调用不同的上游订单系统和结算系统。出故障时,是因为推送支付信息的结算系统接口超时,出了问题,导致任务被积压到了task表。
任务积压之后,消费者线程池很快就被积压的任务占满,导致应该通知BG订单支付成功的任务也被block住,进而影响到订单支付成功率。
当时我已经意识到,我们的消费者线程池隔离没有做到位,立刻找DBA将推送给结算系统的任务进行了备份并清理。并且嘱咐DBA定时清理推送结算任务的数据。这样才化解支付成功率继续下滑的趋势。
危机解除后,我们和DBA配合进一步排查问题,找出了事情的根本原因。
原来是推送结算信息的逻辑中,有一个对task表的查询,而这个查询的sql,没有建索引。这样当这类任务数量积压的比较多时,查询会越来越慢,慢查导致mysql堵塞。堵塞导致消费者无法拉取任务,进而影响到其他通知BG的任务的消费;我们分析了一下日志,其实我们的程序查询数量当时3分钟大概查询了1万多次,可以说qps不多。但是问题出现在sql无法命中索引,把mysql的worker thread都用完了。给我们研发的感觉,mysql是如此的脆弱,2w多条数据,查询没有索引,几千个select,就能把它打挂。
几乎类似的案例,一年前,我们也碰到过一次。当时支付系统有一个bug,用户每支付一次,都会把支付客史中一个月之前的数据都清理一下(1月1日,清12月1日之前的数据,2月1清理1月1日之前的数据)。这个bug藏的很深,这块代码也很少有业务需求,一直没有被发现。但是,是雷就会有爆炸的一天。3月1日凌晨,支付系统突然所有接口都挂了。DBA最终定位是删除支付客史的sql。这个问题,我们研发一开始是不承认的,毕竟这个sql,在线上跑了2年多,一直没有出过问题。DBA说这个delete语句删3000w数据,而且在不断的请求,数据库当然扛不住,我们反驳说,这个客史表一共才3000w数据。事后我们发现,DBA的描述有误,不是说删除3000w,而是这个delete语句没有走索引,每次要扫描3000w数据。这样才能解释通,也就是说,这个delete进行了全表扫描。而实际上,这个delete是按照时间删除的,并且时间字段是有单列索引的,但是为什么这个delete没有走索引呢?我们最后猜测,可能是因为2月份天数太少导致。以前,可能数据比较少,每次删一天,或者2天的数据,mysql可能会走索引。但是3月1日比较特殊,因2月28日删的是1月28日一天的数据,3月1日却要删除1月29,30,31三天的数据,mysql可能认为删除这么多数据,没有必要走索引了。
遇到类似问题如何解决?
1、读写分离。
2、提前消灭慢查询;
3、对异步任务做好线程隔离;
关于mysql的线程池,我最近也了解了一下,收获也不小,给大家推荐一篇好文章;
https://www.jianshu.com/p/88e606eca2a5
关注我的公众号“猿界汪汪队”,关注大并发架构实战。