记录一次数据库CPU被打满的排查过程

1 前言

近期随着数据量的增长,数据库CPU使用率100%报警频繁起来。第一个想到的就是慢Sql,我们对未合理运用索引的表加入索引后,问题依然没有得到解决,深入排查时,发现在 order by id asc limit n时,即使where条件已经包含了覆盖索引,优化器还是选择了错误的索引导致。通过查询大量资料,问题得到了解决。这里将解决问题的思路以及排查过程分享出来,如果有错误欢迎指正。

2 正文

2.1 环境介绍

记录一次数据库CPU被打满的排查过程

2.2 发现问题

22日开始,收到以下图1报警变得频繁起来,由于数据库中会有大数据推数动作,数据库CPU偶尔报警并没有引起对该问题的重视,直到通过图2对整日监控数据分析时,才发现问题的严重性,从0点开始,数据库CPU频繁被打满。

记录一次数据库CPU被打满的排查过程

图1:报警图

记录一次数据库CPU被打满的排查过程

图2:整日CPU监控图

2.3 排查问题

发现问题后,开始排查慢Sql,发现很多查询未添加合适的索引,经过一轮修复后,问题依然没有得到解决,在深入排查时发现了一个奇怪现象,SQL代码如下(表名已经替换),比较简单的一个单表查询语句。

SELECT
*
FROM
test
WHERE
is_delete = 0
AND business_day = '2021-12-20'
AND full_ps_code LIKE 'xxx%'
AND id > 2100
ORDER BY
id
LIMIT 500;

看似比较简单的查询,但执行时长平均在90s以上,并且调用频次较高。如图3所示。

记录一次数据库CPU被打满的排查过程

图3:慢Sql平均执行时长

开始检查表信息,可以看到表数据量在2100w左右。

记录一次数据库CPU被打满的排查过程

图4:数据表情况

排查索引情况,主键为id,并且有business_day与full_ps_code的联合索引。

PRIMARY KEY (id) USING BTREE,
KEY idx_business_day_full_ps_code (business_day,full_ps_code)
==========以下索引可以忽略========
KEY idx_erp_month_businessday (erp,month,business_day),
KEY idx_business_day_erp (business_day,erp),
KEY idx_erp_month_ps_plan_id (erp,month,ps_performance_plan_id),
......

通过Explain查看执行计划时发现,possible_keys中包含上面的联合索引,而Key却选择了Primary主键索引,扫描行数Rows为1700w,几乎等于全表扫描。

记录一次数据库CPU被打满的排查过程

图5:执行计划情况

2.4 解决问题

第一次,我们分析是,由于Where条件中包含了ID,查询分析器认为主键索引扫描行数会少,同时根据主键排序,使用主键索引会更加合理,我们试着添加以下索引,想要让查询分析器命中我们新加的索引。

ADD INDEX idx_test(business_day, full_ps_code, id) USING BTREE;

再次通过Explain语句进行分析,发现执行计划完全没变,还是走的主键索引。

explain
SELECT
*
FROM
test
WHERE
is_delete = 0
AND business_day = '2021-12-20'
AND full_ps_code LIKE 'xxx%'
AND id > 2100
ORDER BY
id
LIMIT 500;

记录一次数据库CPU被打满的排查过程

图6:执行计划情况

第二次,我们通过强制指定索引方式 force index (idx_test)方式,再次分析执行情况,得到图7的结果,同样的查询条件同样的结果,查询时长由90s->0.49s左右。问题得到解决

记录一次数据库CPU被打满的排查过程

图7:强制指定索引后执行计划情况

记录一次数据库CPU被打满的排查过程

第三次,我们怀疑是where条件中有ID导致直接走的主键索引,where条件中去掉id,Sql调整如下,然后进行分析。依然没有命中索引,扫描rows变成111342,查询时间96s

SELECT
*
FROM
test
WHERE
is_delete = 0
AND business_day = '2021-12-20'
AND full_ps_code LIKE 'xxx%'
ORDER BY
id
LIMIT 500

记录一次数据库CPU被打满的排查过程

记录一次数据库CPU被打满的排查过程

