关于MySQL分区表的一个性能BUG
二、使用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
大概逻辑如下:
循环表中每个字段(一层循环)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=innodbpartition 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);
这样template
需要每个分区(scan next partition
)都进行重建,这样就出现了我们上面的问题。这个其实也可以理解,新的分区是新的innodb文件,这样上次定位的持久化游标实际已经没有什么用了,就相当于一次新的表访问。这里在是否进行template
建立还有一个判断如下:
if (m_prebuilt->sql_stat_start) { build_template(false); }
ha_innopart::set_partition:m_prebuilt->sql_stat_start = m_sql_stat_start_parts.test(part_id);
这个栈实际并不完整,但是其中出现了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=innodbpartition 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
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
这样问题就得到了确认。
七、总结
这个问题实际上和二级索引相对于分区键的数据离散度有关,但是我们无法控制二级索引的数据,并且索引也是必须使用的。只能通过一些方面尽量避免,当然我也提交了一个BUG,如下:
https://bugs.mysql.com/bug.php?id=104576
不知道是否有办法修复这个问题,比如对于分区表来讲实际上每个分区的字段都是一样的,是否需要每次都重建mysql_row_templ_t.clust_rec_field_no
?如果不需要那么问题自解,官方目前已经验证了这个问题确实存在。如下是一些避免的方式,
- 分区表字段不宜过多
- 访问的字段不应该一味的使用select *
- 避免使用hash分区,hash分区会增加这种问题
更多相关文章
- android 下写文件性能测试
- [android]android性能测试命令行篇
- Android中对后台任务线程性能的说明及优化
- Android特性
- Android(安卓)开发性能优化简介
- 性能优化之Java(Android)代码优化
- 【Android(安卓)Linux内存及性能优化】(八) 系统性能分析工具
- 微信小程序性能分析Trace工具
- 性能优化之Java(Android)代码优化