MySQL-event機制詳解及官方bug剖析

  • 2019 年 10 月 25 日
  • 筆記

提示:公眾號展示程式碼會自動折行,建議橫屏閱讀



近期,有線上5.6版本event用戶反映了兩個問題: (1) 部分event莫名其妙的延遲執行 (2) 慢日誌不記錄event中的更新及插入語句 經過一系列的分析及驗證,我們確認這兩個問題是mysql原生程式碼的bug,並向官方report。下面就介紹一下相關程式碼及這兩個bug的具體原因及修復方案。

1 mysql event的程式碼類圖

mysql從5.1版本開始引入event機制,這裡介紹的程式碼主要基於5.6/5.7/8.0。5.7/8.0版本除了在priority_queue的實現數據結構上略有變化外,其他的幾乎沒變。下面以5.6版本為例介紹相關的程式碼。

各個類的主要功能如下:

  • Events event的入口模組,主要負責系統中events的載入卸載以及event的創建、刪除、更改等操作。相關文件為events.h/events.cc。
  • Event_parse_data sql解析後的event內部結構體。相關文件為event_parse_data.h/event_parse_data.cc
  • Event_scheduler event的調度模組。相關文件為event_scheduler.h/event_scheduler.cc
  • Event_queue Event任務的記憶體管理結構,內部實現為一個小頂堆,隊頭的event為最近需要執行的任務。Event scheduler會循環獲取隊頭的event並執行。相關文件為event_queue.h/event_queue.cc。
  • Event_db_repository mysql.event表的操作集合。相關文件為event_db_repository.h/event_db_repository.cc。
  • Event_queue_element event queue element元素相關操作。相關文件為event_data_objects.h/event_data_objects.cc。
  • QUEUE 大/小頂堆。除了event模組以外,其他的如partition/file_sort/unique/fts等模組或流程也在使用。在event中用於實現event任務的存放及排序。在5.6中相關文件為queues.h/queues.c,在5.7/8.0中相關的文件為priority_queue.h。

2 mysql event的運行機制

對上圖補充說明如下:

  • event metadata 如上圖所示,event的元數據資訊主要包括兩部分:(1)記憶體中的event queue,管理所有的event任務及對其按照任務執行時間排序 (2)mysql庫中的event表,負責持久化event的相關資訊。
  • event scheduler event scheduler執行緒循環獲取event queue隊列頭的event,並等待任務到時啟動event執行執行緒。event實際是按照存儲過程的方式執行的。event執行完成後會更新event的元數據資訊。

3 主要流程

3.1 event創建流程

handle_one_connection-->do_handle_one_connection---->do_command------>dispatch_command-------->mysql_parse---------->mysql_execute_command------------>Events::create_event-------------->lock_object_name-------------->db_repository->create_event-------------->db_repository->load_named_event-------------->event_queue->create_event-------------->thd->add_to_binlog_accessed_dbs-------------->write_bin_log

3.2 event刪除流程

handle_one_connection-->do_handle_one_connection---->do_command------>dispatch_command-------->mysql_parse---------->mysql_execute_command------------>Events::drop_event-------------->check_access-------------->lock_object_name-------------->Event_db_repository::drop_event-------------->Event_queue::drop_event-------------->thd->add_to_binlog_accessed_dbs-------------->write_bin_log

3.3 event更改流程

handle_one_connection-->do_handle_one_connection---->do_command------>dispatch_command-------->mysql_parse---------->mysql_execute_command------------>Events::update_event-------------->check_access-------------->lock_object_name-------------->Event_db_repository::update_event-------------->Event_queue::update_event-------------->thd->add_to_binlog_accessed_dbs-------------->write_bin_log

3.4 event schduler啟動及調度執行

Event調度器會在兩種情況下啟動: (1)mysqld進程啟動時檢測到event_scheduler打開時 (2)mysqld運行過程中檢測到event_scheduler從關閉切換到打開時

mysqld進程啟動時event scheduler啟動流程:

mysqld_main-->Events::init---->Event_queue::init_queue---->Events::load_events_from_db---->Event_scheduler::start------>mysql_thread_create(event_scheduler_thread)

mysqld運行過程中event_scheduler從關閉切換到打開時,啟動流程:

event_scheduler_update-->Event_scheduler::start---->mysql_thread_create(event_scheduler_thread)

event_scheduler_thread執行緒執行流程:

