关于MySQL分区表的一个性能BUG
作者:老叶茶馆 发布时间:2024-01-17 00:30:11
一、问题描述
最近遇到一个问题,也就是使用分区表进行数据查询/加载的时候比普通表的性能下降了约50%,主要瓶颈出现在CPU,既然是CPU瓶颈理所当然的我们可以采集perf top -a -g
和pstack
来寻找性能瓶颈所在,同时和普通表进行对比,发现CPU主要耗在函数build_template_field
上如下图:
二、使用pt-pmap进行栈分析
为了和perf top -g -a
进行相互印证,我们同时获取了当时的pstack
,由于线程较多为了方便获取有用的信息我们通过pt-pmap进行了格式化如下:
格式化后我们提出掉空闲的等待栈,发现大量的如上,这也和perf top -a -g中的表现进行了相互印证。
三、关于本列中瓶颈点的分析
我们看到这里大量的cpu
耗在
ha_innobase::build_template
->build_template_field
->dict_col_get_clust_pos
对于template
来讲,其几乎是和特定的一次的查询进行绑定的,也就是普通的语句至少需要一个template
。其结构为row_prebuilt_t
,包含查询元组,查询的表,查询用到的索引,事务相关信息,持久化游标,MySQL层查询行的长度,自增信息,ICP相关信息,mysql_row_templ_t
结构等信息。其中mysql_row_templ_t
这个信息就是每个字段一个,主要作用记录的是MySQL层feild信息和Innodb层columns信息的相关属性,用于快速转换一行记录在MySQL层和Innodb层之间转换。为了初始化mysql_row_templ_t
就出现了上面的逻辑,
大概逻辑如下:
循环表中每个字段(一层循环)ha_innobase::build_template
是否为需要访问的字段 build_template_needs_field
这里包含查询和写入的所有字段,需要访问的字段越多越慢
如果不是则不作继续循环
如果需要访问build_template_field
(mysql_row_templ_t结构体填充)
循环主键的每个字段(二层循环)
包含伪列,主键就是表的里面全部字段,表中字段越多越慢)dict_col_get_clust_pos
确认本字段在主键的位置
pos0 主键pos1 DB_TRX_ID pos2 DB_ROLL_PTR pos3
开始为用户其他字段
循环索引的每个字段(二层循环,但是索引字段一般不会太多,因此这里不会慢)dict_index_t::get_col_pos
确认本字段在索引的位置,如果没有则返回NULL
返回pos 比如 主键 id1 二级索引 id2 id3 二级索引为pos0 id2 pos1 id3 pos2 id1
继续完成其他属性比如mysql null位图,mysql显示长度,mysql字符集等等
这里我们看到这里实际上有2层循环,也就是循环套循环(时间复杂度O(M×N)),而循环影响最大的有2个地方:
第一层,表中字段的多少
第二层,需要访问的字段(读和写都算)在主键(也就是全部字段)中循环
这里也就是为什么这里会慢的原因。但是template通常不会一个查询进行多次建立,比如一个普通表的大查询,只有在语句第一次进行数据定位之前会进行建立,这就不得不说这是分区表和普通表的对比中一个特殊的地方了。下面描述一下。
四、分区表中多次建立template的情况
假设我们有如下的分区表:
create table t(
id1 int,
id2 int,
primary key(id1),
key(id2)
)engine=innodb
partition by range(id1)(
partition p0 values less than(100),
partition p1 values less than(200),
partition p2 values less than(300));
insert into t values(1,1);
insert into t values(101,1);
insert into t values(201,1);
insert into t values(2,2);
insert into t values(3,2);
insert into t values(4,2);
insert into t values(7,2);
insert into t values(8,2);
insert into t values(9,2);
insert into t values(10,2);
我们使用语句"select * from t where id2=1
",显然id2是二级索引,由于MySQL全部都是local分区的二级索引,因此这里值分别分布在3个分区中,对于这样一个语句在本该是普通表通过上次定位后的位置继续访问(next_same
)的时候,通过封装分区表的方法,将其改为了index read
再次定位,而我们可以清楚的看到这里是scan next partition,其part=1这是第二个分区了,也就是我们的p1(第一个为0)
这样template
需要每个分区(scan next partition
)都进行重建,这样就出现了我们上面的问题。这个其实也可以理解,新的分区是新的innodb文件,这样上次定位的持久化游标实际已经没有什么用了,就相当于一次新的表访问。这里在是否进行template
建立还有一个判断如下:
if (m_prebuilt->sql_stat_start) {
build_template(false);
}
而m_prebuilt->sql_stat_start除了在语句开始的时候设置为true,每次更换分区依旧会设置为true如下:
ha_innopart::set_partition:
m_prebuilt->sql_stat_start = m_sql_stat_start_parts.test(part_id);
五、关于一个特殊的流程
在我们的故障pstack
中还有一个栈如下:
这个栈实际并不完整,但是其中出现了Partition_helper::handle_ordered_index_scan
,这个函数实际上和分区表的排序有关,如果我们考虑这样一种情况,对于二级索引select max(id2) from t,那么需要首先访问每个分区获取其中的最大值然后对比每个分区的最大值,得到最终的结果,而MySQL则采用优先队列进行处理,这应该是就是本函数完成的部分功能(没仔细去看)。其次我们先出现了QUICK_RANGE_SELECT
这是范围查询会用到的,那么我们构造如下:
select * from t where id2<2 order by id2;
栈:
这里就是因为id2这个字段只保证在分区内部是按照大小排列的但是在整个表来讲,它是无序的,需要额外的处理。
六、问题模拟
有了这些准备,我们可以构造一个300个字段和25个分区的分区表。测试版本最新8.0.26
create table tpar300col(
id1 int,
id2 int,
id3 int,
id4 int,
...
id299 varchar(20),
id300 varchar(20),
primary key(id1),
key(id2)
)engine=innodb
partition by range(id1)(
partition p0 values less than(100),
partition p1 values less than(200),
partition p3 values less than(300),
...
partition p25 values less than(2500));
insert into tpar300col values(1 ,1,1,
....每个分区一条数据
insert into tpar300col values(2401,1,1
然后构造一些其他数据id2不要为1,建立存储过程:
delimiter //
CREATE PROCEDURE test300col()
begin
declare num int;
set num = 1;
while num <= 1000000 do
select * from tpar300col where id2=1;
set num = num+1;
end while;
end //
执行:
/opt/mysql/mysql3340/install/mysql8/bin/mysql -S--socket=/opt/mysql/mgr3315/data/mgr3315.sock -e'use test;call test300col();' > log.log
然后perf top 观察如下:
这样问题就得到了确认。
七、总结
这个问题实际上和二级索引相对于分区键的数据离散度有关,但是我们无法控制二级索引的数据,并且索引也是必须使用的。只能通过一些方面尽量避免,当然我也提交了一个BUG,如下:
https://bugs.mysql.com/bug.php?id=104576
不知道是否有办法修复这个问题,比如对于分区表来讲实际上每个分区的字段都是一样的,是否需要每次都重建mysql_row_templ_t.clust_rec_field_no
?如果不需要那么问题自解,官方目前已经验证了这个问题确实存在。如下是一些避免的方式,
分区表字段不宜过多
访问的字段不应该一味的使用select *
避免使用hash分区,hash分区会增加这种问题
来源:https://www.tuicool.com/articles/EZFjAfa


猜你喜欢
- python time模块计算时间之间的差距练习题1. 当前月1号对应的0点的时间戳# 定义一个当前月分的一号0点字符串格式的时间 now_
- 一、数据集爬取现在的深度学习对数据集量的需求越来越大了,也有了许多现成的数据集可供大家查找下载,但是如果你只是想要做一下深度学习的实例以此熟
- #!/usr/bin/env pythonimport sockets = socket.socket(socket.AF_PACKET,
- 本文讲述了线程安全及Python中的GIL。分享给大家供大家参考,具体如下:摘要什么是线程安全? 为什么python会使用GIL的机制?在多
- 在前后端分离是大趋势的背景下,前端获取数据都是通过调用后台的接口来获取数据微服务的应用越来越多。Django是Python进行web应用开发
- 本文实例讲述了python实现从字典中删除元素的方法。分享给大家供大家参考。具体分析如下:python的字典可以通过del方法进行元素删除,
- 以图像处理见长的微软Live实验室,最近发布了一款新作:Pivot。装完启动后的第一印象就是一款浏览器,和IE、FF、Chrome又不太一样
- Python 正则表达式正则表达式本身是独立于编程语言的知识,但是它又依附于编程语言,基本上我们所使用的编程语言都提供了对它的实现,当然了,
- 第1章 ansible软件概念说明python语言是运维人员必会的语言,而ansible是一个基于Python开发的自动化运维工具 (sal
- 最近几天仔细研究了一下vertical-align这个属性,结果让我大吃一惊,这个很“资深”的CSS标准竟然在各个浏览器里面的表现都各不相同
- 本文实例讲述了PHP实现的AES双向加密解密功能。分享给大家供大家参考,具体如下:<?php/* * Created on 2018-
- 所使用python环境为最新的3.6版本一、安装pdfminer模块 安装anaconda后,直接可以通过pip安装pip install
- 前言提示:以下是本篇文章正文内容🍒PIL库概述PIL库支持图像存储、 显示和处理, 它能够处理几乎所有图片格式, 可以完成对图像的缩放、 剪
- 1. 排序有什么用“排序”这个专业名词原本是来源于计算机程序操作中的,是一种很常见的算法设计,当然,对交互设计来说,探讨冒泡排序和堆排序之间
- python中字典是非常常用的数据类型,了解各种方法的作用及优缺点对于字典的使用非常有用。dict.clear() 的方法用于清空所有的键值
- 本文范例是书写两个日志:错误日志(ERROR级别)和运行日志(DEBUG级别),其中运行日志每日凌晨进行分割import logging,d
- 目录1. 前言2. 实战一下2-1 进入虚拟环境,创建一个项目及 App2-2 创建模板目录并配置 set
- python通过安装使用paramiko模块,将本地文件上传到服务器上import paramikoimport datetimeimpor
- 由于XML本身的诸多优点,XML技术已被广泛的使用,目前的好多软件技术同XML紧密相关,比如微软的.net 平台对xml提供了强大的支持,提
- 前言每个页面按照结构可以分成三部分:window page tabbar.其中window和tabbar一般比较固定,page是平常业务开展