日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > 数据库 >内容正文

数据库

mysql change index_MySQL · 引擎特性 · Innodb change buffer介绍

發(fā)布時(shí)間:2023/12/10 数据库 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 mysql change index_MySQL · 引擎特性 · Innodb change buffer介绍 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

前言

在前面幾期月報(bào)我們介紹了undo log、redo log以及InnoDB如何崩潰恢復(fù)來實(shí)現(xiàn)數(shù)據(jù)ACID的相關(guān)知識(shí)。本期我們介紹另外一種重要的數(shù)據(jù)變更日志,也就是InnoDB change buffer。 Change buffer的主要目的是將對(duì)二級(jí)索引的數(shù)據(jù)操作緩存下來,以此減少二級(jí)索引的隨機(jī)IO,并達(dá)到操作合并的效果。

在MySQL5.5之前的版本中,由于只支持緩存insert操作,所以最初叫做insert buffer,只是后來的版本中支持了更多的操作類型緩存,才改叫change buffer,這也是為什么代碼中有大量的ibuf前綴開頭的函數(shù)或變量。為了表達(dá)方面,本文也將change buffer縮寫為ibuf。

由于歷史上ibuf的數(shù)據(jù)格式曾發(fā)生過多次變化,本文討論的相關(guān)內(nèi)容基于如下設(shè)定:

版本為5.5及之后的版本,不涉及舊版本的邏輯,innodb_change_buffering 設(shè)置為ALL,表示緩存所有操作。

ibuf btree

change buffer的物理上是一顆普通的btree,存儲(chǔ)在ibdata系統(tǒng)表空間中,根頁(yè)為ibdata的第4個(gè)page(FSP_IBUF_TREE_ROOT_PAGE_NO)。

一條ibuf 記錄大概包含如下列:

ibuf btree通過三列(space id, page no , counter)作為主鍵來唯一決定一條記錄,其中counter是一個(gè)遞增值,目的是為了維持不同操作的有序性,例如可以通過counter來保證在merge時(shí)執(zhí)行如下序列時(shí)的循序和用戶操作順序是一致的:INSERT x, DELETE-MARK x, INSERT x。

在插入ibuf記錄前我們是不知道counter的值的,因此總是先將對(duì)應(yīng)tuple的counter設(shè)置為0xFFFF,然后將cursor以模式PAGE_CUR_LE定位到小于等于(space id, page no, 0xFFFF)的位置,新記錄的counter為當(dāng)前位置記錄counter值加1。

ibuf btree最大默認(rèn)為buffer pool size的25%,當(dāng)超過25%時(shí),可能觸發(fā)用戶線程同步縮減ibuf btree。為何要將ibuf btree的大小和buffer pool大小相關(guān)聯(lián)呢 ? 一個(gè)比較重要的原因是防止ibuf本身占用過多的buffer pool資源。

ibuf bitmap

由于ibuf 緩存的操作都是針對(duì)某個(gè)具體page的,因此在緩存操作時(shí)必須保證該操作不會(huì)導(dǎo)致空page 或索引分裂。

針對(duì)第一種情況,即避免空page,主要是對(duì)purge線程而言,因?yàn)橹挥衟urge線程才會(huì)去真正的刪除二級(jí)索引上的物理記錄。在準(zhǔn)備插入類型為IBUF_OP_DELETE的操作緩存時(shí),會(huì)預(yù)估在apply完該page上所有的ibuf entry后還剩下多少記錄(ibuf_get_volume_buffered),如果只剩下一條記錄,則拒絕本次purge操作緩存,改走正常的讀入物理頁(yè)邏輯。

針對(duì)第二種情況,InnoDB通過一種特殊的page來維護(hù)每個(gè)數(shù)據(jù)頁(yè)的空閑空間大小,也就是ibuf bitmap page,該page存在于每個(gè)ibd文件中,具有固定的page no,其文件結(jié)構(gòu)如下圖所示:

ibuf bitmap使用4個(gè)bit來描述一個(gè)page:

IBUF_BITMAP_FREE:使用2個(gè)bit來描述空閑空間大小,以16KB的page size為例,能表示的空閑空間范圍為0(0 bytes)、1(512 bytes)、2(1024 bytes)、3(2048 bytes)。很顯然,能夠緩存的二級(jí)索引記錄最大不可能超過2048字節(jié)。

由于只有INSERT操作才可能導(dǎo)致page記錄滿,因此只需要對(duì)IBUF_OP_INSERT類型的操作進(jìn)行判斷:

