MySQLInnodbPurge簡介

悟道之客發表於2018-09-29

前言

為什麼MySQL InnoDB需要Purge操作?明確這個問題的答案,首先還得從InnoDB的併發機制開始。為了更好的支援併發,InnoDB的多版本一致性讀是採用了基於回滾段的的方式。另外,對於更新和刪除操作,InnoDB並不是真正的刪除原來的記錄,而是設定記錄的delete mark為1。因此為了解決資料Page和Undo Log膨脹的問題,需要引入purge機制進行回收。下面我們來描述下purge整個過程。(程式碼分析基於MySQL 5.7)

Purge資料產生的背景

  • Undo log和Undo history list
  • Mark deleted資料

Undo log

Undo log儲存了記錄修改前的映象。在InnoDB儲存引擎中,undo log分為:

  • insert undo log
  • update undo log

insert undo log是指在insert操作中產生的undo log。由於insert操作的記錄,只是對本事務可見,其他事務不可見,所以undo log可以在事務提交後直接刪除,而不需要purge操作。

update undo log是指在delete和update操作中產生的undo log。該undo log會被後續用於MVCC當中,因此不能提交的時候刪除。提交後會放入undo log的連結串列,等待purge執行緒進行最後的刪除。

下面列出了undo log型別,後續在purge工作執行緒的時候會針對性的講述不同的處理方式:

  • 從表中刪除一行記錄
    TRX_UNDO_DEL_MARK_REC (將主鍵記入日誌),在刪除一條記錄時,並不是真正的將資料從資料庫中刪除,只是標記為已刪除。
  • 向表中插入一行記錄
    TRX_UNDO_INSERT_REC (僅將主鍵記入日誌)
    TRX_UNDO_UPD_DEL_REC (將主鍵記入日誌) 當表中有一條被標記為刪除的記錄和要插入的資料主鍵相同時, 實際的操作是更新這個被標記為刪除的記錄。
  • 更新表中的一條記錄
    TRX_UNDO_UPD_EXIST_REC (將主鍵和被更新了的欄位內容記入日誌)
    TRX_UNDO_DEL_MARK_REC和TRX_UNDO_INSERT_REC,當更新主鍵欄位時,實際執行的過程是刪除舊的記錄然後,再插入一條新的記錄。

Undo history list

事務提交後,對於有update_undo的事務,首先會呼叫trx_serialisation_number_get函式生成一個當前最大的事務號,並且把該事務加到全域性trx_serial_list連結串列中。接下來會根據該事務所在的回滾段資訊

last_page_no == FIL_NULL,來決定是否講該回滾段加入到purge_sys->ib_bh全域性佇列當中。其次,會呼叫trx_undo_update_cleanup->trx_purge_add_update_undo_to_history函式,將undo log加入到history list上,同時記錄這個回滾段上第一個需要purge的undo log資訊,防止rseg再次被新增到purge佇列中,然後喚醒purge執行緒。

	/* Add the log as the first in the history list */
	flst_add_first(rseg_header + TRX_RSEG_HISTORY,
		       undo_header + TRX_UNDO_HISTORY_NODE, mtr);

......

	if (rseg->last_page_no == FIL_NULL) {
		rseg->last_page_no = undo->hdr_page_no;
		rseg->last_offset = undo->hdr_offset;
		rseg->last_trx_no = trx->no;
		rseg->last_del_marks = undo->del_marks;
	}

注意insert_undo並不會放到History list上。

Mark deleted資料

Mark deleted的資料可能會是由於一次delete操作或者一次update操作,InnoDB會呼叫btr_rec_set_deleted_flag->rec_set_deleted_flag_old/rec_set_deleted_flag_new設定record的delete標誌位。

那麼可以標記為mark delete的資料都有哪些型別呢?包括主鍵記錄、二級索引記錄:

btr_cur_del_mark_set_clust_rec

btr_cur_del_mark_set_sec_rec

更新主鍵索引的情況

ha_innobase::update_row -> row_update_for_mysql -> row_upd_step -> row_upd -> row_upd_clust_step -> row_upd_clust_rec_by_insert -> btr_cur_del_mark_set_clust_rec -> row_ins_index_entry

更新非主鍵值,但是影響二級索引的情況

ha_innobase::update_row -> row_update_for_mysql -> row_upd_step -> row_upd -> row_upd_sec_step -> row_upd_sec_index_entry -> btr_cur_del_mark_set_sec_rec -> row_ins_sec_index_entry

