Query Profiling的用法詳解

4.1 Query Profiling用法詳解

Query Profiling是MySQL數據庫提供的一種SQL性能診斷工具, 本節通過對Profiling的簡介,原理,使用方法以及案例的介紹,讓讀者學會如何在SQL優化的過程中,合理的去使用它。

4.1.1 Query Profiling簡介

Query Profiling是MySQL數據庫中提供的一種SQL性能診斷方法,用戶可以在開啓profiling的情況下,查看當前會話執行SQL的時間消耗分佈,CPU用戶時間和系統時間,以及涉及到的關鍵函數,所在的源代碼的文件和行數等等。

4.1.2 開啓和設置profiling

和Query Profiling相關的參數以及他們的含義如下:

  • have_profiling 表示此數據庫是否支持profiling,YES表示支持,NO表示不支持。
  • profiling 此參數表示當前是否開啓了profiling的功能,on表示開啓,off表示關閉,默認爲關閉狀態。
  • profiling_history_size 表示保留當前會話多少條SQL的 profile的記錄,默認爲15條。

通過如下命令開啓profiling功能

set session profiling=on;

切記不要在生產環境使用set global profiling=on,這會打開全局的profiling設置,會對生成環境數據庫的性能造成很大的影響。

然後執行如下SQL:

mysql> select * from order_list order by insert_time limit 1;
+----+----------+---------------------+--------------+
| id | order_id | insert_time         | order_detail |
+----+----------+---------------------+--------------+
|  1 |        1 | 2019-05-03 21:46:00 |              |
+----+----------+---------------------+--------------+
1 row in set (2.16 sec)

查看當前會話歷史的profiles信息

mysql> show profiles;
+----------+------------+-------------------------------------------------------+
| Query_ID | Duration   | Query                                                 |
+----------+------------+-------------------------------------------------------+
|        1 | 2.15071400 | select * from order_list order by insert_time limit 1 |
+----------+------------+-------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)

查看id爲1的SQL的詳細profile信息

mysql> show profiles;
+----------+------------+-------------------------------------------------------+
| Query_ID | Duration   | Query                                                 |
+----------+------------+-------------------------------------------------------+
|        1 | 2.15071400 | select * from order_list order by insert_time limit 1 |
+----------+------------+-------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> show profile for query 1;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000050 |
| checking permissions | 0.000006 |
| Opening tables       | 0.000012 |
| init                 | 0.000014 |
| System lock          | 0.000006 |
| optimizing           | 0.000004 |
| statistics           | 0.000008 |
| preparing            | 0.000024 |
| Sorting result       | 0.000003 |
| executing            | 0.000002 |
| Sending data         | 0.000006 |
| Creating sort index  | 2.150458 |
| end                  | 0.000014 |
| query end            | 0.000007 |
| closing tables       | 0.000007 |
| freeing items        | 0.000024 |
| logging slow query   | 0.000057 |
| cleaning up          | 0.000014 |
+----------------------+----------+
18 rows in set, 1 warning (0.01 sec)

此時,數據庫內部執行此SQL語句的每一個階段的時間消耗都能看到,然後可以針對耗時較長的地方進行分析了。比如這條SQL的時間消耗主要在Creating sort index階段,證明此SQL沒有使用索引排序。

使用profiling還可以查看各個階段的IO情況,如下:

mysql> show profile BLOCK IO for query 1;
+----------------------+----------+--------------+---------------+
| Status               | Duration | Block_ops_in | Block_ops_out |
+----------------------+----------+--------------+---------------+
| starting             | 0.000050 |            0 |             0 |
| checking permissions | 0.000006 |            0 |             0 |
| Opening tables       | 0.000012 |            0 |             0 |
| init                 | 0.000014 |            0 |             0 |
| System lock          | 0.000006 |            0 |             0 |
| optimizing           | 0.000004 |            0 |             0 |
| statistics           | 0.000008 |            0 |             0 |
| preparing            | 0.000024 |            0 |             0 |
| Sorting result       | 0.000003 |            0 |             0 |
| executing            | 0.000002 |            0 |             0 |
| Sending data         | 0.000006 |            0 |             0 |
| Creating sort index  | 2.150458 |       124224 |             0 |
| end                  | 0.000014 |            0 |             0 |
| query end            | 0.000007 |            0 |             0 |
| closing tables       | 0.000007 |            0 |             0 |
| freeing items        | 0.000024 |            0 |             0 |
| logging slow query   | 0.000057 |            0 |             8 |
| cleaning up          | 0.000014 |            0 |             0 |
+----------------------+----------+--------------+---------------+
18 rows in set, 1 warning (0.00 sec)

