(转)前端开发之 MySQL 分区表中的性能 BUG
一、问题描述
最近遇到一个问题,也就是使用分区表进行数据查询/加载的时候比普通表的性能下降了约 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 耗在
对于 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 就出现了上面的逻辑,大概逻辑如下:
这里我们看到这里实际上有 2 层循环,也就是循环套循环(时间复杂度 O(M×N)),而循环影响最大的有 2 个地方:
第一层,表中字段的多少
第二层,需要访问的字段(读和写都算)在主键(也就是全部字段)中循环
这里也就是为什么这里会慢的原因。但是 template 通常不会一个查询进行多次建立,比如一个普通表的大查询,只有在语句第一次进行数据定位之前会进行建立,这就不得不说这是分区表和普通表的对比中一个特殊的地方了。下面描述一下。
四、分区表中多次建立 template 的情况
假设我们有如下的分区表:
我们使用语句"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 建立还有一个判断如下:
而 m_prebuilt->sql_stat_start 除了在语句开始的时候设置为 true,每次更换分区依旧会设置为 true 如下:
五、关于一个特殊的流程
在我们的故障 pstack 中还有一个栈如下:
这个栈实际并不完整,但是其中出现了 Partition_helper::handle_ordered_index_scan,这个函数实际上和分区表的排序有关,如果我们考虑这样一种情况,对于二级索引 select max(id2) from t,那么需要首先访问每个分区获取其中的最大值然后对比每个分区的最大值,得到最终的结果,而 MySQL 则采用优先队列进行处理,这应该是就是本函数完成的部分功能(没仔细去看)。其次我们先出现了 QUICK_RANGE_SELECT 这是范围查询会用到的,那么我们构造如下:
栈:
这里就是因为 id2 这个字段只保证在分区内部是按照大小排列的但是在整个表来讲,它是无序的,需要额外的处理。
六、问题模拟
有了这些准备,我们可以构造一个 300 个字段和 25 个分区的分区表。测试版本最新 8.0.26
然后 perf top 观察如下:
这样问题就得到了确认。
七、总结
这个问题实际上和二级索引相对于分区键的数据离散度有关,但是我们无法控制二级索引的数据,并且索引也是必须使用的。只能通过一些方面尽量避免,当然我也提交了一个 BUG,如下:
https://bugs.mysql.com/bug.php?id=104576
不知道是否有办法修复这个问题,比如对于分区表来讲实际上每个分区的字段都是一样的,是否需要每次都重建 mysql_row_templ_t.clust_rec_field_no?如果不需要那么问题自解,官方目前已经验证了这个问题确实存在。如下是一些避免的方式,
分区表字段不宜过多
访问的字段不应该一味的使用 select *
避免使用 hash 分区,hash 分区会增加这种问题
转载于老叶茶馆
评论