-->Event_scheduler::run---->Event_queue::get_top_for_execution_if_time---->Event_scheduler::execute_top------>mysql_thread_create(event_worker_thread)

event_worker_thread執行緒執行流程:

-->Event_worker_thread::run---->Event_db_repository::load_named_event---->Event_job_data::execute------>check_access------>Event_job_data::construct_sp_sql------>parse_sql------>sp_head::execute_procedure

4 event小頂堆插入演算法

4.1 插入演算法程式碼

/*功能:向queue隊列中插入元素element*/void queue_insert(register QUEUE *queue, uchar *element){....../* 哨兵節點,自下向上搜索時,搜索到element 0則停止 */  queue->root[0]= element;  /*隊列中新增一個元素*/  idx= ++queue->elements;  /*     從最後一個元素的父節點開始,比較其和element的大小;     如果比element大,則將其移到子節點,並搜索父節點的父節點,以此類推……     直到搜索到的節點的值比element小或者相等 */while((queue->compare(queue->first_cmp_arg,                         element + queue->offset_to_key,                         queue->root[(next= idx >> 1)] +                         queue->offset_to_key) * queue->max_at_top) < 0){    queue->root[idx]= queue->root[next];    idx= next;}  /* 將當前節點的子節點賦值為element,插入完成*/  queue->root[idx]= element;}

4.2 插入演算法示例

在小頂堆1、2、7、3、5、9、10、6中插入0。step1:將0插入到最後一個節點,並和其父節點3進行比較

step2: 將最後一個節點及其父節點進行交換,並繼續比較0和其父節點2的大小

step3: 將0和2進行交換,並繼續比較0和其父節點1的大小

step4: 將0和1進行交換,已經到根節點,插入結束

5 event小頂堆刪除演算法

5.1 刪除演算法程式碼

/* 功能:刪除queue中的第idx個元素 */uchar *queue_remove(register QUEUE *queue, uint idx){  uchar *element;  /* 用最後一個元素的值覆蓋當前位置的值,並刪除最後一個元素 */  element= queue->root[++idx];  /* Intern index starts from 1 */  queue->root[idx]= queue->root[queue->elements--];  /* 當前位置的元素髮生了變化,從當前位置開始,向下調整小頂堆 */  _downheap(queue, idx);  return element;}  /* 功能:從第idx個元素開始,向下調整小頂堆 */void _downheap(register QUEUE *queue, uint idx){  uchar *element;uint elements,half_queue,offset_to_key, next_index;  my_bool first= TRUE;uint start_idx= idx;    offset_to_key=queue->offset_to_key;  element=queue->root[idx];  half_queue=(elements=queue->elements) >> 1;  /* 從idx開始,將其子節點中值較小的節點向其父節點挪,直到最後一個索引節點。     目的是將idx子樹節點重構成一個小頂堆 */while(idx <= half_queue){    next_index=idx+idx;if(next_index < elements &&(queue->compare(queue->first_cmp_arg,            queue->root[next_index]+offset_to_key,            queue->root[next_index+1]+offset_to_key) *     queue->max_at_top) > 0)      next_index++;if(first &&(((queue->compare(queue->first_cmp_arg,                          queue->root[next_index]+offset_to_key,                          element+offset_to_key) * queue->max_at_top) >= 0))){      queue->root[idx]= element;return;}    queue->root[idx]=queue->root[next_index];    idx=next_index;    first= FALSE;}    next_index= idx >> 1;/* 查找element這個元素應該在的節點:從next_idx開始,如果其值比element小則將其移動到子節點,並繼續比較其父節點和element的值,     直到找到一個不小於element值的節點,即為element這個值應該在的節點 */while(next_index > start_idx){if((queue->compare(queue->first_cmp_arg,                       queue->root[next_index]+offset_to_key,                       element+offset_to_key) *         queue->max_at_top) < 0)break;    queue->root[idx]=queue->root[next_index];    idx=next_index;    next_index= idx >> 1;}  /* 將element賦值給搜索到的節點 */  queue->root[idx]=element;}

5.2 刪除演算法示例

比如在第四節所述的小頂堆0、1、7、2、5、9、10、6、3中刪除元素7,步驟如下:step1: 用最後一個元素3替換7,並刪除最後一個元素

step2: 從3開始向下調整小頂

5.3 mysql中小頂堆中刪除程式碼的bug

在上述例子中刪除的是7,如果刪除的是10,按照當前的演算法用3替換10,然後從3開始向下調整,由於3沒有子節點,則最終生成的樹形狀如下

