一、問題描述
最近遇到一個問題,也就是使用分區表進行數據查詢/加載的時候比普通表的性能下降了約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分區會增加這種問題
到此這篇關于講訴MySQL分區表的一個性能BUG的文章就介紹到這了,更多相關MySQL分區表的一個性能BUG內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://www.tuicool.com/articles/EZFjAfa