如果讀者對MySQL源代碼感興趣,想了解某一個階段究竟是如何執行的,可以通過如下命令,查看某個階段對應的源碼文件,行號,函數名稱信息,如下

mysql> show profile SOURCE for query 1;
+----------------------+----------+-----------------------+----------------------+-------------+
| Status               | Duration | Source_function       | Source_file          | Source_line |
+----------------------+----------+-----------------------+----------------------+-------------+
| starting             | 0.000050 | NULL                  | NULL                 |        NULL |
| checking permissions | 0.000006 | check_access          | sql_authorization.cc |         802 |
| Opening tables       | 0.000012 | open_tables           | sql_base.cc          |        5714 |
| init                 | 0.000014 | handle_query          | sql_select.cc        |         121 |
| System lock          | 0.000006 | mysql_lock_tables     | lock.cc              |         323 |
| optimizing           | 0.000004 | optimize              | sql_optimizer.cc     |         151 |
| statistics           | 0.000008 | optimize              | sql_optimizer.cc     |         367 |
| preparing            | 0.000024 | optimize              | sql_optimizer.cc     |         475 |
| Sorting result       | 0.000003 | make_tmp_tables_info  | sql_select.cc        |        3842 |
| executing            | 0.000002 | exec                  | sql_executor.cc      |         119 |
| Sending data         | 0.000006 | exec                  | sql_executor.cc      |         195 |
| Creating sort index  | 2.150458 | sort_table            | sql_executor.cc      |        2614 |
| end                  | 0.000014 | handle_query          | sql_select.cc        |         199 |
| query end            | 0.000007 | mysql_execute_command | sql_parse.cc         |        4946 |
| closing tables       | 0.000007 | mysql_execute_command | sql_parse.cc         |        4998 |
| freeing items        | 0.000024 | mysql_parse           | sql_parse.cc         |        5610 |
| logging slow query   | 0.000057 | log_slow_do           | log.cc               |        1713 |
| cleaning up          | 0.000014 | dispatch_command      | sql_parse.cc         |        1924 |
+----------------------+----------+-----------------------+----------------------+-------------+
18 rows in set, 1 warning (0.00 sec)

要想在編譯數據庫期間就禁用掉profiling的功能,可以在cmake階段添加如下選項

-DENABLED_PROFILING=OFF ##此選項默認爲ON

此後編譯得到的數據庫版本就不再包含profiling的功能。
查看數據庫profiling相關的變量,將會看到如下結果

mysql> show global variables like '%profiling%';
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| have_profiling | NO    |
+----------------+-------+
1 row in set (0.01 sec)

have_profiling爲NO表示此數據庫版本中已經沒有了profiling的功能。這就是通過源碼編譯MySQL數據庫的優點所在,通過在編譯期間的參數控制,減少運行期間的各種判斷,提高數據庫的運行效率。

4.1.3 Query Profiling的原理

本節將會帶領讀者深入MySQL源代碼,用來簡單介紹Query Profiling在MySQL中是如何工作的。
最主要完成Query Profiling工作的類有如下三個,這部分代碼集中在sql_profile.cc中,涉及到如下幾個類。

class PROF_MEASUREMENT;
class QUERY_PROFILE;
class PROFILING;
class THD;

