共計 10661 個字符,預計需要花費 27 分鐘才能閱讀完成。
這篇文章主要講解了“MySQL 中 Innodb page clean 線程分析”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著丸趣 TV 小編的思路慢慢深入,一起來研究和學習“MySQL 中 Innodb page clean 線程分析”吧!
一、數據結構和入口函數 1、數據結構
page_cleaner_t:整個 Innodb 只有一個,包含整個 page clean 線程相關信息。其中包含了一個 page_cleaner_slot_t 的指針。
變量名含義 mutex 用于保護整個 page_cleaner_t 結構體和 page_cleaner_slot_t 結構體,當需要修改結構體信息的時候需要獲取這個 mutex,如在 pc_request 函數中 is_requested 一個條件變量,用于喚醒堵塞在這個條件之上的工作線程 is_finished 一個條件變量,用于通知協調線程刷新工作已經完成 n_workers 當前存在的工作線程總數 requested 布爾值,當前是否需要進行臟數據刷新工作 lsn_limit 需要刷新到 lsn 的位置,當需要同步刷新的時候,這個值將被賦予,以保證小于這個 lsn 的日志都已經完成了刷盤工作 n_slots 槽的數量,槽的數量和 buffer instance 的數量相同 n_slots_requested 當前處于需要刷新狀態下 (PAGE_CLEANER_STATE_REQUESTED) 的槽的數量 n_slots_flushing 當前處于刷新狀態下 (PAGE_CLEANER_STATE_FLUSHING) 的槽的數量 n_slots_finished 當前處于已經刷新完成狀態下 (PAGE_CLEANER_STATE_FINISHED) 的槽的數量 flush_time 整個 (以 innodb buffer 為單位) 刷新消耗的時間(累計 page_cleaner- flush_time += ut_time_ms() – tm;)flush_pass 整個 (以 innodb buffer 為單位) 刷新的次數(累計 page_cleaner- flush_pass++;)slots 指針指向實際的槽 is_running 布爾值,如果關閉 innodb 會被設置為 false,進行強行刷新臟數據
page_cleaner_slot_t:每個 buffer instance 都包含一個這樣的結構體,page clean 工作線程刷新的時候每個線程都會輪詢的檢測每個槽,知道找到沒有被其他 page clean 線程刷新的槽進行刷新工作,直到每個槽(buffer instance)都刷新完成。參考 pc_flush_slot 函數。
變量名含義 state 狀態 PAGE_CLEANER_STATE_REQUESTED、PAGE_CLEANER_STATE_FLUSHING 和 PAGE_CLEANER_STATE_FINISHED 中的一種 n_pages_requested 本槽需要刷新的總的塊數量 n_flushed_list 已經刷新的塊數 succeeded_list 布爾值,刷新是否完成 flush_list_time 本槽刷新消耗的時間(累計參考 pc_flush_slot 函數)flush_list_pass 本槽進行刷新操作的次數(累計參考 pc_flush_slot 函數)2、入口函數
協調工作線程入口:buf_flush_page_cleaner_coordinator
工作線程入口:buf_flush_page_cleaner_worker
二、主循環解析
其由函數 buf_flush_page_cleaner_coordinator 實現。實際正常運行情況下的工作都包含在 while (srv_shutdown_state == SRV_SHUTDOWN_NONE) 這個大循環下。
1、是否需要睡眠 1 秒判斷
首先如果沒有活躍的 change buffer 并且沒有 pending 的物理塊,并且上次刷新的塊數量為 0
則不需要睡眠 1 秒:
if (srv_check_activity(last_activity)
|| buf_get_n_pending_read_ios() || n_flushed == 0){
ret_sleep = pc_sleep_if_needed( next_loop_time, sig_count); // 睡眠一秒 if (srv_shutdown_state != SRV_SHUTDOWN_NONE) { break;
}
} else if (ut_time_ms() next_loop_time) { // 如果當前時間大于 上次刷新 時間 +1 秒則 設置為 OS_SYNC_TIME_EXCEEDED
ret_sleep = OS_SYNC_TIME_EXCEEDED;
} else {
ret_sleep = 0;
}
但是這個睡眠是可以被喚醒的,比如同步刷新應該就會喚醒它(buf_flush_request_force 函數)。參考函數 os_event::wait_time_low
2、IO 能力不足警告
如前文所描述這里產生如下警告:
page_cleaner: 1000ms intended loop took **ms. The settings might not be optimal.((flushed= ** , during the time.)
源碼片段:
if (curr_time next_loop_time + 3000) { // 如果刷新時間 大于了 上次時間 +1 秒 +3 秒 則報 info
if (warn_count == 0) { ib::info() page_cleaner: 1000ms
intended loop took
1000 + curr_time
- next_loop_time
ms. The settings might not
be optimal. (flushed=
n_flushed_last
, during the time.) if (warn_interval 300) {
warn_interval = 600;
} else {
warn_interval *= 2;
}
3、同步刷新判斷
觸發條件
(ret_sleep != OS_SYNC_TIME_EXCEEDED
srv_flush_sync
buf_flush_sync_lsn 0)
同步會喚醒正在睡眠狀態的 page clean 協調工作線程那么睡眠應該不會滿足一秒的條件所以不會被標記為 OS_SYNC_TIME_EXCEEDED,同時 srv_flush_sync 和 buf_flush_sync_lsn 均會被設置接下來就是喚醒工作線程進行刷新,同時本協調線程也完成部分任務。
工作代碼
pc_request(ULINT_MAX, lsn_limit); // 喚醒 page clean 工作線程干活
/* Coordinator also treats requests */ // 協調者同樣要完成部分任務
while (pc_flush_slot() 0) {}
喚醒操作
如前文描述在 checkpoint 或者 DML 語句執行過程中都會通過 log_free_check 檢查是否 redo log 處于安全的狀態,如果不安全就會調用如下代碼(log_preflush_pool_modified_pages 函數中)喚醒 page clean 線程進行同步刷新:
if (srv_flush_sync) { /* wake page cleaner for IO burst */
buf_flush_request_force(new_oldest); // 設置全局變量同時通過 broadcast 喚醒同步刷新
}
buf_flush_wait_flushed(new_oldest); // 所有線程等待同步刷新完成
4、活躍刷新
觸發條件
srv_check_activity(last_activity)
這里判斷是否有活躍的線程,所謂活躍就是調用 srv_inc_activity_count 函數進行增加的,一般來講 DML 和 DDL 會標記為活躍,purge 線程及其工作線程工作期間會標記為活躍。可以將斷點做到 srv_inc_activity_count 進行 debug。所以線上數據庫 DML 比較多所以一般都會是活躍刷新。
工作代碼
這里涉及到刷新多少個塊計算主要函數為 page_cleaner_flush_pages_recommendation,后面在討論。
n_to_flush = page_cleaner_flush_pages_recommendation(lsn_limit, last_pages);// 此處 n_to_flush 就是本次需要刷新的塊數的數量 pc_request(n_to_flush, lsn_limit); // 喚醒 page clean 工作線程干活 /* Coordinator also treats requests */ // 工作協調線程同樣要完成部分任務
while (pc_flush_slot() 0) {}
pc_wait_finished(n_flushed_list);// 等待其他刷新完成
5、空閑刷新
觸發條件
else if (ret_sleep == OS_SYNC_TIME_EXCEEDED)
當睡足了 1 秒,并且沒有活躍的線程。那么就進行空閑刷新,一般來講如果沒有 DML/DDL 等語句那么應該進行是空閑刷新。
工作代碼
buf_flush_lists(PCT_IO(100), LSN_MAX, n_flushed); //io 能力 刷新到那個 lsn 以及傳出刷新的塊數量 //PCT_IO 是一個宏如下:#define PCT_IO(p) ((ulong) (srv_io_capacity * ((double) (p) / 100.0)))
可以看到這里的百分比直接是 100% 及按照 innodb_io_capacity 參數的設定進行刷新。
當然這里只是看了正常期間工作的代碼,如果是 Innodb shutdown 也會觸發同步刷新。可自行參考代碼。
三、page_cleaner_flush_pages_recommendation 函數
前面提過這個函數,是活躍刷新刷新塊的計算函數,下面直接給出整個代碼
{ cur_lsn = log_get_lsn();// 獲取當前的 lsn 在 redo buffer 中的
if (prev_lsn == 0) { // 靜態變量如果是 0 則代表是第一次執行本函數
/* First time around. */
prev_lsn = cur_lsn;
prev_time = ut_time(); // 獲取當前時間
return(0);
} if (prev_lsn == cur_lsn) { // 如果沒有 redo 日志生成
return(0);
}
sum_pages += last_pages_in; time_t curr_time = ut_time(); double time_elapsed = difftime(curr_time, prev_time);
avg_page_rate = static_cast ulint ( ((static_cast double (sum_pages)
/ time_elapsed)
+ avg_page_rate) / 2); // 算出上次刷新每秒刷新的 pages 數量,同時加上次計算的每秒平均刷新塊數 然后除以 2 得到一個每秒刷新的 pages 數量 !!!第一個計算條件 avg_page_rate 生成
/* How much LSN we have generated since last call. */
lsn_rate = static_cast lsn_t ( static_cast double (cur_lsn - prev_lsn)
/ time_elapsed);// 計算 redo lsn 生成率
lsn_avg_rate = (lsn_avg_rate + lsn_rate) / 2;// 計算 redo 每秒平均生成率
/* aggregate stats of all slots */
mutex_enter(page_cleaner- mutex);
ulint flush_tm = page_cleaner- flush_time;
ulint flush_pass = page_cleaner- flush_pass;
page_cleaner- flush_time = 0;
page_cleaner- flush_pass = 0;
ulint list_tm = 0;
ulint list_pass = 0; for (ulint i = 0; i page_cleaner- n_slots; i++) {// 掃描所有的槽
page_cleaner_slot_t* slot;
slot = page_cleaner- slots[i];
list_tm += slot- flush_list_time;
list_pass += slot- flush_list_pass;
slot- flush_list_time = 0;
slot- flush_list_pass = 0;
}
mutex_exit(page_cleaner- mutex);
oldest_lsn = buf_pool_get_oldest_modification(); // 獲取 flush list 中最老的 ls
ut_ad(oldest_lsn = log_get_lsn());// 斷言
age = cur_lsn oldest_lsn ? cur_lsn - oldest_lsn : 0; // 獲取當前 LSN 和最老 LSN 的之間的差值
pct_for_dirty = af_get_pct_for_dirty(); // 計算出一個刷新百分比 (比如 100) !!!! 重點
pct_for_lsn = af_get_pct_for_lsn(age);// 計算出 lsn 的比率 百分比(l 列如 4.5)
pct_total = ut_max(pct_for_dirty, pct_for_lsn);// 取他們的大值
/* Estimate pages to be flushed for the lsn progress */// 計算 target_lsn
ulint sum_pages_for_lsn = 0; lsn_t target_lsn = oldest_lsn
+ lsn_avg_rate * buf_flush_lsn_scan_factor; // 計算下一次刷新的 目標 lsn 及 target_lsnbuf_flush_lsn_scan_factor 是定值 3
for (ulint i = 0; i srv_buf_pool_instances; i++) {// 循環整個 buffer instance 找到小于 target_lsn 的臟塊
buf_pool_t* buf_pool = buf_pool_from_array(i);
ulint pages_for_lsn = 0;
buf_flush_list_mutex_enter(buf_pool); for (buf_page_t* b = UT_LIST_GET_LAST(buf_pool- flush_list);// 每個 innodb buffer 的末尾的 flush list 進行掃描,頭插法?
b != NULL;
b = UT_LIST_GET_PREV(list, b)) { if (b- oldest_modification target_lsn) { break;
}
++pages_for_lsn; // 某個 innodb buffer 實例中 flush list 小于這個 target lsn 的 page 計數
}
buf_flush_list_mutex_exit(buf_pool);
sum_pages_for_lsn += pages_for_lsn; // 這里匯總所有 innodb buffer 實例中 flush list 小于這個 target lsn 的 page 總數
mutex_enter(page_cleaner- mutex);
ut_ad(page_cleaner- slots[i].state
== PAGE_CLEANER_STATE_NONE);// 斷言所有的槽處于沒有刷新狀態
page_cleaner- slots[i].n_pages_requested
= pages_for_lsn / buf_flush_lsn_scan_factor + 1; // 確認槽的 n_pages_requested 值
mutex_exit(page_cleaner- mutex);
}
sum_pages_for_lsn /= buf_flush_lsn_scan_factor;//buf_flush_lsn_scan_factor 為定值 3
/* Cap the maximum IO capacity that we are going to use by
max_io_capacity. Limit the value to avoid too quick increase */
n_pages = PCT_IO(pct_total); // 根據 前面得到的 pct_total 和 srv_io_capacity 參數得到 刷新的塊數 !!! 第二個計算參數生成。 if (age log_get_max_modified_age_async()) { // 如果日質量小于 異步刷新的范疇
ulint pages_for_lsn = std::min ulint (sum_pages_for_lsn,
srv_max_io_capacity * 2); // 即便是需要刷新的塊數很多,最多只能刷 max_io_capacity* 2 的數量!!! 第三個計算參數生成
n_pages = (n_pages + avg_page_rate + pages_for_lsn) / 3; // 3 部分組成 1、根據參數計算出來的 IO 能力 2、以往每秒刷新頁的數量 3、根據 target lsn 計算出來的一個需要刷新的塊數
} if (n_pages srv_max_io_capacity) {
n_pages = srv_max_io_capacity;
} return(n_pages);
}
此函數最后計算出了需要刷新的塊,其中刷新比率計算的的重點函數為 af_get_pct_for_dirty 和 af_get_pct_for_lsn 下面將給出代碼注釋,其實前文中的算法就來自 af_get_pct_for_dirty。
四、af_get_pct_for_dirty 和 af_get_pct_for_lsn 函數
af_get_pct_for_dirty 函數
double dirty_pct = buf_get_modified_ratio_pct(); // 得到 修改的塊 / 總的塊的 的百分比 記住臟數據比率
if (dirty_pct == 0.0) { /* No pages modified */
return(0);
}
ut_a(srv_max_dirty_pages_pct_lwm
= srv_max_buf_pool_modified_pct); if (srv_max_dirty_pages_pct_lwm == 0) { // 如果 innodb_max_dirty_pages_pct_lwm 沒有設置
/* The user has not set the option to preflush dirty
pages as we approach the high water mark. */
if (dirty_pct = srv_max_buf_pool_modified_pct) { // 如果臟數據比率大于了 innodb_max_dirty_pages_pct 則返回比率 100%
/* We have crossed the high water mark of dirty
pages In this case we start flushing at 100% of
innodb_io_capacity. */
return(100);
}
} else if (dirty_pct = srv_max_dirty_pages_pct_lwm) { // 如果設置了 innodb_max_dirty_pages_pct_lwm 并且臟數據比率大于了
/* We should start flushing pages gradually. */ //innodb_max_dirty_pages_pct_lwm 參數設置
return(static_cast ulint ((dirty_pct * 100)
/ (srv_max_buf_pool_modified_pct + 1))); // 則返回 (臟數據比率 /(innodb_max_dirty_pages_pct+1))*100 也是一個比率 如(45/76)*100
} return(0);// 否則返回 0
af_get_pct_for_lsn 函數:
注意 innodb_cleaner_lsn_age_factor 參數默認設置為 high_checkpoint,可以看到算法最后是除以 700.5,所有前文我說這個函數算出來的比率一般比較小。
lsn_t af_lwm = (srv_adaptive_flushing_lwm
* log_get_capacity()) / 100;// srv_adaptive_flushing_lwm=10 那么大約就是 logtotalsize*(9/10)*(1/10) 943349 計算一個 low water mark
if (age af_lwm) { // 如果當前生成的 redo 小于了 low water master 則返回 0 也就是說 redo 日志量生成量不高則不需要權衡
/* No adaptive flushing. */ // 可以看出這里和 redo 設置的大小有關,如果 redo 文件設置越大則 af_lwm 越大,觸發權衡的機率越小
return(0);
}
max_async_age = log_get_max_modified_age_async(); // 獲取需要異步刷新的的位置 大約為 logtotalsize*(9/10)*(7/8)
if (age max_async_age !srv_adaptive_flushing) { // 如果小于異步刷新 且 自適應 flush 沒有開啟
/* We have still not reached the max_async point and
the user has disabled adaptive flushing. */
return(0);
} /* If we are here then we know that either:
1) User has enabled adaptive flushing
2) User may have disabled adaptive flushing but we have reached
max_async_age. */
lsn_age_factor = (age * 100) / max_async_age; // 比率 lsn_age_factor = (本次刷新的日志量 /(logtotalsize*(9/10)*(7/8)))
ut_ad(srv_max_io_capacity = srv_io_capacity);
switch ((srv_cleaner_lsn_age_factor_t)srv_cleaner_lsn_age_factor) { case SRV_CLEANER_LSN_AGE_FACTOR_LEGACY: return(static_cast ulint ( ((srv_max_io_capacity / srv_io_capacity)
* (lsn_age_factor
* sqrt((double)lsn_age_factor)))
/ 7.5)); //430
case SRV_CLEANER_LSN_AGE_FACTOR_HIGH_CHECKPOINT: //innodb_cleaner_lsn_age_factor 參數默認設置為 high_checkpoint
return(static_cast ulint (
((srv_max_io_capacity / srv_io_capacity) // ((max_io_cap /io_cap) * (sqrt(lsn_age_factor)*lsn_age_factor*lsn_age_factor))/700.5
* (lsn_age_factor * lsn_age_factor //(10 * (3.3*10*10))/700 =4.3
* sqrt((double)lsn_age_factor)))
/ 700.5)); //
感謝各位的閱讀,以上就是“MySQL 中 Innodb page clean 線程分析”的內容了,經過本文的學習后,相信大家對 MySQL 中 Innodb page clean 線程分析這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是丸趣 TV,丸趣 TV 小編將為大家推送更多相關知識點的文章,歡迎關注!