事故总结集锦-慢SQL导致的数据库雪崩 -like查询条件的那些事儿 03(一周一更)

437 阅读13分钟

事故描述

    10:35 到  10:48,结算页提单失败,持续13分钟,预估影响2300单。

整体经过

    10:35 接到促销规则系统提单接口可用率低的报警和数据库CPU使用率100%的报警,同时接到反馈不能提单。

    10:37 排查出促销规则线程池打满的异常,紧急重启单品促销APP端接口程序,提单依旧失败。

    10:40 怀疑有定时任务在刷单品促销库,紧急停止营销搜索引擎系统的worker,提单依旧失败。

    10:45 怀疑有大量促销创建,停掉单品促销任务系统的worker,3分钟后系统恢复。

    10:48 线上系统恢复,提单正常。

根本原因

经排查事故的主要原因为慢SQL导致,主要慢SQL为:

SQL1: 归档查询语句

耗时: 12.786001秒  查询结果行数 Rows_sent: 0  总记录数: 9xxxxxxx

SELECT  xxx,xxx,xxx,xxx,xxx FROM abc    WHERE     end_date < '2019-06-14 00:00:00'   AND end_time < now()      AND mod(id,4)  IN   ('2' )     LIMIT 1000;

此条SQL在8:00-9:00共计执行1494次,每条SQL平均执行13S,共计需要执行时间为321分钟。也就是说一小时下发的SQL,数据库需要用5小时21分才能执行完成。

SQL2: 模糊搜索查询

耗时: 68.075530秒  查询结果行数: 8  总记录数: xxxxxxxx

    SELECT  xxx,xxxx,xxxxx,xxxxx
    FROM cab    WHERE 1=1  and promotion_name like "%"'宜品'"%"  and promotion_type = 4 and promotion_type != 5 and promotion_type !=7 \
    ORDER BY create_time DESC   limit 0,20;

此类型SQL在21日早晨共计下发476个LIKE语句,数量不多,但会大大拉低数据库性能。

导致提单不可用,其实是一次慢SQL引起的数据库的雪崩,有以下几点:

  • 模糊查询(SQL2的语句)查询数据,导致了数据库的雪崩从下图可以看到10点35后,CPU飙升至100%

  • 提单不可用的原因:用户在提单的时候会插入促销消费记录信息,由于数据库CPU使用率处于100%的状态导致插入一条记录超过了1秒。线程还在占用,当用户看到提单失败后会重复点击提单按钮,由于上一次的线程未被释放,所以会造成线程池打满的问题。从下图可以看出,提单不成功,用户会重复点击,流量会翻10倍。

事故总结

  • 配置合理的报警机制,超过一定时长的查询语句,应该报警给应用负责人及时解决,避免出现数据库宕机。

  • 归档数据的迁移要在晚上执行,避免不必要的操作影响APP端的性能。

  • 运营端的查询页面,避免使用文本的like查询,应命中索引查询。

  • 做好程序自动降级,譬如:单品服务不可用,不应该阻止用户提单,应放弃单品优惠直接让用户提单成功。(这点对系统影响较大,需要和架构小组评估)

树索引原理构造

在谈论like之前 我们先了解下mysql中InnoDB的B+树索引原理构造

B+树索引是B+树在数据库中的一种实现,是最常见也是数据库中使用最为频繁的一种索引。B+树中的B代表平衡(balance),而不是二叉(binary),因为B+树是从最早的平衡二叉树演化而来的。在讲B+树之前必须先了解二叉查找树、平衡二叉树(AVLTree)和平衡多路查找树(B-Tree),B+树即由这些树逐步优化而来。

  • 二叉查找树

  • 平衡二叉树(AVLTree)

  • 平衡多路查找树(B-Tree)

  • B+树

1、二叉查找树

二叉树具有以下性质:左子树的键值小于根的键值,右子树的键值大于根的键值。

如下图所示就是一棵二叉查找树,

image.png

图中 2、4、5、8、11、12、13 七个节点进行查找 深度为1的节点(4、12)的查找次数分别为1次(即 8->4,8->12),深度为2的的节点(2、5、11、13)分别查找次数为2(8->4->2、8->4->5、8->12->11、8->12->13),深度为n的节点的查找次数分别为n,因此上图7个节点的平均查找次数为 (1+2+2+3+3+3+3) / 7 = 2.4次

但是二叉树是可以随意构造的,比如下面这张图

image.png 图中 2、4、5、8、11、12、13 七个节点进行查找 深度为1的节点(4)的查找次数为1次(即 2->4),深度为2的的节点(5)查找次数为2(2->4->5),深度为n的节点的查找次数为n,因此上图7个节点的平均查找次数为 (1+2+3+4+5+6+6) / 7 = 3.8次