第四次,我们把order by去掉,SQL调整如下,然后进行分析。命中了idx_business_day_full_ps_code之前建立的联合索引。扫描行数变成154900,查询时长变为0.062s,但是发现结果与预想的不一致,发生了乱序

SELECT
*
FROM
test
WHERE
is_delete = 0
AND business_day = '2021-12-20'
AND full_ps_code LIKE 'xxx%'
AND id > 2100
LIMIT 500;

记录一次数据库CPU被打满的排查过程

记录一次数据库CPU被打满的排查过程

第五次,经过前几次的分析可以确定,order by 导致查询分析器选择了主键索引,我们在Order by中增加排序字段,将Sql调整如下,同样可以命中我们之前的联合索引,查询时长为0.034s,由于先按照主键排序,结果是一致的。相比第四种方法多了一份filesort,问题得解决。

SELECT
*
FROM
test
WHERE
is_delete = 0
AND business_day = '2021-12-20'
AND full_ps_code LIKE 'xxx%'
AND id > 2100
ORDER BY
id,full_ps_code
LIMIT 500;

记录一次数据库CPU被打满的排查过程

记录一次数据库CPU被打满的排查过程

第六次,我们考虑是不是Limit导致的问题,我们将Limit 500 调整到 1000,Sql调整如下,奇迹发生了,命中了联合索引,查询时长为0.316s,结果一致,只不过多返回来500条数据。问题得到了解决。经过多次实验Limit 大于695时就会命中联合索引,查询条件下的数据量是79963,696/79963大概占比是0.0087,猜测当获取数据比超过0.0087时,会选择联合索引,未找到源代码验证此结论。

SELECT
*
FROM
test
WHERE
is_delete = 0
AND business_day = '2021-12-20'
AND full_ps_code LIKE 'xxx%'
AND id > 2100
ORDER BY
id
LIMIT 1000;

记录一次数据库CPU被打满的排查过程

记录一次数据库CPU被打满的排查过程

经过我们的验证,其中第2、5、6三种方法都可以解决性能问题。为了不影响线上,我们立即修改代码,并选择了force index 的方式,上线观察一段时间后,数据库CPU恢复正常,问题得到了解决。

记录一次数据库CPU被打满的排查过程

3 事后分析

上线后,问题解决了,同时也给我留下了很多疑问。

[En]

After the launch, the problem was solved, and at the same time, it left me a lot of questions.

  • 为什么明明where条件中包含了联合索引,却未能命中,反而选择了性能较慢的主键索引?
  • 为什么在order by中增加了一个索引其他字段,就可以命中联合索引了呢?
  • 为什么我仅仅是将limit限制条件由原来的500调大后,也能命中联合索引呢?

这一切的答案都来自MySQL的查询优化器。

3.1 查询优化器

查询优化器是专门负责优化查询语句的优化器模块。查询优化器通过计算和分析收集到的各种系统统计信息,给出了查询的最优执行计划–最优数据检索方法。

[En]

The query optimizer is an optimizer module which is specially responsible for optimizing query statements. By calculating and analyzing all kinds of system statistical information collected, the query optimizer gives the optimal execution plan-the optimal data retrieval method for the query.

优化器决定如何执行查询的方式是基于一种称为基于代价的优化的方法。5.7在代价类型上分为IO、CPU、Memory。内存的代价收集了,但是并没有参与最终的代价计算。Mysql中引入了两个系统表,mysql.server_cost和mysql.engine_cost,server_cost对应CPU的代价,engine_cost代表IO的代价。

server_cost(CPU代价)
  • row_evaluate_cost (default 0.2) 计算符合条件的行的代价,行数越多,此项代价越大
  • memory_temptable_create_cost (default 2.0) 内存临时表的创建代价
  • memory_temptable_row_cost (default 0.2) 内存临时表的行代价
  • key_compare_cost (default 0.1) 键比较的代价,例如排序
  • disk_temptable_create_cost (default 40.0) 内部myisam或innodb临时表的创建代价
  • disk_temptable_row_cost (default 1.0) 内部myisam或innodb临时表的行代价

由上可以看出创建临时表的代价是很高的,尤其是内部的myisam或innodb临时表。