ibuf_insert_low:

if (op == IBUF_OP_INSERT) {

ulint bits = ibuf_bitmap_page_get_bits(

bitmap_page, page_no, zip_size, IBUF_BITMAP_FREE,

&bitmap_mtr);

if (buffered + entry_size + page_dir_calc_reserved_space(1)

> ibuf_index_page_calc_free_from_bits(zip_size, bits)) {

/* Release the bitmap page latch early. */

ibuf_mtr_commit(&bitmap_mtr);

/* It may not fit */

do_merge = TRUE;

ibuf_get_merge_page_nos(FALSE,

btr_pcur_get_rec(&pcur), &mtr,

space_ids, space_versions,

page_nos, &n_stored);

goto fail_exit;

}

}

其中ibuf_bitmap_page_get_bits函數(shù)根據(jù)space id 和page no 獲取對(duì)應(yīng)的bitmap page,找到空閑空間描述信息;如果本次插入操作可能超出限制,則從當(dāng)前cursor位置附近開始,觸發(fā)一次異步的ibuf merge,目的是盡量將當(dāng)前page的緩存操作做一次合并。

在正常的對(duì)物理頁(yè)的DML過程中,如果page內(nèi)空間發(fā)生了變化,總是需要去更新對(duì)應(yīng)的IBUF_BITMAP_FREE值。參考函數(shù):btr_compress、btr_cur_optimistic_insert。

IBUF_BITMAP_BUFFERED:用于表示該page是否有操作緩存,在ibuf_insert_low函數(shù)中,準(zhǔn)備插入ibuf btree前設(shè)置成true。二級(jí)索引物理頁(yè)讀入內(nèi)存時(shí)會(huì)根據(jù)該標(biāo)記位判斷是否需要進(jìn)行ibuf merge操作。

IBUF_BITMAP_IBUF:表示該數(shù)據(jù)頁(yè)是否是ibuf btree的一部分,該標(biāo)記位主要用于異步AIO讀操作。InnoDB專門為change buffer模塊分配了一個(gè)后臺(tái)AIO線程,如果page屬于change buffer的b樹,則使用該線程做異步讀,參考函數(shù):ibuf_page_low

操作類型

InnoDB change buffer可以對(duì)三種類型的操作進(jìn)行緩存:INSERT、DELETE-MARK 、DELETE操作,前兩種對(duì)應(yīng)用戶線程操作,第三種則由purge操作觸發(fā)。

用戶可以通過參數(shù)innodb_change_buffering來控制緩存何種操作:

/** Allowed values of innodb_change_buffering */

static const char* innobase_change_buffering_values[IBUF_USE_COUNT] = {

"none", /* IBUF_USE_NONE */

"inserts", /* IBUF_USE_INSERT */

"deletes", /* IBUF_USE_DELETE_MARK */

"changes", /* IBUF_USE_INSERT_DELETE_MARK */

"purges", /* IBUF_USE_DELETE */

"all" /* IBUF_USE_ALL */

};

innodb_change_buffering默認(rèn)值為all,表示緩存所有操作。注意由于在二級(jí)索引上的更新操作總是先delete-mark,再insert新記錄,因此update會(huì)產(chǎn)生兩條ibuf entry。

緩存條件

只有滿足一定條件時(shí),操作才會(huì)被緩存,所有對(duì)ibuf操作的判斷,都從btr_cur_search_to_nth_level入口,該函數(shù)用于定位到btree上滿足條件的記錄,大概的判斷條件如下:

用戶設(shè)置了選項(xiàng)innodb_change_buffering;

只有葉子節(jié)點(diǎn)才會(huì)去考慮是否使用ibuf;

對(duì)于聚集索引,不可以緩存操作;

對(duì)于唯一二級(jí)索引(unique key),由于索引記錄具有唯一性,因此無法緩存插入操作,但可以緩存刪除操作;

表上沒有flush 操作,例如執(zhí)行flush table for export時(shí),不允許對(duì)表進(jìn)行 ibuf 緩存 (通過dict_table_t::quiesce 進(jìn)行標(biāo)識(shí))

參考函數(shù):ibuf_should_try:

當(dāng)滿足ibuf緩存條件時(shí),會(huì)使用兩種模式去嘗試獲取數(shù)據(jù)頁(yè):