很明顯,這棵樹已經不是小頂堆了,由於小頂堆每次都只取根節點,則3就會比7更晚被訪問到,造成event延遲。

bug原因: mysql的小頂堆刪除演算法中用最後一個元素替換被刪除位置的元素後,只做向下的堆調整,但是向下調整僅適合最後一個元素比被刪除位置元素值大的情況,如果最後一個元素比被刪除位置元素值小,則需要向上做堆的調整。具體案例請參考第7節。

6 慢更新請求不記錄slow log(官方bug1)

bug1 鏈接: https://bugs.mysql.com/bug.php?id=96722

6.1 現象描述

(1) 在event中執行的慢select語句會被記錄到慢日誌 (2) 如果在在event中insert/update之前有慢select語句,那麼後續的insert/update/delete語句無論快慢都會被記錄到慢日誌

6.2 現象1原因分析

6.2.1 慢日誌的記錄條件分析

慢日誌的記錄條件有兩個,參見log_slow_applicable函數。具體條件如下:

(1)請求執行執行緒被設置了SERVER_QUERY_WAS_SLOW標誌並且請求執行過程中檢查的行數不小於min_examined_row_limit系統變數的值;

(2)設置了log_throttle_queries_not_using_indexes及log_queries_not_using_indexes標誌並且當前記錄未使用index的請求速度未達到流控閾值。

從現場看,第二條不滿足,因為log_throttle_queries_not_using_indexes及log_queries_not_using_indexes都沒有設置,只能是因為第一個原因。同時min_examined_row_limit的值是0,因此只能是因為SERVER_QUERY_WAS_SLOW未被設置才導致沒有記錄慢日誌。

6.2.2 慢insert/update/delete未被設置SERVERQUERYWASSLOW標誌的原因

在event執行過程中,SERVER_QUERY_WAS_SLOW的記錄是在spinstrstmt::execute函數中,event命令被執行完成後檢測更新的,程式碼如下:

if(thd->get_stmt_da()->is_eof()){/* 更新SERVER_QUERY_WAS_SLOW標記 */      thd->update_server_status();      thd->protocol->end_statement();}

update_server_status函數負責更新請求的慢查詢狀態(SERVER_QUERY_WAS_SLOW),這個函數很簡單,直接比較請求執行時間和慢請求定義時間的大小,並設置慢查詢狀態。這個函數很簡單,請求的起始時間也是在event開始之前就記錄了,不可能出錯。因此只可能是thd->getstmtda()->iseof()這個條件不滿足。

繼續跟蹤程式碼,發現DA_EOF和DA_OK分別適用於不同的請求類型,DA_OK適用於不返回結果集的請求類型(增刪改),DA_EOF適用於返回結果集的請求(查詢)。這在程式碼中有注釋說明,相關程式碼如下:

/**  Set OK status -- ends commands that do not return a  result set, e.g. INSERT/UPDATE/DELETE.*/voidDiagnostics_area::set_ok_status(ulonglong affected_rows, ulonglong last_insert_id, constchar*message)

6.2.3 現象1原因總結

存儲過程執行完成後根據Diagnostics_area的狀態是否為DA_EOF標記判斷是否需要更新慢請求狀態,但是更新操作未設置DA_EOF標記。

6.3 現象2原因分析

通過上面的分析,我們就明白了為什麼慢增刪改在event中不記錄慢日誌的原因。但是為什麼如果增刪改之前有慢查詢,增刪改就會記錄慢日誌呢?

從上面的分析中,我們可以猜測假如有慢查詢,那麼肯定會設置SERVER_QUERY_WAS_SLOW 標記,但是由於慢增刪改不會設置這個標記,那麼如果執行完慢查詢之後,THD的慢查詢狀態沒有被重置的話,那麼後續的請求無論快慢就都會被記錄在慢日誌。實際跟蹤程式碼,確實如此。

6.4 建議解決方案

(1) 在spinstrstmt::execute函數中如果是my_ok狀態則也檢測更新慢請求狀態。

(2) 在event中每一個指令執行完成之後,重置慢請求狀態。由於mysql官網上並沒有對event中的慢請求的判斷標準進行定義,因此上述兩點只是建議的方案,需要官方確認後才能修復。

7.drop event後部分event被延遲執行(官方bug2)

bug2 鏈接: https://bugs.mysql.com/bug.php?id=96849

