一、问题描述最近遇到一个问题,就是使用分区表进行数据查询/加载时,性能比普通的下降了50%左右表。瓶颈出现在CPU上。前端训练自然是CPU瓶颈。我们可以收集perftop-a-g和pstack来找到性能瓶颈。同时,我们将其与普通表进行对比,发现CPU主要消耗在函数build_template_field上,如下图:2、使用pt-pmap进行堆栈分析,以便与perftop相互确认——g-a,我们当时也拿到了pstack。由于线程较多,为了方便获取有用的信息,我们通过pt-pmap格式化如下:format经过优化,我们提出去掉空闲的等待栈,发现大量如上,这也是由perftop-a-g中的性能确认。3、关于本专栏的瓶颈点分析,我们可以看到在ha_innobase::build_template->build_template_field->dict_col_get_clust_pos消耗大量cpu。对于模板,它几乎与特定的查询绑定,即普通语句至少需要一个模板。其结构为row_prebuilt_t,包含查询元组、查询表、查询使用的索引、事务相关信息、持久游标、MySQL层查询行长度、自增信息、ICP相关信息、mysql_row_templ_t结构等信息。其中mysql_row_templ_t信息是每个字段一个,其主要作用是记录MySQL层的feild信息和Innodb层的column信息的相关属性,用于MySQL层和Innodb层之间快速转换一行记录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主键pos1DB_TRX_IDpos2DB_ROLL_PTRpos3开始循环遍历每一个为用户其他字段索引的字段(二级循环,但一般索引字段不会太多,所以这里不会慢)dict_index_t::get_col_pos确认posi索引中该字段的归属,如果不存在则返回NULL返回pos例如主键id1二级索引id2id3二级索引为pos0id2pos1id3pos2id1继续补全其他属性如mysqlnullbitmap,mysqldisplaylength,mysqlcharacterSet等这里我们看到其实有2层循环,也就是循环中的循环(时间复杂度O(M×N)),循环影响最大的地方有2个:第一层,表中的字段有多少第二层,需要访问(读和写)的字段在主键(即所有字段)中循环,这也是这里慢的原因。但是,模板通常不会多次创建查询。比如一个普通表的大查询,只会在语句中第一次定位数据之前建立。这不得不说,这是分区表和普通表的对比之一。也很特别的地方。下面描述一下。4.分区表多次创建模板的情况假设我们有如下分区表:createtablet(id1int,id2int,primarykey(id1),key(id2))engine=innodbpartitionbyrange(id1)(分区p0值小于(100),分区p1值小于(200),分区p2值小于(300));插入t值(1,1);插入t值(101,1);插入t值(201,1);插入t值(2,2);插入t值(3,2);插入t值(4,2);插入t值(7,2);插入t值(8,2);插入t值(9,2);插入t值(10,2);我们使用语句“select*fromtwhereid2=1”,显然id2是二级索引,由于MySQL都是本地分区的二级索引,所以这里的值分布在三个分区。对于这样的语句,当普通表通过上次定位后的position继续访问(next_same)时,分区表封装方式改为indexread再次定位,可以清楚的看出这是scannextpartition,而它的part=1就是第二个分区,也就是我们的p1(第一个为0)这样每个分区都需要重新建模板(扫描下一个分区),就导致了上面的问题。这其实是可以理解的。新的分区是一个新的innodb文件,所以上次定位的持久化游标其实已经没有用了,相当于一个新的表访问。这里还有一个是否构建模板的判断:if(m_prebuilt->sql_stat_start){build_template(false);}andm_prebuilt->sql_stat_start在语句开始的时候设置为true,还是会设置为true每次替换分区如下:ha_innopart::set_partition:m_prebuilt->sql_stat_start=m_sql_stat_start_parts.test(part_id);5.关于一个特殊的过程,我们的faultpstack中还有一个栈如下:这个栈其实并不完整,但是里面出现了Partition_helper::handle_ordered_index_scan,这个函数其实是和分区表排序相关的。如果我们考虑这样一种情况,对于二级索引selectmax(id2)fromt,那么我们需要先访问每个分区获取最大值,然后比较每个分区的最大值得到最终的结果,而MySQL使用了一个优先级队列进行处理,应该是这个函数完成的部分功能(没仔细看)。其次,我们首先出现了范围查询使用的QUICK_RANGE_SELECT,然后构造如下:select*fromtwhereid2<2orderbyid2;stack:这里是因为id2字段只保证分区内按size排列,但是对于整张表而言,是无序的,需要额外处理。六、问题模拟有了这些准备工作,我们就可以构造一个300个字段,25个分区的分区表。测试版本最新8.0.26createtabletpar300col(id1int,id2int,id3int,id4int,...id299varchar(20),id300varchar(20),primarykey(id1),key(id2))engine=innodbpartitionbyrange(id1)(分区p0值小于(100),分区p1值小于(200),分区p3值小于(300),...分区p25值小于(2500));insertintotpar300colvalues(1,1,1,....每个分区一个数据insertintotpar300colvalues(2401,1,1然后构造一些其他数据id2不为1,创建存储过程:delimiter//CREATEPROCEDUREtest300col()begindeclarenumint;setnum=1;whilenum<=1000000doselect*fromtpar300colwhereid2=1;setnum=num+1;endwhile;end//执行:/opt/mysql/mysql3340/install/mysql8/bin/mysql-S--socket=/opt/mysql/mgr3315/data/mgr3315.sock-e'usetest;calltest300col();'>log.log然后perftopobserve如下:所以问题已经确认七、总结一下,这个问题其实是和二级索引相对于partitionkey的数据分散有关,但是我们无法控制二级索引的数据,索引必须也被使用。只能通过某些方面来避免。当然我也提交了一个BUG,如下:https://bugs.mysql.com/bug.ph...不知道有没有办法解决这个问题,比如是practicalforpartitiontables每个partition上的字段都是一样的,难道每次都要rebuildmysql_row_templ_t.clust_rec_field_no吗?如果不需要,问题会自行解决。经官方核实,确实存在该问题。以下是一些避免它的方法。分区表中的字段不要访问太多。你不应该盲目地使用select*Avoidusinghashpartitions。散列分区会增加这个问题。转载自老爷茶馆