BUF_GET_IF_IN_POOL: 如果數(shù)據(jù)頁(yè)在內(nèi)存中,則獲取page并返回,否則返回NULL;

BUF_GET_IF_IN_POOL_OR_WATCH:如果數(shù)據(jù)頁(yè)在內(nèi)存中,則獲取page并返回,否則為請(qǐng)求的page設(shè)置一個(gè)`sentinel`(buf_pool_watch_set),相當(dāng)于標(biāo)記這個(gè)page,表示這個(gè)page上的記錄正在被purge。(下一小節(jié)介紹)

前者是前臺(tái)用戶線程觸發(fā),后者為purge線程在物理清除無效數(shù)據(jù)時(shí)觸發(fā),如果數(shù)據(jù)已經(jīng)在內(nèi)存中了,則不進(jìn)行緩存。隨后進(jìn)入函數(shù)ibuf_insert,經(jīng)過一系列的檢查后(不可產(chǎn)生空page 和索引分裂、未超出最大ibuf size限制)執(zhí)行操作緩存。

purge操作緩存

對(duì)于purge操作,當(dāng)page不存在于內(nèi)存時(shí)設(shè)置的sentinel是什么鬼?它是如何設(shè)置的,什么時(shí)候會(huì)被清理掉,這幾個(gè)問題涉及到purge操作的緩存流程:

如何設(shè)置sentinel

當(dāng)purge線程嘗試讀入page時(shí),若數(shù)據(jù)頁(yè)不在buffer pool中,則調(diào)用函數(shù)buf_pool_watch_set,分為兩步:

Step1: 首先檢查page hash,如果存在于page hash中:1)若未被設(shè)置成sentinel (別的線程將數(shù)據(jù)頁(yè)讀入內(nèi)存時(shí)會(huì)清理掉對(duì)應(yīng)標(biāo)記),返回?cái)?shù)據(jù)頁(yè);2)否則返回NULL;

Step2: 若page hash中不存在,則從buf_pool_t::watch數(shù)組中找到一個(gè)空閑的(狀態(tài)為BUF_BLOCK_POOL_WATCH)page控制結(jié)構(gòu)體對(duì)象buf_page_t,將其狀態(tài)設(shè)置為BUF_BLOCK_ZIP_PAGE,初始化相關(guān)變量,并插入到page hash中。buf_pool_t::watch數(shù)組的大小為purge線程的個(gè)數(shù),這意味著即使所有purge線程同時(shí)訪問同一個(gè)buffer pool instance,總會(huì)擁有一個(gè)空閑的watch數(shù)組對(duì)象。

判斷是否可以緩存purge操作

當(dāng)設(shè)置sentinel并返回后,在決定緩存purge之前,需要去判斷是否有別的線程對(duì)同一條記錄緩存了新的操作,舉個(gè)簡(jiǎn)單的例子:

Step 1: delete-mark X (sec index) //session 1

Step 2: insert X (clust index) //session 1

Step 3: delete X(sec index) //purge thread

Step 4: insert X (sec index) //session 1

如果二級(jí)索引頁(yè)在內(nèi)存中,那么Step 3 和Step4必然是有序的,因?yàn)樾枰@取block鎖才能進(jìn)行數(shù)據(jù)變更操作。但數(shù)據(jù)頁(yè)不在內(nèi)存時(shí),就需要確保Step 4在Step 3之后執(zhí)行。因此在緩存purge操作之前,需要根據(jù)當(dāng)前要清理的記錄,找到對(duì)應(yīng)的聚集索引記錄,并檢查相比當(dāng)前purge線程的readview是否有新版本的聚集索引記錄(即有新的插入操作發(fā)生)。

如果檢查到有新的插入,則本次purge操作直接放棄。因?yàn)楫?dāng)符合一定條件時(shí),Step 4的操作可以直接把Step1產(chǎn)生的記錄刪除標(biāo)記清除掉,重用物理空間。

參考函數(shù):row_purge_poss_sec:

但是注意上述檢查流程結(jié)束時(shí),會(huì)在函數(shù)row_purge_poss_sec中將mtr提交掉,對(duì)應(yīng)的聚集索引頁(yè)的Latch會(huì)被釋放掉,這意味著可能出現(xiàn)如下序列:

Step 1: delete-mark X;

Step 2: delete X,purge線程為其設(shè)置watch,并完成在函數(shù)row_purge_poss_sec中的檢查,準(zhǔn)備插入ibuf