engine_cost(IO代价)
  • io_block_read_cost (default 1.0) 从磁盘读数据的代价,对innodb来说,表示从磁盘读一个page的代价
  • memory_block_read_cost (default 1.0) 从内存读数据的代价,对innodb来说,表示从buffer pool读一个page的代价

这些信息都可以在数据库中配置,当数据库中未配置时,从MySql源代码(5.7)中可以看到以上默认值情况

记录一次数据库CPU被打满的排查过程

3.2 代价配置

--修改io_block_read_cost值为2
UPDATE mysql.engine_cost
SET cost_value = 2.0
WHERE cost_name = 'io_block_read_cost';
--FLUSH OPTIMIZER_COSTS 生效,只对新连接有效,老连接无效。
FLUSH OPTIMIZER_COSTS;

3.3 代价计算

代价是如何算出来的呢,通过读MySql的源代码,可以找到最终的答案

3.3.1 全表扫描(table_scan_cost)

以下代码摘自MySql Server(5.7分支),全表扫描时,IO与CPU的代价计算方式。

double scan_time=
cost_model->row_evaluate_cost(static_cast<double>(records)) + 1;
// row_evaluate_cost 核心代码
// rows * m_server_cost_constants->row_evaluate_cost()
// 数据行数 * 0.2 (row_evaluate_cost默认值) + 1 = CPU代价
Cost_estimate cost_est= head->file->table_scan_cost();
//table_scan_cost 核心代码
//const double io_cost
// = scan_time() * table->cost_model()->page_read_cost(1.0)
// 这部分代价为IO部分
//page_read_cost 核心代码
//
//const double in_mem= m_table->file->table_in_memory_estimate();
//
// table_in_memory_estimate 核心逻辑
//如果表的统计信息中提供了信息,使用统计信息,如果没有则使用启发式估值计算
//pages=1.0
//
//const double pages_in_mem= pages * in_mem;
//const double pages_on_disk= pages - pages_in_mem;
//
//
//计算出两部分IO的代价之和
//const double cost= buffer_block_read_cost(pages_in_mem) +
// io_block_read_cost(pages_on_disk);
//
//
//buffer_block_read_cost 核心代码
// pages_in_mem比例 * 1.0 (memory_block_read_cost的默认值)
// blocks * m_se_cost_constants->memory_block_read_cost()
//
//
//io_block_read_cost 核心代码
//pages_on_disk * 1.0 (io_block_read_cost的默认值)
//blocks * m_se_cost_constants->io_block_read_cost();
//返回IO与CPU代价
//这里增加了个系数调整,原因未知
cost_est.add_io(1.1);
cost_est.add_cpu(scan_time);

根据源代码分析,当表包含100行数据时,全表扫描的成本为23.1,计算逻辑如下。

[En]

According to the source code analysis, when the table contains 100 rows of data, the cost of the full table scan is 23.1, and the calculation logic is as follows.

//CPU代价 = 总数据行数 * 0.2 (row_evaluate_cost默认值) + 1
cpu_cost = 100 * 0.2 + 1 等于 21
io_cost = 1.1 + 1.0 等于 2.1
//总成本 = cpu_cost + io_cost = 21 + 2.1 = 23.1

验证结果如下图

记录一次数据库CPU被打满的排查过程
3.3.2 索引扫描(index_scan_cost)

以下代码摘自MySql Server(5.7分支),当出现索引扫描时,是如何进行计算的,核心代码如下

//核心代码解析
*cost= index_scan_cost(keyno, static_cast<double>(n_ranges),
static_cast<double>(total_rows));
cost->add_cpu(cost_model->row_evaluate_cost(
static_cast<double>(total_rows)) + 0.01)

io代价计算核心代码

//核心代码
const double io_cost= index_only_read_time(index, rows) *
table->cost_model()->page_read_cost_index(index, 1.0);
// index_only_read_time(index, rows)
// 估算index占page个数
//page_read_cost_index(index, 1.0)
//根据buffer pool大小和索引大小来估算page in memory和in disk的比例,计算读一个page的代价

cpu代价计算核心代码