其中跟用戶線程交互的類爲PROFILING,和MySQL數據庫中的主要類THD之間的關係爲
[外鏈圖片轉存失敗(img-8xHl1npn-1562559590686)(http://note.youdao.com/yws/res/12428/0A078E0DE3C84088BD9F2ABD8E9C6E49)]

THD類通過調用PROFILING類的公有成員函數,來實現Query Profiling的功能。

在用戶連接MySQL數據庫,做THD的初始化時,會調用PROFILING::set_thd(THD *thd_arg),這個函數只做了一件事情,就是設置私有成員指針變量thd

 inline void set_thd(THD *thd_arg) { thd= thd_arg; };

完整的線程函數調用堆棧如下

::pfs_spawn_thread(void *)
    ::handle_connection(void *)
        init_new_thd(Channel_info*)
            Channel_info_local_socket::create_thd()
                Channel_info::create_thd()
                    THD::THD(bool)
                        THD::THD(bool)
                            PROFILING::set_thd(THD*)

在用戶下發SQL命令後,會調用PROFILING::start_new_query(char const*)函數去開始一個新的SQL profile信息的記錄:

void PROFILING::start_new_query(const char *initial_state)
{
  DBUG_ENTER("PROFILING::start_new_query");

  /* 除非人爲的修改了代碼,不然不可能出現這種情況 */
  if (unlikely(current != NULL))
  {
    DBUG_PRINT("warning", ("profiling code was asked to start a new query "
                           "before the old query was finished.  This is "
                           "probably a bug."));
    finish_current_query();
  }
  
  //判斷用戶線程變量的profiling是否開啓,如果開啓的話,enabled則爲true,否則爲false
  enabled= ((thd->variables.option_bits & OPTION_PROFILING) != 0);

  //如果爲false,也就是說用戶沒有開啓profiling的功能,則直接返回
  if (! enabled) DBUG_VOID_RETURN; 

  DBUG_ASSERT(current == NULL);
  //否則初始化current,關於QUERY_PROFILE類的構造函數,讀者可以自行查閱。通過上文的介紹中
  //讀者應該清除,profiling是可以記錄當前session多條SQL的profile信息的,而current表示當
  //執行SQL的profile。
  current= new QUERY_PROFILE(this, initial_state);
  
  //返回上層調用
  DBUG_VOID_RETURN;
}

隨後,如果用戶輸入的命令類型爲COM_QUERY的話,則調用PROFILING::set_query_source(char const*, unsigned long)函數,設置此次記錄的profile信息對應的SQL語句,如下:

void PROFILING::set_query_source(const char *query_source_arg, size_t query_length_arg)
{
  DBUG_ENTER("PROFILING::set_query_source");
  //如果用戶沒有開啓profiling,則不做任何操作,直接返回
  if (! enabled)
    DBUG_VOID_RETURN;
  //通過QUERY_PROFILE::set_query_source(char const*, unsigned long)保存當前用戶的SQL語句。這部分需要讀者自行展開閱讀。
  if (current != NULL)
    current->set_query_source(query_source_arg, query_length_arg);
  else
    DBUG_PRINT("info", ("no current profile to send query source to"));
  DBUG_VOID_RETURN;
}

在設置完SQL內容之後,進入到SQL執行的其它不同階段中,通過PROFILING去記錄相應的時間消耗,對應的入口函數,IO信息,源代碼的位置等等。這裏不再列舉所有階段的信息獲取,只以權限檢測這個階段爲例,權限檢測對應show profile for query結果中的checking permissions。獲取這些信息通過調用函數PROFILING::status_change(char const*, char const*, char const*, unsigned int)來實現,THD通過THD::enter_stage函數來調用它。 具體的函數調用關係如下:

dispatch_command(THD*, COM_DATA const*, enum_server_command)
    mysql_parse(THD*, Parser_state*)
        mysql_execute_command(THD*, bool)
            Opt_trace_start::Opt_trace_start(THD*, TABLE_LIST*, enum_sql_command, List<set_var_base>*, char const*, unsigned long, sp_printable*, charset_info_st const*)
                Opt_trace_start::Opt_trace_start(THD*, TABLE_LIST*, enum_sql_command, List<set_var_base>*, char const*, unsigned long, sp_printable*, charset_info_st const*)
                    check_access(THD*, unsigned long, char const*, unsigned long*, st_grant_internal_info*, bool, bool)
                        THD::enter_stage(PSI_stage_info_v1 const*, PSI_stage_info_v1*, char const*, char const*, unsigned int)
                            PROFILING::status_change(char const*, char const*, char const*, unsigned int)

status_change函數實現:

void PROFILING::status_change(const char *status_arg,
                              const char *function_arg,
                              const char *file_arg, unsigned int line_arg)
{
  DBUG_ENTER("PROFILING::status_change");

  //status_arg表示當前的執行階段
  if (status_arg == NULL)
    DBUG_VOID_RETURN;
  
  //表示當前profiling並沒有開啓,或者當前的Query Profile已經被丟棄。
  if (current == NULL)
    DBUG_VOID_RETURN;
  
  //如果用戶開啓了profiling,則記錄上一個階段的消耗,分析見下文
  if (unlikely(enabled))
    current->new_status(status_arg, function_arg, file_arg, line_arg);

  DBUG_VOID_RETURN;
}

函數QUERY_PROFILE::new_status(char const*, char const*, char const*, unsigned int)如下

void QUERY_PROFILE::new_status(const char *status_arg,
                               const char *function_arg, const char *file_arg,
                               unsigned int line_arg)
{
  PROF_MEASUREMENT *prof;
  DBUG_ENTER("QUERY_PROFILE::status");

  DBUG_ASSERT(status_arg != NULL);
  //根據參數的不同,調用不同的PROF_MEASUREMENT構造函數,在構造函數中,會去收集當前時間,以及通過系統函數getrusage,來獲取進程的相關資源信息,這其中包括了cpu的用戶和系統開銷時間,接收的信號量等。
  if ((function_arg != NULL) && (file_arg != NULL))
    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
  prof->m_seq= m_seq_counter++;
  //將獲取到的時間賦值給m_end_time_usecs,表示查詢截止時間。
  m_end_time_usecs= prof->time_usecs;
  //將此階段的相關信息加入到隊列entries中
  entries.push_back(prof);

  /* 如果隊列已經超過了MAX_QUERY_HISTORY的大小,則需要將隊首元素刪除 */
  while (entries.elements > MAX_QUERY_HISTORY)
    delete entries.pop();
  //返回上層調用
  DBUG_VOID_RETURN;
}

直到整個查詢的結束,最後調用到PROFILING::finish_current_query(),結束一次profiling操作。代碼如下:

void PROFILING::finish_current_query()
{
  DBUG_ENTER("PROFILING::finish_current_profile");
  if (current != NULL)
  {
    /* The last fence-post, so we can support the span before this. */
    //記錄SQL執行最後一個階段消耗的時間
    status_change("ending", NULL, NULL, 0);
    
    
    if ((enabled) &&
        ((thd->variables.option_bits & OPTION_PROFILING) != 0) &&
        (current->m_query_source.str != NULL) &&
        (! current->entries.is_empty()))
    {
      //分配一個profiling_query_id,此值在當前線程內單調遞增。
      current->profiling_query_id= next_profile_id();
      //將當前查詢的profile信息添加到history隊列中
      history.push_back(current);
      last= current; 
      current= NULL;
    }
    else
    {
      delete current;
      current= NULL;
    }
  }

  /* 對應到參數profiling_history_size,如果隊列中元素個數超過了設置的大小,則剔除隊首元素*/
  while (history.elements > thd->variables.profiling_history_size)
    delete history.pop();
  //返回上層調用。
  DBUG_VOID_RETURN;
}

Query Profiling的工作原理大致如上所示,可見,在開啓profiling的情況下,增加了很多系統調用。讀者可以嘗試在開啓全局profiling的情況下,通過程序去壓測數據庫,然後通過perf工具去抓取mysqld進程的函數調用,可以發現如下圖所示的情況:
在這裏插入圖片描述

儘管profiling對性能影響較大,但是由於它使用方便,並且支持線程級別的設置,所以在做SQL診斷和優化時,還是可以發揮很大的作用的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章