Purge操作流程

  • 建立srv_purge_coordinator_thread協調執行緒和srv_worker_thread工作執行緒
  • 啟動purge流程
    • 初始化purge
    • purge工作執行緒流程
    • 清理回滾段undo資料

建立srv_purge_coordinator_thread協調執行緒和srv_worker_thread工作執行緒

 
Purge執行緒包括協調執行緒和工作執行緒,總共數量由innodb_purge_threads設定,最大不能超過32個,執行緒在Innodb啟動時在函式innobase_start_or_create_for_mysql中建立。
 
	/* Create the master thread which does purge and other utility
	operations */

	if (!srv_read_only_mode) {
		os_thread_create(
			srv_master_thread,
			NULL, thread_ids + (1 + SRV_MAX_N_IO_THREADS));
	}

	if (!(srv_read_only_mode)
	    && srv_force_recovery < SRV_FORCE_NO_BACKGROUND) {

		os_thread_create(
			srv_purge_coordinator_thread,
			NULL, thread_ids + 5 + SRV_MAX_N_IO_THREADS);

		ut_a(UT_ARR_SIZE(thread_ids)
		     > 5 + srv_n_purge_threads + SRV_MAX_N_IO_THREADS);

		/* We`ve already created the purge coordinator thread above. */
		for (i = 1; i < srv_n_purge_threads; ++i) {
			os_thread_create(
				srv_worker_thread, NULL,
				thread_ids + 5 + i + SRV_MAX_N_IO_THREADS);
		}

		srv_start_wait_for_purge_to_start();

	} else {
		purge_sys->state = PURGE_STATE_DISABLED;
	}
 
另外,啟動過程還要呼叫trx_purge_sys_init初始化purge_sys相關的變數和建立purge view,後續會講到purge view的作用。

啟動purge流程

purge的主要任務是將資料庫中已經mark del的資料刪除,另外也會批量回收undo pages。資料庫的資料頁很多,要清除被刪除的資料,不可能遍歷所有的資料頁。由於所有的變更都有undo log, 因此,從undo作為切入點,在清理過期的undo的同時,也將資料頁中的被刪除的記錄一併清除。 整個purge操作的入口函式是srv_do_purge->trx_purge。

  • 初始化Purge的記錄:

Purge操作會克隆最老舊的read_view:purge_sys->view->open_purge(),這個purge view包括了重要的兩個資訊:

m_low_limit_no:trx_sys->trx_serial_list最小的提交事務號no,不是事務id。

m_up_limit_id:當前最老read_view中,最小的活躍事務id。

然後,開始獲取那些可以被purge掉的undo records(trx_purge_attach_undo_recs->trx_purge_fetch_next_rec),然後轉化為purge_rec, 輪流放在purge thread的上下文purge_node_t *node->undo_recs中。

那麼從什麼位置開始purge還需要關心purge_sys的兩個變數:

	purge_iter_t	iter;		/* Limit up to which we have read and
					parsed the UNDO log records.  Not
					necessarily purged from the indexes.
					Note that this can never be less than
					the limit below, we check for this
					invariant in trx0purge.cc */
	purge_iter_t	limit;		/* The `purge pointer` which advances
					during a purge, and which is used in
					history list truncation */

 

下面看看如何獲取更新這兩個變數:

首先,先需要確定purge_sys->iter是不能大於purge_sys->limit的,原因是由於purge->limit是用來truncate對應的undo log並且更新history list,而iter用於找到對應del的data record進行purge,我們一定要保證purge del的data後才能purge對應的undo log。

		/* Track the max {trx_id, undo_no} for truncating the
		UNDO logs once we have purged the records. */

		if (purge_sys->iter.trx_no > limit->trx_no
		    || (purge_sys->iter.trx_no == limit->trx_no
			&& purge_sys->iter.undo_no >= limit->undo_no)) {

			*limit = purge_sys->iter;
		}

其次,需要根據purge_sys->next_stored判斷是否當前purge系統中有儲存的purge record,如果沒有就要通過purge queue中儲存的需要purge的回滾段rseg來進行purge record的生成,詳細見函式trx_purge_choose_next_log。在函式trx_purge_get_rseg_with_min_trx_id會更新purge_sys->iter.trx_no成為purge rseg的last_trx_no,也就是指定回滾段上最早提交的事務號。

最後,通過呼叫trx_purge_get_next_rec找到真正需要purge的undo log,並且更新purge_sys->iter。如果該rseg指向的last_page_no的page上並沒有其他可以需要purge的mark del的undo log,那會繼續呼叫trx_purge_rseg_get_next_history_log來獲取下一個history list下的undo page。