add_cpu(cost_model->row_evaluate_cost(
static_cast<double>(total_rows)) + 0.01);
//total_rows 等于索引过滤后的总行数
//row_evaluate_cost 与全表扫描的逻辑类似,
//区别在与一个是table_in_memory_estimate一个是index_in_memory_estimate
3.3.3 其他方式

计算代价的方式有很多,其他方式请参考 MySql原代码。https://github.com/mysql/mysql-server.git

3.4 深度解析

通过查看optimizer_trace,可以了解查询优化器是如何选择的索引。

set optimizer_trace="enabled=on";
--如果不设置大小,可能导致json输出不全
set OPTIMIZER_TRACE_MAX_MEM_SIZE=1000000;
SELECT
*
FROM
test
WHERE
is_delete = 0
AND business_day = '2021-12-20'
AND full_ps_code LIKE 'xxx%'
AND id > 0
ORDER BY
id
LIMIT 500;
select * FROM information_schema.optimizer_trace;
set optimizer_trace="enabled=off";

通过分析rows_estimation节点,可以看到通过全表扫描(table_scan)的话的代价是 8.29e6,同时也可以看到该查询可以选择到主键索引与联合索引,如下图。

记录一次数据库CPU被打满的排查过程

上图中全表扫描的代价是8.29e6,我们转换成普通计数法为 8290000,如果使用主键索引成本是 3530000,联合索引 185881,最小的应该是185881联合索引,也可以看到第一步通过成本分析确实选择了我们的联合索引。

记录一次数据库CPU被打满的排查过程

记录一次数据库CPU被打满的排查过程

记录一次数据库CPU被打满的排查过程

但是为什么选择主键索引呢?

[En]

But why did you choose the primary key index?

通过往下看,在reconsidering_access_paths_for_index_ordering节点下, 发现由于Order by 导致重新选择了索引,在下图中可以看到主键索引可用(usable=true),我们的联合索引为not_applicable (不适用),意味着排序只能使用主键索引。

记录一次数据库CPU被打满的排查过程

接下来通过index_order_summary可以看出,执行计划最终被调整,由原来的联合索引改成了主键索引,就是说这个选择无视了之前的基于索引成本的选择。

记录一次数据库CPU被打满的排查过程

之所以有这样的选择,主要原因如下:

[En]

The main reasons why there is such an option are as follows:

The short explanation is that the optimizer thinks — or should I say hopes — that scanning the whole table (which is already sorted by the id field) will find the limited rows quick enough, and that this will avoid a sort operation. So by trying to avoid a sort, the optimizer ends-up losing time scanning the table.

从这段解释可以看出主要原因是由于我们使用了order by id asc这种基于 id 的排序写法,优化器认为排序是个昂贵的操作,所以为了避免排序,并且它认为 limit n 的 n 如果很小的话即使使用全表扫描也能很快执行完,所以它选择了全表扫描,也就避免了 id 的排序。

5 总结

查询优化器会基于代价来选择最优的执行计划,但由于order by id limit n的存在,MySql可能会重新选择一个错误的索引,忽略原有的基于代价选择出来的索引,转而选择全表扫描的主键索引。这个问题在国内外有大量的用户反馈,BUG地址 https://bugs.mysql.com/bug.php?id=97001 。官方称在5.7.33以后版本可以关闭prefer_ordering_index 来解决。如下图所示。

记录一次数据库CPU被打满的排查过程

另外在我们日常慢Sql调优时,可以通过以下两种方式,了解更多查询优化器选择过程。

