一个由二级索引引发的P1惨案

前言

前不久在实习的时候搞了个P1故障,导致服务不可用将近一小时,最后排查复盘发现竟然只是一个二级索引!

一、业务背景

5000w+数据,1024分表

中午12点准时推单,一般推单会持续15min-30min,该业务为核心业务,一旦受阻影响面很大。

二、故障出现

业务侧反馈中午推单卡住,一单未推(该业务为关键业务,很紧急,影响面大)。

上阿里云日志看到

 Error updating database. 
 Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: [15b848918060c000-2][10.0.62.142:3306][booking_center]ERR-CODE: [TDDL-4614][ERR_EXECUTE_ON_MYSQL] 
 Error occurs when execute on GROUP 'BOOKING_CENTER_1617764186004XHSA_PURM_0001' ATOM 'rm-bp17rbdzegw1600mp_booking_center_nmqq_0001': Lock wait timeout exceeded; try restarting transaction

报警出现大量锁等待超时

同时钉钉群里有大量长事务告警

三、故障定位&&止血

12:19 初步定位

看到报警内容,可以定位到是数据库长事务导致新的语句无法执行。此时确定是数据库的问题,开始联系dba解决。

由于是饭点,其他开发和dba都去吃午饭了,都匆匆赶回公司。

12:38 dba到场

看到故障的第一反应是kill掉所有的长事务,但是尝试kill发现并不能成功kill。

回顾最近的修改,只有昨天五点左右删除了相关表的一个二级索引。我们推测和这个有关,所以建议执行增加二级索引的DDL操作。

12:43 执行后发现该索引一直在pending,没有执行,猜测是长事务霸占了行锁。开始联系阿里云技术人员支持。

此时所有update都被阻塞。

我们找到了产生长事务的sql

<!-- 经过脱敏的代码 -->
    <update id="***">
        <foreach collection="***" item="i" open="" close="" index="index" separator=";">
            update bh_order_line
            <set>
                <if test="i.a!=null">
                    a=#{i.a},
                </if>
                <if test="i.b!=null">
                    b=#{i.replaceProductInfo,typeHandler=...},
                </if>
                <if test="i.c!=null">
                    c=#{i.c}
                </if>
            </set>
            where id =#{i.id}
        </foreach>
    </update>

这时开始分析,意识到该sql用了id进行定位,而分区键是order_id。在没有二级索引的情况下,每一条update都会下发请求到所有分表,而该事务有多个update(几十个),从而导致长事务的发生,而之前推单为了快速完成,所以这个量是非常大的,进而导致拖垮整个库。

这时基本确认是二级索引删了的缘故。

此时也考虑了其他方案,比如改代码,增加查询条件,从而命中分区键,但发现代码没有做防腐层,改起来很难,复杂度高,在没有测试的情况下容易出大问题,于是否定了这个方案。

12:50 阿里云技术人员定位并给出了kill语句尝试kill,执行没效果。

在kill不掉长事务的情况下,打算重启,但阿里云并没有针对单个私有rds重启的按钮,只能重启整个drds。而该drds上涉及很多其他的业务,影响面太大了。


考虑到该故障极大可能短时间内无法有效恢复,开始考虑人工止损,由于查询导出走的是订单中心(ES),所以导出功能正常,此时由文员人工导出并审单分配。


13:00 阿里云技术人员决定重启cn节点,此时评估影响,阿里云技术人员说明可能会出现闪断,大约 3min 左右。

考虑到重启是为了停止所有长事务,让增加索引的操作顺利执行,而此时服务还在源源不断发送update产生新的长事务。所以此时需要先关闭服务。

13:04 等待业务方完成导出后关闭(因为需要人工推单来兜底止损)

13:31 停掉我们的服务,重启drds

增加二级索引的DDL操作开始执行,发现进展缓慢,预计要1-2小时。

13:58 我们觉得实在太慢了,询问有没有加速的方法,阿里云技术人员提供了新的方法,并由他们来操作,发现速度快了很多。(改了参数)

14:24 二级索引添加成功,重启我们自己的服务,服务恢复正常

四、事后复盘

删除二级索引的动机

当初删除二级索引是为了避免触发阿里云一个已知的bug

二级索引的设计

原先二级索引的设计确实是有问题的

这也是为什么当时阿里云技术人员都认为该索引是无用的。

如果要选择gsi二级索引去优化 用id查询无法命中分区键的问题,那么正确的建立gsi语句应该是

GLOBAL INDEX `idx_gsi_id`(`id`) COVERING (`order_id`) DBPARTITION BY HASH(`id`)

至于为什么原先那个二级索引能起到部分作用,原因在于,虽然之前的二级索引也是全表扫描,但是扫描的表不同,它扫描的是索引表,索引表只有8个,而对于主库的表有1024个,扫描索引表花费的时间更少,即使还要回表,但此次回表并不需要去下发请求到每个主表当中。

不过根本原因还是评估不到位,并没有考虑到二级索引删除可能带来的影响。

五、故障总结

开发侧:

1.历史背景设计不合理性与阿里云版本的bug导致

因 bh_order_line 表中GSI索引 都需要根据 查询字段id ->与order_Id 做一层映射关系,不然无法找到order_Id 对应的分库,

就会全局查询,出现异常告警或长事务慢SQL,如果GSI索引有映射关系,但是所有查询都要查询二级索引表然后回主表查询,会出现之前告警出现的异常

合理设计

GSI索引 建id->与表中热点key建立(除去order_Id)这个字段,代码SQL查询时候带上分库主键order_Id进行查询

2.代码层是否健壮(能否做到熔断,降级,快速修改代码进行止血等能力)

DBA侧:

1.线上删索引操作应该落实工单审批流程。

2.drds1.0库,增加应对突发的数据库请求流量、资源消耗过高的语句,进行限流熔断的机制;

阿里云:

1.线上当前版本bug触发业务偶发报错,同时gsi索引存在回表也会触发bug;

2.线上当前版本bug,导致kill 'all’操作不起作用,升级版本后才能解决;

3.ddl加了限速,导致ddl操作十分漫长,影响恢复进度;

结语

作为实习生第一次搞出这么大的故障,还是蛮惭愧的,而原因竟然只是因为一个二级索引!
还好有DBA、Leader和mentor的支援,不然就我一个肯定GG。
第一次经历故障的止血和恢复,说实话学到了很多很多,在复盘阶段,也学了很多sql优化的相关知识,团队也开始着手处理之前一直出现的慢SQL问题。
多亏了Leader和mentor,不然没有线上处理故障经验的我直接GG。
这次故障也为我敲响警钟,对待问题需要小心再小心,尤其是线上问题,对待线上,要有敬畏之心!
同时自身也要去深入学习自己使用到技术和工具,只有掌握底层原理,才能在做技术方案时更加全面的考虑到各种情况的可能。

猜你喜欢

转载自blog.csdn.net/qq_46101869/article/details/129186569