所以二叉树的查询效率如果要提高,只有两种情况,一种是根节点和子树节点层级尽可能的少,另一种就是需要这棵二叉树是平衡的,所以 引出了下一个要讲的树形结构——平衡二叉树,或称AVL树。

2、平衡二叉树(AVL Tree)

平衡二叉树(AVL树)在符合二叉查找树的条件下,还满足任何节点的两个子树的高度最大差为1。 下面的两张图片,左边是AVL树,它的任何节点的两个子树的高度差<=1;右边的不是AVL树,其根节点的左子树高度为3,而右子树高度为1;

image.png

image.png

明白了平衡二叉树的概念后,思考一个问题,如果针对平衡二叉的节点 进行新增或者删除 比如 删除14节点 、删除 13节点会发生什么,这会导致平衡二叉树失去平衡,然后则会发生自旋,从而恢复平衡,节点自旋的方式有很多种,我们在这里不详细进行阐述。

3、平衡多路查找树(B-Tree)

  • 树结构的查询效率高,有序,应用在数据库索引使用非常方便。

  • 但是既然使用树结构,二叉搜索树的查询时间复杂度是O(log(n)),从算法逻辑上来讲,无论查找速度还是比较次数都是最小的,那为什么数据库索引没有采用二叉搜索树呢?

  • 因为在实际情况下,我们不得不考虑另外一个现实问题,磁盘IO,因为数据库的索引通常十分庞大,需要以文件形式存储,而磁盘IO的存取次数就是评价一个数据库索引优劣的关键性指标。

  • 因为索引的加载不可能一次全部加载进内存,磁盘读取每次读取的长度为一个磁盘页的长度,所以数据库系统会将一个节点的大小设为等于一页,这样保证了数据库每个节点只需要一次IO就可以完全加载。

  • 每次新建节点,直接申请一个页的空间,计算机存储分配是按页对齐的,这样在物理上也保证了一个节点对应一页,保证一个节点只需要一次IO。在B-tree中m值一般会设的比较大,让树的高度降低,有利于一次完整载入。

  • 在树中查找数据的速度 是由树的高度决定的,所以在二叉查找树中,最坏的情况下,磁盘的IO次数等于索引树的高度,而二叉查找树的性质决定了,大数据量的情况下树的高度必然会很高,所以为了减少磁盘IO次数,我们需要将瘦高的树变得矮胖,这也是B-tree的特征之一。

B-tree是一种多路平衡查找树,它的每一个节点最多包含m个孩子,m被称为B-tree的阶。数据库索引树中,m的大小取决于磁盘页的大小。一个m阶的B-tree具有如下几个特征:

  • 根节点至少有两个孩子

  • 每个中间节点都包含k-1个元素和k个孩子,其中m/2<= k <=m

  • 每一个叶子节点都包含k-1个元素,其中m/2<= k <=m

  • 所有的叶子节点都位于同一层

  • 每个节点中的元素从小到大排列,节点中当k-1个元素正好是k个孩子包含的元素的值域分划

以3阶B-tree为例子

image.png 1.根节点至少有两个孩子 (满足)

(1)13根节点 包含2个子节点

(2)(4 9)节点包含 3个子节点

(3)15节点包含2个子节点

2.每个中间节点都包含k-1个元素数和k个孩子,其中3/2<= k <=3(满足)

(1)根据公式得出 1.5<=k<=3

(2)看(4 9)节点,有4、9这2个元素 和 2、(5 8)、 10 这三个孩子节点,所以k=3 元素=(k-1)=2;孩子k=3

(3)看 15节点,有14、(16 19)这两个孩子节点,元素=1 ;所以k=2 元素=(k-1)=1;孩子k=2

3.每一个叶子节点都包含k-1个元素数,其中m/2<= k <=m(满足)

(1)叶子节点分别为 2、(5 8)、10 、14、(16 19)

(2)叶子节点都包含 1.5<=k<=3 个元素,可以从图中的叶子节点看到 元素数=1、或者元素数=2

4.所有的叶子节点都位于同一层(满足)

叶子节点分别为 2、(5 8)、10 、14、(16 19)都在第三层

5.每个节点中的元素从小到大排列,节点中当k-1个元素正好是k个孩子包含的元素的值域分划(满足)

(1)(4 9)元素所在节点 包含的2个元素 是在孩子节点中元素的范围之内 即:

2元素所在节点<(4 9)元素所在节点< 10元素所在节点

(2) 15元素所在节点: 14元素<15元素< (16 19)节点中的元素 19

实际查询的场景复现:

比如查8这个元素

(1)8和13进行比较

(2)8和(4 9)比较

(3)8和(5 8)比较 最终找到元素所在的节点

随着元素的增多,实际情况下内存中比较次数可能很多,但是内存的比较时间与磁盘IO消耗相比几乎可以忽略不计。

接下来我们再演示下B-tree添加和删除的情况。