7.1 現象描述

部分event被延遲執行,有些延遲幾個小時,有些延遲幾天,有些甚至不執行。客戶回饋說drop掉一個event後重建新的event就很容易復現,出現延遲後刪除重新創建event就可以正常執行。最近一次出現的被延遲的event名稱為event_delayed.

7.2 原因分析

7.2.1 懷疑點

結合相關程式碼,可能的原因有以下幾種:

(1) event調度執行緒或者執行執行緒由於cpu負載較高,出現了延遲,導致event執行延遲;

(2) event被調度了但是執行過程中出錯;

(3) event的執行時間計算錯誤;

(4) event隊列的小頂堆排序出錯;上面四種原因中第一個可以通過在測試環境通過加大cpu負載來複現驗證;後面三個可以通過給線上加列印復現驗證。

7.2.2 原因一驗證

驗證方法:在一個8核的實例上,啟動sysbench測試執行緒至cpu基本滿負載,然後創建80個event(線上實際只有40個)同時啟動,觀察event延遲情況。測試現象:實際觀察,這80個event幾乎同一時間執行,最多延遲一秒。沒有出現延遲幾個小時或者更長時間的情況。驗證結論:cpu負載不可能造成event長時間延遲的原因。

7.2.3 原因二三驗證

驗證方法:在event執行的啟動執行及結束流程添加相關流程列印及過程中event隊列的數據列印,通過日誌資訊確定是哪種原因。測試現象及分析:(1) 相關的日誌中沒有event執行錯誤的列印,原因二被排除。(2) 通過對過程中列印的event隊列的任務執行時間進行人工計算,發覺event隊列中的任務執行時間全部正確,原因三被排除。(3) 通過上面兩個排查,說明很可能是原因四。這個排查就比較繁瑣,因為從出錯的event被創建到延遲被發現這個過程進行了50多次event隊列的更新,需要對這50多次列印的event隊列資訊進行逐一排查。

7.2.4 原因四排查分析

(1) 對這50個任務隊列進行解析並檢查每個隊列是否滿足小頂堆的特性。分析發現部分隊列不滿足小頂堆的特性,而且出錯的節點就是用戶發現的被延遲的event eventdelayed,如圖:

由於每次取任務時只取根節點,而且後續的堆調整也是假設當前堆滿足小頂堆條件的前提下進行, 所以當出現這種錯誤節點後,後續的調度就會出現很多不符合預期的情況,造成不可預期的延遲。

(2) 分析這個錯誤隊列的造成原因通過對event event_delayed創建後的隊列進行逐一排查,確定event隊列的小頂堆被破壞的過程如下:

從上面的分析可以看出,小頂堆被破壞原因是:queue_remove函數在做堆調整時只做了向下調整,而沒有根據實際被刪除位置值的變化分別向下或者向上調整。

(3) 在5.6上復現

復現方法:在mysqs/queues.c的main函數中構造和線上刪除event_dropped前一樣的隊列,刪除event_dropped,觀察刪除後的堆是否滿足小頂堆性質。

復現結論:由於queue_remove中堆調整演算法不正確,導致刪除event_dropped後的堆,小頂堆性質被破壞;

7.3 解決方案

(1) 解決方案:修改queue_remove函數,將down_heap替換為queue_fix。

(2)方案驗證:修改queue_remove後,按照上述問題復現方法同樣的操作後,小頂堆特性繼續保持。如圖所示:

經驗證,該bug在5.7/8.0上都存在,修復思路和在5.6上一樣。

8.總結

mysql的event機制從現在還不是特別完善,如果用戶的業務對任務的執行時間要求很精確或者任務之間存在強依賴關係,最好不要強依賴event機制。同時我們作為雲服務提供商也會及時發現並解決運行過程中存在的問題,努力讓mysql運行的越來越穩定。


騰訊資料庫技術團隊對內支援QQ空間、微信紅包、騰訊廣告、騰訊音樂、騰訊新聞等公司自研業務,對外在騰訊雲上支援TencentDB相關產品,如CynosDB、CDB、CTSDB、CMongo等。騰訊資料庫技術團隊專註於持續優化資料庫內核和架構能力,提升資料庫性能和穩定性,為騰訊自研業務和騰訊雲客戶提供「省心、放心」的資料庫服務。此公眾號和廣大資料庫技術愛好者一起,推廣和分享資料庫領域專業知識,希望對大家有所幫助。