如何將 performance_schema 中的 TIMER 欄位轉換為日期時間

資料庫工作筆記發表於2023-12-13

來源:MySQL實戰

問題

最近有好幾個朋友問,如何將 performance_schema.events_statements_xxx 中的 TIMER 欄位(主要是TIMER_START和TIMER_END)轉換為日期時間。

因為 TIMER 欄位的單位是皮秒(picosecond),所以很多童鞋會嘗試直接轉換,但轉換後的結果並不對,看下面這個示例。

mysql> select * from performance_schema.events_statements_current limit 1\G
*************************** 1. row ***************************
              THREAD_ID: 57
               EVENT_ID: 13
           END_EVENT_ID: 13
             EVENT_NAME: statement/sql/commit
                 SOURCE: log_event.cc:4825
            TIMER_START: 3304047000000
              TIMER_END: 3305287000000
             TIMER_WAIT: 1240000000
             ...
       EXECUTION_ENGINE: PRIMARY
1 row in set (0.00 sec)

# 因為1秒等於10^12皮秒,所以需要先除以 1000000000000。
mysql> select from_unixtime(3304047000000/1000000000000);
+--------------------------------------------+
| from_unixtime(3304047000000/1000000000000) |
+--------------------------------------------+
| 1970-01-01 08:00:03.3040                   |
+--------------------------------------------+
1 row in set (0.00 sec)

下面會從原始碼角度分析 TIMER 欄位的生成邏輯。

對原始碼分析不感興趣的童鞋,可直接跳到後面的案例部分看結論。

TIMER 欄位的生成邏輯

當我們查詢 events_statements_xxx 表時,會呼叫對應的 make_row() 函式來生成行資料。

如 events_statements_current 表,對應的生成函式是 table_events_statements_current::make_row()

make_row 會呼叫 make_row_part_1 和 make_row_part_2 來生成資料。

TIMER_START、TIMER_END 實際上就是table_events_statements_common::make_row_part_1呼叫to_pico來生成的。

int table_events_statements_common::make_row_part_1(
    PFS_events_statements *statement, sql_digest_storage *digest)
 
{
  ulonglong timer_end;
  ...
  m_normalizer->to_pico(statement->m_timer_start, timer_end,
                        &m_row.m_timer_start, &m_row.m_timer_end,
                        &m_row.m_timer_wait);
  m_row.m_lock_time = statement->m_lock_time * MICROSEC_TO_PICOSEC;

  m_row.m_name = klass->m_name.str();
  m_row.m_name_length = klass->m_name.length();
  ...
  return 0;
}

void time_normalizer::to_pico(ulonglong start, ulonglong end,
                              ulonglong *pico_start, ulonglong *pico_end,
                              ulonglong *pico_wait)
 
{
  if (start == 0) {
    *pico_start = 0;
    *pico_end = 0;
    *pico_wait = 0;
  } else {
    *pico_start = (start - m_v0) * m_factor;
    if (end == 0) {
      *pico_end = 0;
      *pico_wait = 0;
    } else {
      *pico_end = (end - m_v0) * m_factor;
      *pico_wait = (end - start) * m_factor;
    }
  }
}

函式中的 start 和 end 分別對應語句的開始時間(m_timer_start)和結束時間(m_timer_end)。

如果 start,end 不為 0,則 pico_start = (start - m_v0) * m_factor,pico_end = (end - m_v0) * m_factor。

pico_start、pico_end 即我們在 events_statements_current 中看到的 TIMER_START 和 TIMER_END。

m_timer_start 和 m_timer_end 的實現邏輯

如果 performance_schema.setup_instruments 中 statement 相關的採集項開啟了(預設開啟),則語句在開始和結束時會分別呼叫pfs_start_statement_vc()pfs_end_statement_vc()這兩個函式。

m_timer_start 和 m_timer_end 實際上就是在這兩個函式中被賦值的。

void pfs_start_statement_vc(PSI_statement_locker *locker, const char *db,
                            uint db_len, const char *src_file, uint src_line)
 
{
  ...
  if (flags & STATE_FLAG_TIMED) {
    timer_start = get_statement_timer();
    state->m_timer_start = timer_start;
  }
  ...
    pfs->m_timer_start = timer_start;
  ...
}

void pfs_end_statement_vc(PSI_statement_locker *locker, void *stmt_da) {
  ...
  if (flags & STATE_FLAG_TIMED) {
    timer_end = get_statement_timer();
    wait_time = timer_end - state->m_timer_start;
  }
  ...
    pfs->m_timer_end = timer_end;
  ...
}

可以看到,無論是語句開始時間(timer_start)還是結束時間(timer_end),呼叫的都是get_statement_timer()

接下來,我們看看get_statement_timer()的具體實現。

ulonglong inline get_statement_timer() return USED_TIMER(); }

# 如果有其它的計數器實現,只需更新宏定義即可。
#define USED_TIMER my_timer_nanoseconds