Step 3: insert X,索引頁(yè)不在內(nèi)存,準(zhǔn)備插入ibuf

在函數(shù)ibuf_insert中,針對(duì)IBUF_OP_INSERT和IBUF_OP_DELETE_MARK操作,會(huì)去檢查是否對(duì)應(yīng)的二級(jí)索引頁(yè)被設(shè)置成sentinel(buf_page_get_also_watch),如果是的話,表明當(dāng)前有一個(gè)pending的purge操作,目前的處理邏輯是放棄insert和delete-mark的緩存操作,轉(zhuǎn)而讀取物理頁(yè)。

綜上,如果purge操作先進(jìn)入ibuf_insert,則對(duì)應(yīng)二級(jí)索引頁(yè)的watch必然被設(shè)置,insert操作將放棄緩存,轉(zhuǎn)而嘗試讀入索引頁(yè);如果insert先進(jìn)入ibuf_insert,則purge操作的緩存放棄。

即使Purge線程完成一系列檢查,進(jìn)入緩存階段,這時(shí)候用戶線程依舊可能會(huì)去讀入物理頁(yè);有沒有可能導(dǎo)致purge操作丟失呢 ?答案是否定的!因?yàn)閜urge線程在緩存操作時(shí)先將cursor定位到ibuf btree上,對(duì)應(yīng)的ibuf page已將加上latch;而用戶線程如果讀入物理頁(yè),為了merge ibuf entry,也需要請(qǐng)求page latch;當(dāng)purge線程在拿到latch后,會(huì)再檢查一次看看物理頁(yè)是否已讀入內(nèi)存(buf_pool_watch_occurred),如果是的話,則放棄本次緩存。

何時(shí)清理sentinel

有兩種情況會(huì)清理sentinel:

第一種情況是purge操作完成緩存后(或者判斷無法進(jìn)行purge緩存)進(jìn)行清理;

第二種情況是從磁盤讀入文件塊的時(shí)候,會(huì)調(diào)用buf_page_init_for_read->buf_page_init初始化一個(gè)page對(duì)象。這時(shí)候會(huì)做一個(gè)判斷,如果將被讀入的page被設(shè)置為sentinel(在watch數(shù)組中被設(shè)置),則調(diào)用buf_pool_watch_remove將其從page hash中移除,對(duì)應(yīng)bp->watch的數(shù)據(jù)元素被重置成空閑狀態(tài)。

ibuf merge

有以下幾種場(chǎng)景會(huì)觸發(fā)ibuf merge操作:

用戶線程選擇二級(jí)索引進(jìn)行數(shù)據(jù)查詢,這時(shí)候必須要讀入二級(jí)索引頁(yè),相應(yīng)的ibuf entry需要merge到Page中。

當(dāng)嘗試緩存插入操作時(shí),如果預(yù)估page的空間不足,可能導(dǎo)致索引分裂,則定位到嘗試緩存的page no在ibuf btree中的位置,最多merge 8個(gè)(IBUF_MERGE_AREA) page,merge方式為異步,即發(fā)起異步讀索引頁(yè)請(qǐng)求。

參考函數(shù):ibuf_insert_low —> ibuf_get_merge_page_nos_func

若當(dāng)前ibuf tree size 超過ibuf->max_size + 10(IBUF_CONTRACT_DO_NOT_INSERT)時(shí),執(zhí)行一次同步的ibuf merge(ibuf_contract),merge的page no為隨機(jī)定位的cursor,最多一次merge 8個(gè)page,同時(shí)放棄本次緩存。

其中ibuf->max_size默認(rèn)為25% * buffer pool size,百分比由參數(shù)innodb_change_buffer_max_size控制,可動(dòng)態(tài)調(diào)整。

參考函數(shù):ibuf_insert_low —> ibuf_contract

若本次插入ibuf操作可能產(chǎn)生ibuf btree索引分裂(BTR_MODIFY_TREE)時(shí):

當(dāng)前ibuf->size < ibuf->max_size, 不做處理;

當(dāng)前ibuf->size >= ibuf->max_size + 5 (IBUF_CONTRACT_ON_INSERT_SYNC)時(shí),執(zhí)行一次同步ibuf merge,位置隨機(jī);

當(dāng)前Ibuf->size介于ibuf->max_size 和ibuf->max_size +5 之間時(shí)。執(zhí)行一次異步ibuf merge,位置隨機(jī)。

參考函數(shù):ibuf_insert_low —> ibuf_contract_after_insert