`
–第一种
explain format=json
sql语句

Original: https://www.cnblogs.com/Jcloud/p/16642188.html
Author: 京东云开发者
Title: 记录一次数据库CPU被打满的排查过程

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/504910/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 微信小程序二维码

    一、获取小程序码的三个接口 不同的接口对应不同的业务场景,接口B用的较多,接口C官方不推荐使用,也就是说根据需码量来决定选择A接口还是B接口。 (1)、接口 A: 适用于需要的码数…

    数据库 2023年6月6日
    0126
  • 记一次部署系列:prometheus配置通过alertmanager进行邮件告警

    1、修改配置 首先在prometheus中配置alertmanager地址,并配置告警规则文件,如下,然后重启prometheus。 规则文件如下:rules.yml &#8211…

    数据库 2023年6月9日
    077
  • 分布式全局唯一ID

    方案一、UUID UUID的方式能生成一串唯一随机32位长度数据,它是无序的一串数据,按照开放软件基金会(OSF)制定的标准计算,UUID的生成用到了以太网卡地址、纳秒级时间、芯片…

    数据库 2023年6月9日
    097
  • 23种设计模式之命令模式

    文章目录 概述 命令模式的优缺点 命令模式的应用场景 命令模式的结构和实现 * 模式的结构 模式的实现 总结 ; 概述 命令模式(Command Pattern)是一种数据驱动的设…

    数据库 2023年6月6日
    0132
  • [LeetCode]3. 无重复字符的最长子串

    给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: “abcabcbb”输出: 3解释: 因为无重复字符的最长子串是 &#…

    数据库 2023年6月9日
    074
  • NO.2 Windows桌面图标-间距参数调整

    遇到如下问题: 桌面图标自动排序后间隔过大,且如图二这种指向图标能看到图标之间的间隔虚框,此方法可调整虚框的水平和垂直距离,即调整图标之间的间距。 测试电脑: 华为 mateboo…

    数据库 2023年6月14日
    0103
  • 多商户商城系统功能拆解22讲-平台端分销商品

    多商户商城系统,也称为B2B2C(BBC)平台电商模式多商家商城系统。可以快速帮助企业搭建类似拼多多/京东/天猫/淘宝的综合商城。 多商户商城系统支持商家入驻加盟,同时满足平台自营…

    数据库 2023年6月14日
    0107
  • macOS快捷键

    1. 最小化所有应用程序 command+option+h+m 2. 同应用窗口切换 command &#xFF5E; 3. 截图 "&#x5168;&a…

    数据库 2023年6月14日
    081
  • SQL与数据库编程学习笔记——day3

    SQL与数据库编程学习笔记-day3 增加语句; 利用insert into语句进行增加数据库数据; 格式: insert into 表名 (字段名) values (数值);ps…

    数据库 2023年6月9日
    0136
  • Linux 下重启 PHP 服务、nginx 服务

    一、重启 PHP 服务 service php-fpm start 开启 service php-fpm stop 停止 service php-fpm restart 重启 二、…

    数据库 2023年6月14日
    076
  • JavaScript进阶内容——jQuery

    JavaScript进阶内容——jQuery 我们在前面的文章中已经掌握了JavaScript的全部内容,现在让我们了解一下JavaScript库 这篇文章主要是为了为大家大致讲解…

    数据库 2023年6月14日
    0111
  • 第18章 MySQL8其它新特性

    第18章 MySQL8其它新特性 1. MySQL8新特性概述 MySQL&#x4ECE;5.7&#x7248;&#x672C;&#x76F4;&a…

    数据库 2023年6月6日
    090
  • 2022春秋杯春季赛勇者山峰-misc

    这几天搞完取证就开始弄这些个东西,属实把我整麻了,临场我就整不出来,可能是功利心太重了 ,一着急就啥也不会,毕竟这misc只是用来辅助锻炼我的取证的。。 tiger 1.These…

    数据库 2023年6月11日
    0107
  • dba+开源工具:MHA复刻版,轻松实现MySQL高可用故障转移(附下载)

    由于MHA(mha4mysql-manager)工具在2018年已经停止维护更新,且不支持GTID复制模式,在原版基础上增补功能难度较大,因此考虑将其重构。 参考了原版MHA的故障…

    数据库 2023年5月24日
    0111
  • 「开源推荐」一个通用的后台管理系统

    最近跟朋友组了个团队一起做一款开源产品,所以在找一款好用的,功能性强的后台管理系统,找了几天资料,了解了很多后台管理系统,最后发现了一款功能完整,代码结构清晰而且还开源的后台管理系…

    数据库 2023年6月14日
    091
  • gh-ost使用问题记录

    因为 pt-osc 对数据库性能影响较大,且容易造成死锁问题,目前我们在线更改表结构都使用 gh-ost 工具进行修改,这里记录一下使用 gh-ost 过程中的问题,以作记录;首先…

    数据库 2023年6月9日
    084
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球