ulonglong my_timer_nanoseconds(void) {
 ...
#elif defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_REALTIME)
  {
    struct timespec tp;
    clock_gettime(CLOCK_REALTIME, &tp);
    return (ulonglong)tp.tv_sec * 1000000000 + (ulonglong)tp.tv_nsec;
  }
 ...
#else
  return 0;
#endif
}

get_statement_timer()呼叫的是 USED_TIMER(),而 USED_TIMER 只不過是個宏定義,實際呼叫的還是my_timer_nanoseconds

my_timer_nanoseconds是一個計時器函式,用於獲取系統當前時間,並將其轉換為納秒級別的時間戳。不同的系統,會使用不同的方法來獲取。

對於 linux 系統,它會首先呼叫clock_gettime函式獲取系統當前時間,然後再將其轉換為納秒。

所以,語句的開始時間(m_timer_start)和結束時間(m_timer_end)取的都是系統當前時間。

m_v0 和 m_factor 的實現邏輯

m_v0和m_factor是結構體 time_normalizer 中的兩個變數。其中,

  • m_v0:例項的啟動時間(計數器值)。
  • m_factor:將計數器值轉換為皮秒的轉換因子。

這兩個變數是在例項啟動時被賦值的。

void init_timers(void) {
  double pico_frequency = 1.0e12;
  ...
  my_timer_init(&pfs_timer_info);
  ...
  cycle_v0 = my_timer_cycles();
  nanosec_v0 = my_timer_nanoseconds(); # 獲取系統當前時間,以納秒錶示。
  ...
  if (pfs_timer_info.nanoseconds.frequency > 0) {
    nanosec_to_pico =
        lrint(pico_frequency / (double)pfs_timer_info.nanoseconds.frequency);
  } else {
    nanosec_to_pico = 0;
  }
  ...
  to_pico_data[TIMER_NAME_NANOSEC].m_v0 = nanosec_v0;
  to_pico_data[TIMER_NAME_NANOSEC].m_factor = nanosec_to_pico;
  ...
}

可以看到,nanosec_v0 呼叫的函式,實際上同 m_timer_start、m_timer_end 一樣,都是my_timer_nanoseconds

nanosec_to_pico 是將納秒轉換為皮秒的轉換因子,等於 1.0e12/1.0e9 = 1000。

案例

基於上面的分析,我們總結下 TIMER_START 的計算公式。

TIMER_START = (語句執行時的系統時間(單位納秒)- 例項啟動時的系統時間(單位納秒))* 1000

所以,如果要獲取語句執行時的系統時間,可將 TIMER_START 除以 1000,然後再加上例項啟動時的系統時間。

而例項啟動時的系統時間,可透過當前時間(now)減去Uptime這個狀態變數來實現。

下面我們透過一個具體的案例來驗證下。

mysql> create database test;
Query OK, 1 row affected (0.01 sec)

mysql> create table test.t1(id int primary key, c1 datetime(6));
Query OK, 0 rows affected (0.05 sec)

mysql> insert into test.t1 values(1now(6));
Query OK, 1 row affected (0.02 sec)

mysql> select * from test.t1;
+----+----------------------------+
| id | c1                         |
+----+----------------------------+
|  1 | 2023-12-05 23:57:01.892242 |
+----+----------------------------+
1 row in set (0.01 sec)

mysql> select * from performance_schema.events_statements_history where digest_text like '%insert%'\G
*************************** 1. row ***************************
              THREAD_ID: 69
               EVENT_ID: 8
           END_EVENT_ID: 9
             EVENT_NAME: statement/sql/insert
                 SOURCE: init_net_server_extension.cc:97
            TIMER_START: 24182166000000
              TIMER_END: 24208896000000
             TIMER_WAIT: 26730000000
              LOCK_TIME: 254000000
               SQL_TEXT: insert into test.t1 values(1now(6))
                 DIGEST: b2e0770f7505d35d2894321783fe92b7ebfbb908f687b98966efdc58d3386b3c
            DIGEST_TEXT: INSERT INTO `test` . `t1` VALUES ( ? , NOW (?) )
            ...
       EXECUTION_ENGINE: PRIMARY
1 row in set (0.04 sec)

mysql> select (unix_timestamp(now(6)) - variable_value) * 1000000000 into @mysql_start_time from performance_schema.global_status where variable_name = 'uptime';
Query OK, 1 row affected (0.02 sec)