后臺(tái)master線程發(fā)起merge

master線程有三種工作狀態(tài):

IDLE:實(shí)例處于空閑狀態(tài),以100%的io capacity來作merge操作:

n_pages = PCT_IO(100);

相當(dāng)于一次merge的page數(shù)等于innodb_io_capacity

參考函數(shù):srv_master_do_idle_tasks

ACTIVE:實(shí)例處于活躍狀態(tài),這時(shí)候會(huì)以如下算法計(jì)算需要merge的page數(shù):

/* By default we do a batch of 5% of the io_capacity */

n_pages = PCT_IO(5);

mutex_enter(&ibuf_mutex);

/* If the ibuf->size is more than half the max_size

then we make more agreesive contraction.

+1 is to avoid division by zero. */

if (ibuf->size > ibuf->max_size / 2) {

ulint diff = ibuf->size - ibuf->max_size / 2;

n_pages += PCT_IO((diff * 100)

/ (ibuf->max_size + 1));

}

mutex_exit(&ibuf_mutex);

可見在系統(tǒng)active時(shí),會(huì)以比較溫和的方式去做merge,如果當(dāng)前ibuf btree size超過最大值的一半,則嘗試多做一些merge操作。

參考函數(shù): srv_master_do_active_tasks

SHUTDOWN:當(dāng)執(zhí)行slow shutdown時(shí),會(huì)強(qiáng)制做一次全部的ibuf merge

參考函數(shù):srv_master_do_shutdown_tasks

對(duì)某個(gè)表執(zhí)行flush table 操作時(shí),會(huì)觸發(fā)對(duì)該表的強(qiáng)制ibuf merge,例如執(zhí)行:

flush table tbname for export;

flush table tbname with read lock;

實(shí)際上強(qiáng)制ibuf merge主要是為flush for export準(zhǔn)備的,當(dāng)執(zhí)行該命令后,為了保證能安全的將ibd拷貝到其他實(shí)例上, 需要對(duì)該表應(yīng)用全部的ibuf 緩存。

參考函數(shù):row_quiesce_table_start

“著名” bug

在change buffer的應(yīng)用史上,最著名的bug要屬 bug#61104,其現(xiàn)象為當(dāng)實(shí)例意外crash后,無法從崩潰中恢復(fù),錯(cuò)誤日志中報(bào)如下斷言:

InnoDB: Failing assertion: page_get_n_recs(page) > 1

最初官方花了很長(zhǎng)的時(shí)間都沒有找到這個(gè)問題的root cause,只能加了一些代碼邏輯避免不斷crash重啟,讓用戶有機(jī)會(huì)登錄實(shí)例,重建二級(jí)索引。

后來Percona的開發(fā)人員Alexey Kopytov在bug#66819 提出了該問題的根本原因,指出ibuf entry的刪除和merge 并不是一個(gè)原子的操作(即處于兩個(gè)mtr事務(wù)中),當(dāng)merge ibuf的mtr提交后crash,就可能在重啟時(shí)重復(fù)做ibuf merge。如果上次執(zhí)行DELETE操作導(dǎo)致對(duì)應(yīng)索引頁(yè)上記錄數(shù)只剩下一條,第二次apply時(shí)認(rèn)為本次操作會(huì)產(chǎn)生空頁(yè),從而導(dǎo)致斷言錯(cuò)誤。

官方很快根據(jù)Alexey的意見做了修復(fù),修復(fù)方式也比較簡(jiǎn)單:

在第一個(gè)mtr里,merge ibuf entry 到二級(jí)索引頁(yè),并標(biāo)記刪除ibuf entry,提交mtr;

在第二個(gè)mtr里,執(zhí)行真正的悲觀刪除ibuf entry;

在執(zhí)行merge操作前,對(duì)于被delete mark的ibuf entry,不做merge操作。

具體的參考函數(shù):ibuf_merge_or_delete_for_page 和 ibuf_delete_rec。

比較烏龍的是,我們發(fā)現(xiàn)第一次修復(fù)并沒有處理purge線程產(chǎn)生的delete緩存;我們將該發(fā)現(xiàn)公布到社區(qū),很快得到了響應(yīng),并由上游快速fix掉了,因此完整的補(bǔ)丁分布在兩個(gè)版本中:

總結(jié)

以上是生活随笔為你收集整理的mysql change index_MySQL · 引擎特性 · Innodb change buffer介绍的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。