您好,登錄后才能下訂單哦!
今天小編給大家分享的是關(guān)于MySQL8.0 InnoDB并行執(zhí)行的詳解,很多人都不太了解,今天小編為了讓大家更加了解MySQL8.0,所以給大家總結(jié)了以下內(nèi)容,一起往下看吧。一定會(huì)有所收獲的哦。
概述
MySQL經(jīng)過多年的發(fā)展已然成為最流行的數(shù)據(jù)庫(kù),廣泛用于互聯(lián)網(wǎng)行業(yè),并逐步向各個(gè)傳統(tǒng)行業(yè)滲透。之所以流行,一方面是其優(yōu)秀的高并發(fā)事務(wù)處理的能力,另一方面也得益于MySQL豐富的生態(tài)。MySQL在處理OLTP場(chǎng)景下的短查詢效果很好,但對(duì)于復(fù)雜大查詢則能力有限。最直接一點(diǎn)就是,對(duì)于一個(gè)SQL語(yǔ)句,MySQL最多只能使用一個(gè)CPU核來(lái)處理,在這種場(chǎng)景下無(wú)法發(fā)揮主機(jī)CPU多核的能力。MySQL沒有停滯不前,一直在發(fā)展,新推出的8.0.14版本第一次引入了并行查詢特性,使得check table和select count(*)類型的語(yǔ)句性能成倍提升。雖然目前使用場(chǎng)景還比較有限,但后續(xù)的發(fā)展值得期待。
使用方式
通過配置參數(shù)innodb_parallel_read_threads來(lái)設(shè)置并發(fā)線程數(shù),就能開始并行掃描功能,默認(rèn)這個(gè)值為4。我這里做一個(gè)簡(jiǎn)單的實(shí)驗(yàn),通過sysbench導(dǎo)入2億條數(shù)據(jù),分別配置innodb_parallel_read_threads
為1,2,4,8,16,32,64,測(cè)試并行執(zhí)行的效果。測(cè)試語(yǔ)句為select count(*) from sbtest1;
橫軸是配置并發(fā)線程數(shù),縱軸是語(yǔ)句執(zhí)行時(shí)間。從測(cè)試結(jié)果來(lái)看,整個(gè)并行表現(xiàn)還是不錯(cuò)的,掃描2億條記錄,從單線程的18s,下降到32線程的1s。后面并發(fā)開再多,由于數(shù)據(jù)量有限,多線程的管理消耗超過了并發(fā)帶來(lái)的性能提升,不能再繼續(xù)縮短SQL執(zhí)行時(shí)間。
MySQL并行執(zhí)行
實(shí)際上目前MySQL的并行執(zhí)行還處于非常初級(jí)階段,如下圖所示,左邊是之前MySQL串行處理單個(gè)SQL形態(tài);中間的是目前MySQL版本提供的并行能力,InnoDB引擎并行掃描的形態(tài);最右邊的是未來(lái)MySQL要發(fā)展的形態(tài),優(yōu)化器根據(jù)系統(tǒng)負(fù)載和SQL生成并行計(jì)劃,并將分區(qū)計(jì)劃下發(fā)給執(zhí)行器并行執(zhí)行。并行執(zhí)行不僅僅是并行掃描,還包括并行聚集,并行連接,并行分組,以及并行排序等。目前版本MySQL的上層的優(yōu)化器以及執(zhí)行器并沒有配套的修改。因此,下文的討論主要集中在InnoDB引擎如何實(shí)現(xiàn)并行掃描,主要包括分區(qū),并行掃描,預(yù)讀以及與執(zhí)行器交互的適配器類。
分區(qū)
并行掃描的一個(gè)核心步驟就是分區(qū),將掃描的數(shù)據(jù)劃分成多份,讓多個(gè)線程并行掃描。InnoDB引擎是索引組織表,數(shù)據(jù)以B+tree的形式存儲(chǔ)在磁盤上,節(jié)點(diǎn)的單位是頁(yè)面(block/page),同時(shí)緩沖池中會(huì)對(duì)熱點(diǎn)頁(yè)面進(jìn)行緩存,并通過LRU算法進(jìn)行淘汰。分區(qū)的邏輯就是,從根節(jié)點(diǎn)頁(yè)面出發(fā),逐層往下掃描,當(dāng)判斷某一層的分支數(shù)超過了配置的線程數(shù),則停止拆分。在實(shí)現(xiàn)時(shí),實(shí)際上總共會(huì)進(jìn)行兩次分區(qū),第一次是按根節(jié)點(diǎn)頁(yè)的分支數(shù)劃分分區(qū),每個(gè)分支的最左葉子節(jié)點(diǎn)的記錄為左下界,并將這個(gè)記錄記為相鄰上一個(gè)分支的右上界。通過這種方式,將B+tree劃分成若干子樹,每個(gè)子樹就是一個(gè)掃描分區(qū)。經(jīng)過第一次分區(qū)后,可能出現(xiàn)分區(qū)數(shù)不能充分利用多核問題,比如配置了并行掃描線程為3,第一次分區(qū)后,產(chǎn)生了4個(gè)分區(qū),那么前3個(gè)分區(qū)并行做完后,第4個(gè)分區(qū)至多只有一個(gè)線程掃描,最終效果就是不能充分利用多核資源。
二次分區(qū)
為了解決這個(gè)問題,8.0.17版本引入了二次分區(qū),對(duì)于第4個(gè)分區(qū),繼續(xù)下探拆分,這樣多個(gè)子分區(qū)又能并發(fā)掃描,InnoDB引擎并發(fā)掃描的最小粒度是頁(yè)面級(jí)別。具體判斷二次分區(qū)的邏輯是,一次分區(qū)后,若分區(qū)數(shù)大于線程數(shù),則編號(hào)大于線程數(shù)的分區(qū),需要繼續(xù)進(jìn)行二次分區(qū);若分區(qū)數(shù)小于線程數(shù)且B+tree層次很深,則所有的分區(qū)都需要進(jìn)行二次分區(qū)。
相關(guān)代碼如下:
split_point = 0; if (ranges.size() > max_threads()) { //最后一批分區(qū)進(jìn)行二次分區(qū) split_point = (ranges.size() / max_threads()) * max_threads(); } else if (m_depth < SPLIT_THRESHOLD) { /* If the tree is not very deep then don't split. For smaller tables it is more expensive to split because we end up traversing more blocks*/ split_point = max_threads(); } else { //如果B+tree的層次很深(層數(shù)大于或等于3,數(shù)據(jù)量很大),則所有分區(qū)都需要進(jìn)行二次分區(qū) }
無(wú)論是一次分區(qū),還是二次分區(qū),分區(qū)邊界的邏輯都一樣,以每個(gè)分區(qū)的最左葉子節(jié)點(diǎn)的記錄為左下界,并且將這個(gè)記錄記為相鄰上一個(gè)分支的右上界。這樣確保分區(qū)足夠多,粒度足夠細(xì),充分并行。下圖展示了配置為3的并發(fā)線程,掃描進(jìn)行二次分區(qū)的情況。
相關(guān)代碼如下:
create_ranges(size_t depth, size_t level) 一次分區(qū): parallel_check_table add_scan partition(scan_range, level=0) /* start at root-page */ create_ranges(scan_range, depth=0, level=0) create_contexts(range, index >= split_point) 二次分區(qū): split() partition(scan_range, level=1) create_ranges(depth=0,level)
并行掃描
在一次分區(qū)后,將每個(gè)分區(qū)掃描任務(wù)放入到一個(gè)lock-free隊(duì)列中,并行的worker線程從隊(duì)列中獲取任務(wù),執(zhí)行掃描任務(wù),如果獲取的任務(wù)帶有split屬性,這個(gè)時(shí)候worker會(huì)將任務(wù)進(jìn)行二次拆分,并投入到隊(duì)列中。這個(gè)過程主要包括兩個(gè)核心接口,一個(gè)是工作線程接口,另外一個(gè)是遍歷記錄接口,前者從隊(duì)列中獲取任務(wù)并執(zhí)行,并維護(hù)統(tǒng)計(jì)計(jì)數(shù);后者根據(jù)可見性獲取合適的記錄,并通過上層注入的回調(diào)函數(shù)處理,比如計(jì)數(shù)等。
Parallel_reader::worker(size_t thread_id)
{
1.從ctx-queue提取ctx任務(wù)
2.根據(jù)ctx的split屬性,確定是否需要進(jìn)一步拆分分區(qū)(split())
3.遍歷分區(qū)所有記錄(traverse())
4.一個(gè)分區(qū)任務(wù)結(jié)束后,維護(hù)m_n_completed計(jì)數(shù)
5.如果m_n_compeleted計(jì)數(shù)達(dá)到ctx數(shù)目,喚醒所有worker線程結(jié)束
6.根據(jù)traverse接口,返回err信息。
}
Parallel_reader::Ctx::traverse()
{
1.根據(jù)range設(shè)置pcursor
2.找到btree,將游標(biāo)定位到range的起始位置
3.判斷可見性(check_visibility)
4.如果可見,根據(jù)回調(diào)函數(shù)計(jì)算(比如統(tǒng)計(jì))
5.向后遍歷,若達(dá)到了頁(yè)面的最后一條記錄,啟動(dòng)預(yù)讀機(jī)制(submit_read_ahead)
6.超出范圍后結(jié)束
}
同時(shí)在8.0.17版本還引入了預(yù)讀機(jī)制,避免因?yàn)镮O瓶頸導(dǎo)致并行效果不佳的問題。目前預(yù)讀的線程數(shù)不能配置,在代碼中硬編碼為2個(gè)線程。每次預(yù)讀的單位是一個(gè)簇(InnoDB文件通過段,簇,頁(yè)三級(jí)結(jié)構(gòu)管理,一個(gè)簇是一組連續(xù)的頁(yè)),根據(jù)頁(yè)面配置的大小,可能為1M或者2M。對(duì)于常見的16k頁(yè)面配置,每次預(yù)讀1M,也就是64個(gè)頁(yè)面。worker線程在進(jìn)行掃描時(shí),會(huì)先判斷相鄰的下一個(gè)頁(yè)面是否為簇的第一個(gè)頁(yè)面,如果是,則發(fā)起預(yù)讀任務(wù)。預(yù)讀任務(wù)同樣通過lock-free 隊(duì)列緩存,worker線程是生產(chǎn)者,read-ahead-worker是消費(fèi)者。由于所有分區(qū)頁(yè)面沒有重疊,因此預(yù)讀任務(wù)也不會(huì)重復(fù)。
執(zhí)行器交互(適配器)
實(shí)際上,MySQL已經(jīng)封裝了一個(gè)適配器類Parallel_reader_adapter來(lái)供上層使用,為后續(xù)的更豐富的并行執(zhí)行做準(zhǔn)備。首先這個(gè)類需要解決記錄格式的問題,將引擎層掃描的記錄轉(zhuǎn)換成MySQL格式,這樣做到上下層解耦,執(zhí)行器不用感知引擎層格式,統(tǒng)一按MySQL格式處理。整個(gè)過程是一個(gè)流水線,通過一個(gè)buffer批量存儲(chǔ)MySQL記錄,worker線程不停的將記錄從引擎層上讀上來(lái),同時(shí)有記錄不停的被上層處理,通過buffer可以平衡讀取和處理速度的差異,確保整個(gè)過程流動(dòng)起來(lái)。緩存大小默認(rèn)是2M,根據(jù)表的記錄行長(zhǎng)來(lái)確定buffer可以緩存多少個(gè)MySQL記錄。核心流程主要在process_rows接口中,流程如下
process_rows
{
1.將引擎記錄轉(zhuǎn)換成MySQL記錄
2.獲取本線程的buffer信息(轉(zhuǎn)換了多少mysql記錄,發(fā)送了多少給上層)
3.將MySQL記錄填充進(jìn)buffer,自增統(tǒng)計(jì)m_n_read
4.調(diào)用回調(diào)函數(shù)處理(比如統(tǒng)計(jì),聚合,排序等),自增統(tǒng)計(jì)m_n_send
}
對(duì)于調(diào)用者來(lái)說,需要設(shè)置表的元信息,以及注入處理記錄回調(diào)函數(shù),比如處理聚集,排序,分組的工作。回調(diào)函數(shù)通過設(shè)置m_init_fn,m_load_fn和m_end_fn來(lái)控制。
總結(jié)
MySQL8.0引入了并行查詢雖然還比較初級(jí),但已經(jīng)讓我們看到了MySQL并行查詢的潛力,從實(shí)驗(yàn)中我們也看到了開啟并行執(zhí)行后,SQL語(yǔ)句執(zhí)行充分發(fā)揮了多核能力,響應(yīng)時(shí)間急劇下降。相信在不久的將來(lái),8.0的會(huì)支持更多并行算子,包括并行聚集,并行連接,并行分組以及并行排序等。
看完上文,你對(duì)關(guān)于MySQL8.0 InnoDB并行執(zhí)行大概了解了嗎?如果想了解更多,歡迎關(guān)注億速云行業(yè)資訊頻道哦
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。