MySQL?優(yōu)化利器?SHOW?PROFILE?的實(shí)現(xiàn)原理及細(xì)節(jié)展示
背景
最近碰到一個 case,通過可傳輸表空間的方式導(dǎo)入一個 4GB 大小的表,耗時 13 分鐘。
通過PROFILE
定位,發(fā)現(xiàn)大部分耗時竟然是在System lock
階段。
mysql> set profiling=1; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> alter table sbtest2 import tablespace; Query OK, 0 rows affected (13 min 8.99 sec) mysql> show profile for query 1; +--------------------------------+------------+ | Status | Duration | +--------------------------------+------------+ | starting | 0.000119 | | Executing hook on transaction | 0.000004 | | starting | 0.000055 | | checking permissions | 0.000010 | | discard_or_import_tablespace | 0.000007 | | Opening tables | 0.000156 | | System lock | 788.966338 | | end | 0.007391 | | waiting for handler commit | 0.000041 | | waiting for handler commit | 0.011179 | | query end | 0.000022 | | closing tables | 0.000019 | | waiting for handler commit | 0.000031 | | freeing items | 0.000035 | | cleaning up | 0.000043 | +--------------------------------+------------+ 15 rows in set, 1 warning (0.03 sec)
不僅如此,SQL 在執(zhí)行的過程中,show processlist
中的狀態(tài)顯示的也是System lock
。
mysql> show processlist; +----+-----------------+-----------+--------+---------+------+------------------------+---------------------------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+-----------------+-----------+--------+---------+------+------------------------+---------------------------------------+ | 5 | event_scheduler | localhost | NULL | Daemon | 818 | Waiting on empty queue | NULL | | 10 | root | localhost | sbtest | Query | 648 | System lock | alter table sbtest2 import tablespace | | 14 | root | localhost | NULL | Query | 0 | init | show processlist | +----+-----------------+-----------+--------+---------+------+------------------------+---------------------------------------+ 3 rows in set, 1 warning (0.00 sec)
這個狀態(tài)其實(shí)有很大的誤導(dǎo)性。
接下來我們從SHOW PROFILE
的基本用法出發(fā),從源碼角度分析它的實(shí)現(xiàn)原理。
最后在分析的基礎(chǔ)上,看看 case 中的表空間導(dǎo)入操作為什么大部分耗時是在System lock
階段。
SHOW PROFILE 的基本用法
下面通過一個示例來看看SHOW PROFILE
的用法。
# 開啟 Profiling mysql> set profiling=1; Query OK, 0 rows affected, 1 warning (0.00 sec) # 執(zhí)行需要分析的 SQL mysql> select count(*) from slowtech.t1; +----------+ | count(*) | +----------+ | 1048576 | +----------+ 1 row in set (1.09 sec) # 通過 show profiles 查看 SQL 對應(yīng)的 Query_ID mysql> show profiles; +----------+------------+----------------------------------+ | Query_ID | Duration | Query | +----------+------------+----------------------------------+ | 1 | 1.09378600 | select count(*) from slowtech.t1 | +----------+------------+----------------------------------+ 1 row in set, 1 warning (0.00 sec) # 查看該 SQL 各個階段的執(zhí)行耗時情況,其中,1 是該 SQL 對應(yīng)的 Query_ID mysql> show profile for query 1; +--------------------------------+----------+ | Status | Duration | +--------------------------------+----------+ | starting | 0.000157 | | Executing hook on transaction | 0.000009 | | starting | 0.000020 | | checking permissions | 0.000012 | | Opening tables | 0.000076 | | init | 0.000011 | | System lock | 0.000026 | | optimizing | 0.000013 | | statistics | 0.000033 | | preparing | 0.000032 | | executing | 1.093124 | | end | 0.000025 | | query end | 0.000013 | | waiting for handler commit | 0.000078 | | closing tables | 0.000048 | | freeing items | 0.000076 | | cleaning up | 0.000037 | +--------------------------------+----------+ 17 rows in set, 1 warning (0.01 sec)
如果指定 all 還會輸出更詳細(xì)的統(tǒng)計信息,包括 CPU、上下文切換、磁盤IO、IPC(進(jìn)程間通信)發(fā)送/接受的消息數(shù)量、頁面故障次數(shù)、交換次數(shù)等。
需要注意的是,這里的統(tǒng)計信息是針對整個進(jìn)程的,不是單個 SQL 的。如果在執(zhí)行上述 SQL 的同時還有其它 SQL 在執(zhí)行,那么這些數(shù)據(jù)就不能用來評估該 SQL 的資源使用情況。
mysql> show profile all for query 1\G ... *************************** 11. row *************************** Status: executing Duration: 0.825417 CPU_user: 1.486951 CPU_system: 0.007982 Context_voluntary: 0 Context_involuntary: 553 Block_ops_in: 0 Block_ops_out: 0 Messages_sent: 0 Messages_received: 0 Page_faults_major: 0 Page_faults_minor: 24 Swaps: 0 Source_function: ExecuteIteratorQuery Source_file: sql_union.cc Source_line: 1678 ... 17 rows in set, 1 warning (0.00 sec)
SHOW PROFILE 的實(shí)現(xiàn)原理
SHOW PROFILE 主要是在sql_profile.cc
中實(shí)現(xiàn)的。它的實(shí)現(xiàn)主要分為兩部分:
- 數(shù)據(jù)的采集。
- 數(shù)據(jù)的計算。
下面我們分別從這兩個維度來看看 SHOW PROFILE 的實(shí)現(xiàn)原理。
數(shù)據(jù)的采集
數(shù)據(jù)的采集實(shí)際上是通過“埋點(diǎn)”實(shí)現(xiàn)的。不同階段對應(yīng)的“埋點(diǎn)”地址可通過show profile source
查看。
mysql> show profile source for query 1; +--------------------------------+----------+-------------------------+----------------------+-------------+ | Status | Duration | Source_function | Source_file | Source_line | +--------------------------------+----------+-------------------------+----------------------+-------------+ | starting | 0.000157 | NULL | NULL | NULL | | Executing hook on transaction | 0.000009 | launch_hook_trans_begin | rpl_handler.cc | 1484 | | starting | 0.000020 | launch_hook_trans_begin | rpl_handler.cc | 1486 | | checking permissions | 0.000012 | check_access | sql_authorization.cc | 2173 | | Opening tables | 0.000076 | open_tables | sql_base.cc | 5911 | | init | 0.000011 | execute | sql_select.cc | 760 | | System lock | 0.000026 | mysql_lock_tables | lock.cc | 332 | | optimizing | 0.000013 | optimize | sql_optimizer.cc | 379 | | statistics | 0.000033 | optimize | sql_optimizer.cc | 721 | | preparing | 0.000032 | optimize | sql_optimizer.cc | 806 | | executing | 1.093124 | ExecuteIteratorQuery | sql_union.cc | 1677 | | end | 0.000025 | execute | sql_select.cc | 796 | | query end | 0.000013 | mysql_execute_command | sql_parse.cc | 4896 | | waiting for handler commit | 0.000078 | ha_commit_trans | handler.cc | 1636 | | closing tables | 0.000048 | mysql_execute_command | sql_parse.cc | 4960 | | freeing items | 0.000076 | dispatch_sql_command | sql_parse.cc | 5434 | | cleaning up | 0.000037 | dispatch_command | sql_parse.cc | 2478 | +--------------------------------+----------+-------------------------+----------------------+-------------+ 17 rows in set, 1 warning (0.00 sec)
以executing
為例,它對應(yīng)的“埋點(diǎn)”地址是sql_union.cc
文件的第 1677 行,該行對應(yīng)的代碼是:
THD_STAGE_INFO(thd, stage_executing);
其它的“埋點(diǎn)”地址也類似,調(diào)用的都是THD_STAGE_INFO
,唯一不一樣的是 stage 的名稱。
THD_STAGE_INFO 主要會做兩件事情:
- 采集數(shù)據(jù)。
- 將采集到的數(shù)據(jù)添加到隊列中。
下面我們結(jié)合代碼看看具體的實(shí)現(xiàn)細(xì)節(jié)。
void QUERY_PROFILE::new_status(const char *status_arg, const char *function_arg, const char *file_arg, unsigned int line_arg) { PROF_MEASUREMENT *prof; ... // 初始化 PROF_MEASUREMENT,初始化的過程中會采集數(shù)據(jù)。 if ((function_arg != nullptr) && (file_arg != nullptr)) prof = new PROF_MEASUREMENT(this, status_arg, function_arg, base_name(file_arg), line_arg); else prof = new PROF_MEASUREMENT(this, status_arg); // m_seq 是階段的序號,對應(yīng) information_schema.profiling 中的 SEQ。 prof->m_seq = m_seq_counter++; // time_usecs 是采集到的系統(tǒng)當(dāng)前時間。 m_end_time_usecs = prof->time_usecs; // 將采集到的數(shù)據(jù)添加到隊列中,這個隊列在查詢時會用到。 entries.push_back(prof); ... }
繼續(xù)分析PROF_MEASUREMENT
的初始化邏輯。
PROF_MEASUREMENT::PROF_MEASUREMENT(QUERY_PROFILE *profile_arg, const char *status_arg, const char *function_arg, const char *file_arg, unsigned int line_arg) : profile(profile_arg) { collect(); set_label(status_arg, function_arg, file_arg, line_arg); } void PROF_MEASUREMENT::collect() { time_usecs = (double)my_getsystime() / 10.0; /* 1 sec was 1e7, now is 1e6 */ #ifdef HAVE_GETRUSAGE getrusage(RUSAGE_SELF, &rusage); #elif defined(_WIN32) ... #endif }
PROF_MEASUREMENT 在初始化時會調(diào)用collect
函數(shù),collect()
函數(shù)非常關(guān)鍵,它會做兩件事情:
通過
my_getsystime()
獲取系統(tǒng)的當(dāng)前時間。通過
getrusage(RUSAGE_SELF, &rusage)
獲取當(dāng)前進(jìn)程(注意是進(jìn)程,不是當(dāng)前 SQL)的資源使用情況。getrusage
是一個用于獲取進(jìn)程或線程資源使用情況的系統(tǒng)調(diào)用。它返回進(jìn)程在執(zhí)行期間所消耗的資源信息,包括 CPU 時間、內(nèi)存使用、頁面故障、上下文切換等信息。
PROF_MEASUREMENT 初始化完畢后,會將其添加到 entries 中。entries 是一個隊列(Queue<PROF_MEASUREMENT> entries
)。這個隊列,會在執(zhí)行show profile for query N
或者information_schema.profiling
時用到。
說完數(shù)據(jù)的采集,接下來我們看看數(shù)據(jù)的計算,畢竟“埋點(diǎn)”收集的只是系統(tǒng)當(dāng)前時間,而我們在show profile for query N
中看到的Duration 是一個時長。
數(shù)據(jù)的計算
當(dāng)我們在執(zhí)行show profile for query N
時,實(shí)際上查詢的是information_schema.profiling
,此時,會調(diào)用PROFILING::fill_statistics_info
來填充數(shù)據(jù)。
下面我們看看該函數(shù)的實(shí)現(xiàn)邏輯。
int PROFILING::fill_statistics_info(THD *thd_arg, Table_ref *tables) { DBUG_TRACE; TABLE *table = tables->table; ulonglong row_number = 0; QUERY_PROFILE *query; // 循環(huán) history 隊列,隊列中的元素是 QUERY_PROFILE,每一個查詢對應(yīng)一個 QUERY_PROFILE。 // 隊列的大小由參數(shù) profiling_history_size 決定,默認(rèn)是 15。 void *history_iterator; for (history_iterator = history.new_iterator(); history_iterator != nullptr; history_iterator = history.iterator_next(history_iterator)) { query = history.iterator_value(history_iterator); ulong seq; void *entry_iterator; PROF_MEASUREMENT *entry, *previous = nullptr; // 循環(huán)每個查詢中的 entries,entries 存儲了每個階段的系統(tǒng)當(dāng)前時間。 for (entry_iterator = query->entries.new_iterator(); entry_iterator != nullptr; entry_iterator = query->entries.iterator_next(entry_iterator), previous = entry, row_number++) { entry = query->entries.iterator_value(entry_iterator); seq = entry->m_seq; if (previous == nullptr) continue; if (thd_arg->lex->sql_command == SQLCOM_SHOW_PROFILE) { if (thd_arg->lex->show_profile_query_id == 0) /* 0 == show final query */ { if (query != last) continue; } else { // 如果記錄中的 Query_ID 跟 show profile for query query_id 中的不一致,則繼續(xù)判斷下一條記錄 if (thd_arg->lex->show_profile_query_id != query->profiling_query_id) continue; } } restore_record(table, s->default_values); // query->profiling_query_id 用來填充 information_schema.profiling 中的 QUERY_ID table->field[0]->store((ulonglong)query->profiling_query_id, true); // seq 用來填充 information_schema.profiling 中的 SEQ table->field[1]->store((ulonglong)seq, true); // status 用來填充 information_schema.profiling 中的 STATE // 注意,這里是上一條記錄的 status,不是當(dāng)前記錄的 status table->field[2]->store(previous->status, strlen(previous->status), system_charset_info); // 當(dāng)前記錄的 time_usecs 減去上一條記錄的 time_usecs 的值,換算成秒,用來填充 information_schema.profiling 中的 DURATION my_decimal duration_decimal; double2my_decimal( E_DEC_FATAL_ERROR, (entry->time_usecs - previous->time_usecs) / (1000.0 * 1000), &duration_decimal); table->field[3]->store_decimal(&duration_decimal); #ifdef HAVE_GETRUSAGE my_decimal cpu_utime_decimal, cpu_stime_decimal; // 當(dāng)前記錄的 ru_utime 減去上一條記錄的 ru_utime,用來填充 information_schema.profiling 中的 CPU_USER double2my_decimal( E_DEC_FATAL_ERROR, RUSAGE_DIFF_USEC(entry->rusage.ru_utime, previous->rusage.ru_utime) / (1000.0 * 1000), &cpu_utime_decimal); ... table->field[4]->store_decimal(&cpu_utime_decimal); ... return 0; }
可以看到,information_schema.profiling
中的第三列(STATE,對應(yīng) show profile for query N
中的 Status)存儲的是上一條記錄的 status(階段名),而第四列(DURATION)的值等于當(dāng)前記錄的采集時間(entry->time_usecs)減去上一條記錄的采集時間(previous->time_usecs)。
所以,我們在show profile for query N
中看到的 Duration 實(shí)際上通過下一個階段的采集時間減去當(dāng)前階段的采集時間得到的,并不是show profile source
中函數(shù)(Source_function)的執(zhí)行時長。
這種實(shí)現(xiàn)方式在判斷操作當(dāng)前狀態(tài)和分析各個階段耗時時存在一定的誤導(dǎo)性。
回到開頭的 case。
表空間導(dǎo)入操作為什么大部分耗時是在 System lock 階段?
表空間導(dǎo)入操作是在mysql_discard_or_import_tablespace
函數(shù)中實(shí)現(xiàn)的。
下面是該函數(shù)簡化后的代碼。
bool Sql_cmd_discard_import_tablespace::mysql_discard_or_import_tablespace( THD *thd, Table_ref *table_list) { ... THD_STAGE_INFO(thd, stage_discard_or_import_tablespace); ... if (open_and_lock_tables(thd, table_list, 0, &alter_prelocking_strategy)) { return true; } ... const bool discard = (m_alter_info->flags & Alter_info::ALTER_DISCARD_TABLESPACE); error = table_list->table->file->ha_discard_or_import_tablespace(discard, table_def); THD_STAGE_INFO(thd, stage_end); ... return true; }
可以看到,該函數(shù)實(shí)際調(diào)用的是 THD_STAGE_INFO(thd, stage_discard_or_import_tablespace)。
只不過,在調(diào)用 THD_STAGE_INFO(thd, stage_discard_or_import_tablespace) 后,調(diào)用了 open_and_lock_tables。
而 open_and_lock_tables 最后會調(diào)用 THD_STAGE_INFO(thd, stage_system_lock)。
這也就是為什么上述函數(shù)中雖然調(diào)用了 THD_STAGE_INFO(thd, stage_discard_or_import_tablespace),但show profile
和show processlist
的輸出中卻顯示System lock
。
但基于對耗時的分析,我們發(fā)現(xiàn)這么顯示其實(shí)并不合理。
在開頭的 case 中,雖然System lock
階段顯示的耗時是 788.966338 秒,但實(shí)際上open_and_lock_tables
這個函數(shù)只消耗了 0.000179 秒,真正的耗時是來自 table_list->table->file->ha_discard_or_import_tablespace
,其執(zhí)行時間長達(dá) 788.965481 秒。
為什么這個函數(shù)需要執(zhí)行這么久呢?主要是表空間在導(dǎo)入的過程中會檢查并更新表空間中的每個頁,包括驗(yàn)證頁是否損壞、更新表空間 ID 和 LSN、處理 Btree 頁(如設(shè)置索引 ID、清除 delete marked 記錄等)、將頁標(biāo)記為臟頁等。表越大,檢查校驗(yàn)的時候會越久。
如此來看,針對表空間導(dǎo)入操作,將其狀態(tài)顯示為discard_or_import_tablespace
更能反映操作的真實(shí)情況。
總結(jié)
在
SHOW PROFILE
中顯示的每個階段的耗時,實(shí)際上是由下一個階段的采集時間減去當(dāng)前階段的采集時間得出的。每個階段的采集時間是通過在代碼的不同路徑中植入
THD_STAGE_INFO(thd, stage_xxx)
實(shí)現(xiàn)的,采集的是系統(tǒng)當(dāng)前時間。這種實(shí)現(xiàn)方式在判斷操作當(dāng)前狀態(tài)(通過 SHOW PROCESSLIST)和分析各個階段耗時(通過 SHOW PROFILE )時存在一定的誤導(dǎo)性,主要是因?yàn)轭A(yù)定義的階段數(shù)量是有限的。
在 MySQL 8.4 中,共定義了 98 個階段,具體的階段名可在
mysqld.cc
中的all_server_stages
數(shù)組找到。在表空間導(dǎo)入操作中,雖然大部分耗時顯示為
System lock
階段,但實(shí)際上,使用discard_or_import_tablespace
來描述這一過程會更為準(zhǔn)確。
參考資料
- https://dev.mysql.com/doc/refman/8.4/en/show-profile.html
- https://dev.mysql.com/doc/refman/8.4/en/performance-schema-query-profiling.html
- https://dev.mysql.com/worklog/task/?id=5522
到此這篇關(guān)于MySQL 優(yōu)化利器 SHOW PROFILE 的實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)MySQL SHOW PROFILE內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談MySQL中float、double、decimal三個浮點(diǎn)類型的區(qū)別與總結(jié)
這篇文章主要介紹了淺談MySQL中float、double、decimal三個浮點(diǎn)類型的區(qū)別與總結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11詳解讓MySQL和Redis數(shù)據(jù)保持一致的四種策略
在分布式系統(tǒng)中,保證Redis和MySQL之間的數(shù)據(jù)一致性是一個復(fù)雜且重要的問題,下面這篇文章主要給大家介紹了關(guān)于讓MySQL和Redis數(shù)據(jù)保持一致的四種策略,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-07-07MySQL多表關(guān)聯(lián)on和where速度對比實(shí)測看誰更快
這篇文章主要介紹了MySQL多表關(guān)聯(lián)on和where速度對比實(shí)測看誰更快問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03MySQL5.7中 performance和sys schema中的監(jiān)控參數(shù)解釋(推薦)
在MySQL5.7中,performance schema有很大改進(jìn),包括引入大量新加入的監(jiān)控項、降低占用空間和負(fù)載,以及通過新的sys schema機(jī)制顯著提升易用性。下面通過本文給大家介紹 MySQL5.7中 performance和sys schema中的監(jiān)控參數(shù)解釋,需要的朋友可以參考下2017-08-08MySQL啟動失敗之MySQL服務(wù)無法啟動的原因及解決
這篇文章主要介紹了MySQL啟動失敗之MySQL服務(wù)無法啟動的原因及解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12