作者通過源碼分析 truncate 語句形成慢 SQL 的原因和解決方案,並與 MySQL 5.7 就相關實現邏輯進行對比。
作者:李錫超
一個愛笑的江蘇蘇寧銀行 數據庫工程師,主要負責數據庫日常運維、自動化建設、DMP 平臺運維。擅長 MySQL、Python、Oracle,愛好騎行、研究技術。
本文來源:原創投稿
- 愛可生開源社區出品,原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。
問題現象
收到反饋某測試環境執行批量操作時,有 truncate
語句存在於慢查詢日誌中。擔心上線後可能影響數據庫,請求 DBA 配合分析。
關鍵配置
配置項 | 說明 |
---|---|
數據庫版本 | MySQL 5.7 |
參數 long_query_time <br/> 慢查詢閾值,單位爲秒 |
0.1(100 毫秒) |
參數 innodb_adaptive_hash_index |
ON |
問題分析總結
總結下來主要有如何幾個問題:
Q1: TRUNCATE 語句是如何執行的?fd 句柄不變化?爲什麼執行時間長?
TRUNCATE 語句如何執行?
關鍵堆棧:
關鍵操作 debug:
爲什麼執行時間長?
從以上堆棧可以看到,耗時過程主要是 row_drop_table_for_mysql
、os_file_delete_func
。
其中:row_drop_table_for_mysql
主要是調用 btr_drop_ahi_for_table
執行 AHI 的 page 頁的刪除。os_file_delete_func
主要調用 unlink
執行文件的清理。
句柄爲什麼不變化?
假如需要 truncate
的表分配的 fd 爲 43,truncate
過程中,會先將表 rename
。這個時候這個 fd 會被關閉,43 就被釋放了。然後執行 create table
操作。一般這個間隙過程很短,因此新建立的表可以使用被釋放的 43 了,所以會看到 fd 沒有變化。
如果 rename
之後,在內部執行 create table
之前,又打開了新文件,那這時候 fd 43 就會被其它打開的文件持有,truncate
之後表的 fd 也就會發生變化。
注意:MySQL 8.0 是真正使用
rename
+create
+drop
實現的truncate
,但 MySQL 5.7 是通過文件的truncate
實現的。
Q2: 如何分析 TRUNCATE 慢的問題?
方式一:慢日誌?
只能看到慢的結果,無法確認原因。
方式二:執行計劃?
不支持 truncate
語句。
方式三:profile
從 profile
結果來看,對於 truncate
語句,只能看到耗時過程都在System lock
上,無法看到更近一步的原因。
方式四:DEBUG
// 推薦設置
// 其中 T 其實是 MySQL 支持(在 trace 中打印時間)的,但官方文檔中缺少了說明。已提交bug說明:Bug #111174
set global debug='d:t:T:i:n:N:o,/tmp/debug_3306.trace.f';
set global debug='';
- ① 表示
show processlist
的線程 ID - ② 執行時間
- ③ 函數調用層級
- ④ 函數名稱
MySQL 8.0 切換對比
// TRUNCATE
// 默認規範配置
// innodb_flush_method = on & innodb_flush_method = O_DIRECT
([email protected]) [eolbimsdb] 08:44:46 15> truncate table t5;
Query OK, 0 rows affected (0.98 sec)
// 設置 innodb_adaptive_hash_index = off
([email protected]) [eolbimsdb] 08:52:03 5> truncate table t5;
Query OK, 0 rows affected (0.03 sec)
// 設置 innodb_flush_method = fsync
([email protected]) [eolbimsdb] 09:03:34 28> truncate table t5;
Query OK, 0 rows affected (1.04 sec)
// 設置 innodb_adaptive_hash_index = off & innodb_flush_method = fsync
([email protected]) [eolbimsdb] 09:20:24 5> truncate table t5;
Query OK, 0 rows affected (0.22 sec)
// DROP
// 默認規範配置
// innodb_flush_method = on & innodb_flush_method = O_DIRECT
([email protected]) [eolbimsdb] 10:05:41 9> drop table t5;
Query OK, 0 rows affected (0.94 sec)
// 設置 innodb_adaptive_hash_index = off & innodb_flush_method = O_DIRECT
([email protected]) [eolbimsdb] 09:44:24 5> drop table t5;
Query OK, 0 rows affected (0.01 sec)
// 設置 innodb_flush_method = on & innodb_flush_method = fsync
([email protected]) [eolbimsdb] 09:32:15 13> drop table t5;
Query OK, 0 rows affected (1.13 sec)
// 設置 innodb_adaptive_hash_index = off & innodb_flush_method = fsync
([email protected]) [eolbimsdb] 09:25:10 14> drop table t5;
Query OK, 0 rows affected (0.19 sec)
Q3: 能否優化?慢在哪裏?post_ddl 如何調用?
從 Q1 的結果中可以看出,執行的主要耗時在 row_drop_table_for_mysql
、os_file_delete_func
:
MySQL 8.0 的優化措施
row_drop_table_for_mysql
慢的問題,可以通過設置innodb_adaptive_hash_index = off
進行優化;os_file_delete_func
慢的問題,可以設置innodb_flush_method = O_DIRECT
或者配置表的 HARD LINK 進行優化。
MySQL 5.7 的優化措施
詳見後面 3-Q1、3-Q4 部分。
post_ddl
如何調用?
MySQL 8.0 引入了 scope guard 功能:當定義了 scope guard 之後,會創建 Scope_guard 對象。正常情況下,當執行 return
操作前,會執行 scope guard 定義的邏輯。除非在函數結束前執行 Scope_guard 對象的 commit
操作。文件的刪除功能實在 scope guard 的 cleanup_base 階段調用是現的。
Q4: 生產執行 TRUNCATE 是否存在隱患?
從實現機制來看,主要有以下風險:
IO 壓力
當觸發 truncate
操作後,需要在短時間由數據庫線程將文件 unlink
或 truncate
,如果被處理的文件很大,服務器的 IO 壓力可能會影響正常的數據庫請求。
內存併發
在執行 truncate
、drop
的過程中,由於需要對內存的數據進行清理,特別是對 LRU 和 flush_LRU 進行掃描,並釋放對應的數據塊。這個過程是需要逐個根據 buffer pool instance
獲取 mutex 資源的。如果在業務高峯期,特別是 buffer pool
較大時,可能會影響正常的業務情況。
同時,執行 create drop table
操作時需要 dict_operation_lock
的 X 鎖(RW_X_LATCH),而一些其他後臺線程,比如 Main Thread 檢查 dict cache 時,也需要獲取 dict_operation_lock
的 X 鎖,因此被阻塞。然後用戶線程可能由於獲取不到鎖而處於掛起狀態,當無法立刻獲得鎖時。更多參考:《Drop Table 對 MySQL 的性能影響分析》。
Q5: 不同版本對於 TRUNCATE 的實現是否存在差異?
通過對比 2-Q1 與 3-Q4:
MySQL 8.0 的 truncate
實現方式基本和 drop
實現方式相同,包括主要的耗時位置(都在 row_drop_table_for_mysql
、os_file_delete_func
)都是相同的。
MySQL 5.7 的 truncate
和 drop
實現差異較大,整個實現過程幾乎是完全獨立的代碼。truncate
使用 row_truncate_table_for_mysql
,drop
使用 row_drop_table_for_mysql
;truncate
操作的主要的耗時有 dict_drop_index_tree
、os_file_truncate
。
DROP TABLE 優化失敗分析
下面來看一個 MySQL 5.7 測試環境上線 DROP TABLE 優化方案失敗問題。
Q1:上線爲什麼會失敗?HARD LINK 爲什麼不生效?AHI 爲什麼不生效?
- 當 MySQL 5.7 使用規範配置啓動時,從
debug-trace
過程來看,在row_drop_single_table_tablespace
、row_drop_table_from_cache
函數執行期間根本沒有耗時,所以實施優化方案後,沒有效果; - 耗時的過程在
que_eval_sql: query: PROCEDURE DROP_TABLE_PROC ---> dict_drop_index_tree
; row_drop_single_table_tablespace
的耗時被 MySQL 5.7 配置innodb_flush_method=O_DIRECT
優化了。
Q2:該優化是否適用於 MySQL 8.0?
設置 innodb_flush_method=O_DIRECT
的優化操作,同樣適用於 MySQL 8.0。
Q3:MySQL 8.0 如何解決 DROP TABLE 時執行 DROP_TABLE_PROC
慢的問題?
- WL#9536: InnoDB_New_DD: Support crash-safe DDL;
- 依賴於自 Version 8.0.3 的 NEW DD;
- 整個
drop
慢的que_eval_sql
、DROP_TABLE_PROC
被整體砍掉; - 包括
dict_drop_index_tree
在內的整個函數,都被砍了; - 具體實現機制,參考分析 NEW DD 實現方法。
Q4:MySQL 5.7 DROP TABLE 和 TRUNCATE 在實現機制、優化措施有何區別呢?
- 執行
truncate
操作的耗時,仍然是在dict_drop_index_tree
、os_file_truncate
這兩個階段; os_file_truncate
的耗時:可以通過設置innodb_flush_method=O_DIRECT
時間進行優化(不可以通過hard link
進行優化);dict_drop_index_tree
的耗時,暫時沒有優化思路。瞭解更多:InnoDB 文件系統之文件物理結構。
Q5:5.7 慢查詢爲什麼有時記錄 TRUNCATE 執行慢,有時不記錄?
根據源碼,MySQL 是否記錄慢查詢判斷時,主要有兩個維度:一個是執行時間(不包括 utime_alter_lock
);一個是執行掃描的行數,並對特殊的語句(如 call
)進行了忽略。對於 truncate
操作而言,無論執行時間是多少,掃描行數都是 0。當配置了 min_examined_row_limit
大於 0 之後,一般 truncate
操作由於不滿足該條件,都不會被記錄到慢查詢。
但是當 truncate
操作位於存儲過程中時,在 truncate
操作之前有其它 DML 操作(如 insert selecct
),這時候由於位於同一個 THD 下,在 MySQL 5.7 版本里面 thd->get_examined_row_count()
返回的結果其實是上一個 DML 語句的(這裏應該是缺陷)。如果此時 truncate
操作的執行時間又超過了 long_query_time
,那麼此時這個 truncate
語句就會被記錄慢查詢。
同時,在 MySQL 8.0 針對 call
的語句,將不在單獨記錄記錄的語句。而是記錄爲統一的 call
語句裏面。需要看存儲過程裏面的語句執行情況,可以用 show profiles
查看。
慢查詢記錄堆棧
測試存儲過程
DROP PROCEDURE truncate_test;
DELIMITER //
CREATE PROCEDURE truncate_test()
BEGIN
insert into t1 select * from t1_bak;
truncate table t1;
END
//
DELIMITER ;
call truncate_test();
mysql> call truncate_test();
Query OK, 0 rows affected (1 min 59.58 sec)
# Time: 2023-06-08T00:28:30.969993+08:00
# User@Host: root[root] @ localhost [] Id: 2
# Schema: db2 Last_errno: 0 Killed: 0
# Query_time: 119.177518 Lock_time: 0.000233 Rows_sent: 0 Rows_examined: 131072 Rows_affected: 131072
# Bytes_sent: 0
# Stored_routine: db2.truncate_test
use db2;
SET timestamp=1686155310;
insert into t1 select * from t1_bak;
# Time: 2023-06-08T00:28:31.375873+08:00
# User@Host: root[root] @ localhost [] Id: 2
# Schema: db2 Last_errno: 0 Killed: 0
# Query_time: 0.405734 Lock_time: 0.003310 Rows_sent: 0 Rows_examined: 131072 Rows_affected: 0
# Bytes_sent: 0
# Stored_routine: db2.truncate_test
SET timestamp=1686155311;
truncate table t1;
與 MySQL 8.0 對比
mysql> call truncate_test();
Query OK, 0 rows affected (2 min 28.51 sec)
# Time: 2023-06-07T17:18:39.215632Z
# User@Host: root[root] @ localhost [] Id: 8
# Query_time: 148.516478 Lock_time: 0.000372 Rows_sent: 0 Rows_examined: 172032
use testdb;
SET timestamp=1686158318;
call truncate_test();
MySQL 8.0 如何跟蹤?
mysql> call truncate_test();
Query OK, 0 rows affected (2 min 24.84 sec)
mysql>
mysql>
mysql> show profiles;
+----------+--------------+-----------------------------------------+
| Query_ID | Duration | Query |
+----------+--------------+-----------------------------------------+
| 1 | 144.55113600 | insert into ltb2 select * from ltb3_bak |
| 2 | 0.29312375 | truncate table ltb2 |
+----------+--------------+-----------------------------------------+
2 rows in set, 1 warning (0.00 sec)
以上包括 truncate
執行慢的分析,如針對細節有任何疑問和建議,歡迎留言交流。
關於 SQLE
愛可生開源社區的 SQLE 是一款面向數據庫使用者和管理者,支持多場景審覈,支持標準化上線流程,原生支持 MySQL 審覈且數據庫類型可擴展的 SQL 審覈工具。