mysql> select sql_text, timer_start, from_unixtime((timer_start/1000 + @mysql_start_time)/1000000000as formatted_time from performance_schema.events_statements_history where digest_text like '%insert%';
+---------------------------------------+----------------+----------------------------+
| sql_text                              | timer_start    | formatted_time             |
+---------------------------------------+----------------+----------------------------+
insert into test.t1 values(1now(6)) | 24182166000000 | 2023-12-05 23:57:02.356767 |
+---------------------------------------+----------------+----------------------------+
1 row in set (0.01 sec)

插入時間(2023-12-05 23:57:01.892242)和 formatted_time(2023-12-05 23:57:02.356767)基本吻合,相差不到 0.5s。

為什麼會有誤差呢?

  1. Uptime這個狀態變數的單位是秒。
  2. 語句的開始時間(m_timer_start)要比語句中的 now(6) 這個時間早。

細節補充

為了可讀性,上面其實忽略了很多細節,這裡簡單記錄下。

1. to_pico_data

to_pico_data是個陣列,這個陣列包含了多個 time_normalizer 型別的元素。

例項啟動,在呼叫init_timers函式時,實際上還會將以微秒、毫秒為單位的系統時間分別賦值給to_pico_data[TIMER_NAME_MICROSEC].m_v0to_pico_data[TIMER_NAME_MILLISEC].m_v0

to_pico_data[TIMER_NAME_CYCLE].m_v0 = cycle_v0;
to_pico_data[TIMER_NAME_CYCLE].m_factor = cycle_to_pico;

to_pico_data[TIMER_NAME_NANOSEC].m_v0 = nanosec_v0;
to_pico_data[TIMER_NAME_NANOSEC].m_factor = nanosec_to_pico;

to_pico_data[TIMER_NAME_MICROSEC].m_v0 = microsec_v0;
to_pico_data[TIMER_NAME_MICROSEC].m_factor = microsec_to_pico;

to_pico_data[TIMER_NAME_MILLISEC].m_v0 = millisec_v0;
to_pico_data[TIMER_NAME_MILLISEC].m_factor = millisec_to_pico;

既然有這麼多個 m_v0,怎麼知道time_normalizer::to_pico函式取的是哪一個呢?

實際上,events_statements_xxx 系列表的實現中,有個基類table_events_statements_common

該類的建構函式裡面會基於time_normalizer::get_statement()來初始化 m_normalizer,

time_normalizer::get_statement()實際上返回的就是to_pico_data[TIMER_NAME_NANOSEC]

table_events_statements_common::table_events_statements_common(
    const PFS_engine_table_share *share, void *pos)
    : PFS_engine_table(share, pos) {
  m_normalizer = time_normalizer::get_statement();
}

time_normalizer *time_normalizer::get_statement() {
  return &to_pico_data[USED_TIMER_NAME];
}

#define USED_TIMER_NAME TIMER_NAME_NANOSEC

2. performance_schema 表的實現註釋

storage/perfschema/pfs.cc檔案中有一段註釋。

這段註釋非常重要,它介紹了 performance_schema 中的表是如何實現的。

以下是 events_statements_xxx 相關的註釋。

 ... 
 Implemented as:
  - [1] #pfs_start_statement_vc(), #pfs_end_statement_vc()
       (1a, 1b) is an aggregation by EVENT_NAME,
        (1c, 1d, 1e) is an aggregation by TIME,
        (1f) is an aggregation by DIGEST
        all of these are orthogonal,
        and implemented in #pfs_end_statement_vc().
  - [2] #pfs_delete_thread_v1(), #aggregate_thread_statements()
  - [3] @PFS_account::aggregate_statements()
  - [4] @c PFS_host::aggregate_statements()
  - [A] EVENTS_STATEMENTS_SUMMARY_BY_THREAD_BY_EVENT_NAME,
        @c table_esms_by_thread_by_event_name::make_row()
  ...
  - [H] EVENTS_STATEMENTS_HISTORY_LONG,
        @c table_events_statements_history_long::make_row()
  - [I] EVENTS_STATEMENTS_SUMMARY_BY_DIGEST
        @c table_esms_by_digest::make_row()

3. 如何知道 TIMER 欄位對應 m_row 中的哪些變數?

兩者的對應關係實際上是在table_events_statements_common::read_row_values中定義的。

int table_events_statements_common::read_row_values(TABLE *table,
                                                    unsigned char *buf,
                                                    Field **fields,
                                                    bool read_all)
 
{
  Field *f;
  uint len;

  /* Set the null bits */
  assert(table->s->null_bytes == 3);
  buf[0] = 0;
  buf[1] = 0;
  buf[2] = 0;

  for (; (f = *fields); fields++) {
    if (read_all || bitmap_is_set(table->read_set, f->field_index())) {
      switch (f->field_index()) {
        case 0/* THREAD_ID */
          set_field_ulonglong(f, m_row.m_thread_internal_id);
          break;
        ...
        case 5/* TIMER_START */
          if (m_row.m_timer_start != 0) {
            set_field_ulonglong(f, m_row.m_timer_start);
          } else {
            f->set_null();
          }
          break;
        case 6/* TIMER_END */
          if (m_row.m_timer_end != 0) {
            set_field_ulonglong(f, m_row.m_timer_end);
          } else {
            f->set_null();
          }
          break;
          ...

來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70027826/viewspace-2999850/,如需轉載,請註明出處,否則將追究法律責任。

相關文章