在獲取undo log的過程中,還有一個重要的判斷:

	if (purge_sys->iter.trx_no >= purge_sys->view->low_limit_no()) {
		return(NULL);
	}

這意味著,如果purge_sys->iter的trx_no已經大於等於最老讀事務的事務提交號,就放棄該undo log的purge過程,只處理目前已經可以purge的undo log。

目前purge一次處理的undo log為預設300個,可通過引數innodb_purge_batch_size引數調整。

  • Purge工作執行緒流程:

Purge工作執行緒啟動,是藉助於MySQL中的查詢計劃圖(que0que.cc)來排程的,也就是藉助了MySQL Innodb的執行Process Model。簡單可以通過程式碼中的註釋理解:

舉例說明

X := 1;
WHILE X < 5 LOOP
 X := X + 1;
 X := X + 1;
X := 5

將會生成下面的架構,x軸代表下一個執行關係,Y軸代表父子關係

A - W - A
    |
    |
    A - A

A = assign_node_t, W = while_node_t.

 

啟動並行purge的程式碼流程如下:

que_run_threads->que_run_threads_low-> que_thr_step-> row_purge_step-> row_purge

每個work thread需要處理的流程如下:

row_purge->row_purge_record
                  case TRX_UNDO_DEL_MARK_REC:
                            ->row_purge_del_mark->row_purge_remove_sec_if_poss
                                                                   ->row_purge_remove_clust_if_poss
                  case TRX_UNDO_UPD_EXIST_REC:
                            ->row_purge_upd_exist_or_extern->row_purge_remove_clust_if_poss

可以看出work thread要只需要處理兩種情況,一種是由於刪除或者更新導致的mark delete的資料能夠刪除老版本,包括可能的二級索引和一級索引,另一種是處理由於更新非mark delete的資料導致的可能的二級索引老版本。

另外還需要介紹兩個函式row_purge_parse_undo_rec,也就是從undo log裡解析出行引用資訊和其他資訊,返回值為true表明需要執行purge操作。通過函式trx_undo_rec_get_pars獲得undo記錄的型別,主要包括以下幾個型別:

#define TRX_UNDO_INSERT_REC 11 /* fresh insert into clustered index */
#define TRX_UNDO_UPD_EXIST_REC        
  12 /* update of a non-delete-marked 
     record */
#define TRX_UNDO_UPD_DEL_REC                
  13 /* update of a delete marked record to 
     a not delete marked record; also the   
     fields of the record can change */
#define TRX_UNDO_DEL_MARK_REC              
  14 /* delete marking of a record; fields 
     do not change */
#define TRX_UNDO_CMPL_INFO_MULT           
  16 /* compilation info is multiplied by 
     this and ORed to the type above */

#define TRX_UNDO_MODIFY_BLOB              
  64 /* If this bit is set in type_cmpl,  
     then the undo log record has support 
     for partial update of BLOBs. Also to 
     make the undo log format extensible, 
     introducing a new flag next to the   
     type_cmpl flag. */

#define TRX_UNDO_UPD_EXTERN                
  128 /* This bit can be ORed to type_cmpl 
      to denote that we updated external   
      storage fields: used by purge to     
      free the external storage */

通過trx_undo_update_rec_get_sys_cols函式獲取對應的table_id,trx_id,和roll_ptr。另外,為了防止所有對錶的DROP操作,還會對dict_operation_lock加S全域性鎖。

  • Purge一級索引(row_purge_remove_clust_if_poss):

Purge一級索引首先會嘗試樂觀刪除,即直接刪除leaf

row_purge_remove_clust_if_poss_low(BTR_MODIFY_LEAF)->btr_cur_optimistic_delete

失敗後,會不斷嘗試(最多100次)悲觀刪除,即修改tree本身

row_purge_remove_clust_if_poss_low(BTR_MODIFY_TREE)->btr_cur_pessimistic_delete

  • Purge二級索引(row_purge_remove_sec_if_poss):

二級索引purge時,同樣先樂觀刪除(row_purge_remove_sec_if_poss_leaf),失敗再進行悲觀刪除(row_purge_remove_sec_if_poss_tree)。不同的是,需要通過row_purge_poss_sec判斷該二級索引記錄是否可以被Purge,當該二級索引記錄對應的聚集索引記錄沒有delete mark並且其trx id比當前的purge view還舊時,不可以做Purge操作。 

參考資料