B-tree之所以叫Balanced tree,是因为它时刻保持自平衡,插入节点的过程非常复杂,需要配合不同的场景去分析,我们这里只列举一个典型的例子来表示出B-tree的特性,从而更方便大家理解它的理念。

比如我们插入一个节点6 ,自顶向下查找发现6的位置在(5,8)之间:

image.png (1)节点(5,8)已经是两元素节点,无法在增加。

(2)父节点(4,9)也是两元素节点,同样无法再增加。

(3)根节点13是单元素节点,可以升级为两元素节点。变为 根节点(6 13)

(4)根节点13升级为双元素节点后,需要满足b-tree的条件。即非叶子结点的元素个数=指向儿子的指针个数-1

解释为:节点(4 9) 元素为2 指向3个孩子的指针,则要求节点(4 9)必须有 3个孩子节点, 才能满足 (非叶子结点的元素个数=指向儿子的指针个数-1)

(5)根节点升级后,需要拆分(4,9)节点为单元素节点,从而(满足节点非叶子结点(6 13)的元素个数=指向儿子的指针个数-1(4、9、15))

(6)拆分完(4,9)节点后,4节点只有1个元素,为了满足(每个中间节点都包含k-1个元素数和k个孩子,其中3/2<= k <=3)拆分(5,8)为单元素节点,分别组建成新的属性结构。

结果如图:

image.png

再看删除的例子:删除上图14的节点。节点15只有一个孩子,不符合B-tree的规范,因此在15,16,19中找出中位数16,使其取代节点15,而15左下移成为孩子。

image.png

4、B+Tree

B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。

  • B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。

  • 在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。

    B+Tree相对于B-Tree有几点不同:

    1、B+Tree非叶子节点只存储键值信息。

    2、B+Tree所有叶子节点之间都有一个链指针。

    3、B+Tree数据记录都存放在叶子节点中

B-Tree:

image.png

B+Tree:

image.png

1、通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点。

2、而且所有叶子节点(即数据节点)之间是一种链式环结构。

3、因此可以对B+Tree进行两种查找运算:

    (1)一种是对于主键的范围查找和分页查找

    (2)另一种是从根节点开始,进行随机查找。

做一个推算:

InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为48个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为〖10〗^3)。也就是说一个深度为3B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿 条记录。(计算结果摘自网上)

(1)实际情况中每个节点存储的键值不可能全是满的,所以在数据库中,B+Tree的高度一般都在2~4层 

(2)mysql的InnoDB存储引擎在设计时是将根节点常驻内存的 **,** 也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。

了解了B+tree的原理,话题回到like上面。

最左前缀匹配原则: 在MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配

先举一个遵循最佳左前缀法则的例子

age和score组成一个联合索引

select * from user where age=18 and score=100

首先age字段在B+树上是有序的,所以我们可以通过二分查找法来定位到age=18的位置。

其次在age确定的情况下score是相对有序的,因为有序,所以同样可以通过二分查找法找到score=100的位置。

不遵循最佳左前缀的例子

select * from user where score=100

score有顺序的前提:是在age确定的情况下。

age条件都没了,score肯定是不能确定顺序,在一个无序的B+树上是无法用二分查找来定位到score字段的。

like索引失效原理

where name like "a%"
     
where name like "%a%"
      
where name like "%a"

我们先来了解一下%的用途

  • %放在右边,代表查询以"a"开头的数据,如:abc

  • 两个%%,代表查询数据中包含"a"的数据,如:cab、cba、abc

  • %放在左边,代表查询以"a"为结尾的数据,如cba

为什么%放在右边有时候能用到索引

  • %放右边叫做:前缀

  • %%叫做:中缀

  • %放在左边叫做:后缀

没错,这里依然是最佳左前缀法则这个概念

一、%号放右边(前缀)

由于B+树的索引顺序,是按照首字母的大小进行排序,前缀匹配又是匹配首字母。所以可以在B+树上进行有序的查找,查找首字母符合要求的数据。所以有些时候可以用到索引。

二、%号放右边

是匹配字符串尾部的数据,我们上面说了排序规则,尾部的字母是没有顺序的,所以不能按照索引顺序查询,就用不到索引。

三、两个%%号

这个是查询任意位置的字母满足条件即可,只有首字母是进行索引排序的,其他位置的字母都是相对无序的,所以查找任意位置的字母是用不上索引的。

总结

事故的问题根本原因在于 慢sql导致的线程阻塞,打满了线程池,后续请求全部走拒绝策略,服务器也因为线程数过多导致cpu负载100%造成了雪崩。

在toC的接口服务,最好是隔离db,尽量采用nosql的中间件去提供服务:比如redis

再说toB的服务,我们在查询数据写sql的时候,尽量避免用 %like% 这种肯定不走索引的查询条件,如果业务非得要用,可以考虑拆分业务、做好业务隔离,不要和核心服务揉在一起。