某日,路上收到使用者諮詢,為了清除空間,想刪除某200多G大表資料,且已經確認此表不再有業務存取,於是執行了一條命令‘delete from bigtable’,但好長時間也沒刪完,經過諮詢後,獲知drop table刪除錶速度快,而且能徹底釋放空間,於是又在另外一個session中執行了‘drop table bigtable’命令,但是這個命令並沒有快速返回結果,遊標一直hang在原地不動。最後找我們協助,在登入資料庫執行‘show processlist’後發現drop語句的狀態是‘waiting for table metadata lock’,而之前執行的另外一個delete語句依舊能看到,狀態為‘updating’,截圖如下:
到底什麼是metadata lock?這個鎖等待是如何產生的?會帶來什麼影響?最後又如何來解決?今天我們挑6個常見問題給大家解答一下。
一、什麼是metadata lock?
在MySQL5.5.3之前,有一個著名的bug#989,大致如下:
session1:
BEGIN;
INSERT INTO t ... ;
COMMIT;
session2:
DROP TABLE t;
然而上面的操作流程在binlog記錄的順序是
DROP TABLE t;
BEGIN;
INSERT INTO t ... ;
COMMIT;
很顯然備庫執行binlog時會先刪除表t,然後執行insert 會報1032 error,導致複製中斷。為了解決該bug,MySQL 在5.5.3引入了MDL鎖(metadata lock),來保護表的後設資料資訊,用於解決或者保證DDL操作與DML操作之間的一致性。
再舉一個簡單的例子,如果你在查詢一個表的過程中,另外一個session對該表刪除了一個列,那前面的查詢到底該顯示什麼呢?如果在RR隔離級別下,事物中再次執行相同的語句還會和之前結果一致嗎?為了防止這種情況,表查詢開始MySQL會在表上加一個鎖,來防止被別的session修改了表定義,這個鎖就叫‘metadata lock’,簡稱MDL,翻譯成中文也叫‘後設資料鎖’。
二、MDL和行鎖有什麼區別?
metadata lock是表級鎖,是在server層加的,適用於所有儲存引擎。所有的dml操作都會在表上加一個metadata讀鎖;所有的ddl操作都會在表上加一個metadata寫鎖。讀鎖和寫鎖的阻塞關係如下:
熟悉innodb行鎖的同學這裡可能有點困惑,因為行鎖分類和metadata lock很類似,也主要分為讀鎖和寫鎖,或者叫共用鎖和排他鎖,讀寫鎖之間阻塞關係也一致。二者最重要的區別一個是表鎖,一個是行鎖,且行鎖中的讀寫操作對應在metadata lock中都屬於讀鎖。
大家也許會奇怪,以前聽說普通查詢不加鎖的,怎麼這裡又說要加表鎖,我們做一個簡單測試:
session1:查詢前,先看一下metadata_locks表,這個表位於performance_schema下,記錄了metadata lock的加鎖資訊。
mysql> select * from performance_schema.metadata_locks ;
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| OBJECT_TYPE | OBJECT_SCHEMA | OBJECT_NAME | COLUMN_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_DURATION | LOCK_STATUS | SOURCE | OWNER_THREAD_ID | OWNER_EVENT_ID |
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| TABLE | performance_schema | metadata_locks | NULL | 139776223308432 | SHARED_READ | TRANSACTION | GRANTED | sql_parse.cc:6014 | 54 | 12 |
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
1 row in set (0.00 sec)
session2:執行簡單查詢,為了讓表處於執行狀態,這裡使用了sleep函數。
mysql> select sleep(10) from t1;
+-----------+
| sleep(10) |
+-----------+
| 0 |
| 0 |
| 0 |
+-----------+
3 rows in set (30.00 sec)
session1:
mysql> select * from performance_schema.metadata_locks ;
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| OBJECT_TYPE | OBJECT_SCHEMA | OBJECT_NAME | COLUMN_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_DURATION | LOCK_STATUS | SOURCE | OWNER_THREAD_ID | OWNER_EVENT_ID |
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
| TABLE | db1 | t1 | NULL | 139776154308336 | SHARED_READ | TRANSACTION | GRANTED | sql_parse.cc:6014 | 53 | 22 |
| TABLE | performance_schema | metadata_locks | NULL | 139776223308432 | SHARED_READ | TRANSACTION | GRANTED | sql_parse.cc:6014 | 54 | 13 |
+-------------+--------------------+----------------+-------------+-----------------------+-------------+---------------+-------------+-------------------+-----------------+----------------+
2 rows in set (0.00 sec)
此時再次檢視metadata_lock表,發現多了一條t1的加鎖記錄,加鎖型別為SHARED_READ,且狀態是已授予(GRANTED)。大家通常理解的查詢不加鎖,是指不在表上加innodb行鎖。
如果在執行sleep期間,另外一個session執行了一個加欄位操作,此時就會產生metadata lock鎖等待:
session2:
mysql> select sleep(10) from t1;
執行中......
session3:
mysql> alter table t1 add col1 int;
阻塞中......
session1:
mysql> show processlist;
+----+-----------------+-----------+------+---------+--------+---------------------------------+-----------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------+------+---------+--------+---------------------------------+-----------------------------+
| 4 | event_scheduler | localhost | NULL | Daemon | 861577 | Waiting on empty queue | NULL |
| 18 | root | localhost | db1 | Sleep | 50 | | NULL |
| 19 | root | localhost | NULL | Query | 0 | starting | show processlist |
| 20 | root | localhost | db1 | Query | 11 | Waiting for table metadata lock | alter table t1 add col1 int |
+----+-----------------+-----------+------+---------+--------+---------------------------------+-----------------------------+
4 rows in set (0.00 sec)
顯然,id為20的執行緒還未執行alter操作,狀態為‘Waiting for table metadata lock’,也就是在等待session2的sleep操作完成。
三、MDL為什麼會造成系統崩潰?
舉一個簡單例子:
各個session序列操作。
session1:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t1 where id=1;
+----+------+------+-------+
| id | name | age | birth |
+----+------+------+-------+
| 1 | aa | 10 | NULL |
+----+------+------+-------+
1 row in set (0.00 sec)
session2:
mysql> alter table t1 add col1 int;
阻塞中...
session3:
mysql> select sleep(10) from t1 ;
阻塞中...
session4:
mysql> update t1 set name='aaaa' where id=2;
阻塞中...
也就是由於session1的一個事務沒有提交,導致session2的ddl操作被阻塞,session3和session4本身不會被session1阻塞,但由於在鎖佇列中,session2排隊更早,它準備加的是metadata lock寫鎖,阻塞了session3和session4的讀鎖。如果t1是一個執行頻繁的表,show processlist會發現大量‘waiting for table metadata lock’的執行緒,資料庫連線很快就會消耗完,導致業務系統無法正常響應。
此時如果session1提交,是session2的alter語句先執行還是session3和session4先執行呢?之前一直以為先到的先執行,當然是session2先執行,但經過測試,在5.7中,session3和session4先執行,session2最後執行,也就會出現alter長時間無法執行的情況;而在8.0中,session2先執行,session3和session4後執行,由於5.6以後ddl是online的,session2並不會阻塞session3和session4,感覺這樣才是合理的,alter不會被‘餓死’。
四、MDL的生命週期有多長?
事務!事務!事務! 重要的事情說三遍,表上的metadata lock的生命週期從事務中的第一條涉及自身的語句開始,到整個事務結束而結束。而5.5之前是基於語句的,事務中執行完語句就釋放,如果此時另外一個session對錶做了一個刪欄位操作,那麼就會造成兩個問題:
所以,如果要降低metadata lock的鎖等待時間,最好要及時提交事務,同時儘量避免大事務。
那麼如果發生metadata lock鎖等待,等待鎖的session會等待多長時間呢?大家都知道MySQL裡面行鎖等待有個超時時間(引數innodb_lock_wait_timeout),預設50s。metadata lock也有類似引數控制:
mysql> show variables like 'lock_wait_timeout' ;
+-------------------+----------+
| Variable_name | Value |
+-------------------+----------+
| lock_wait_timeout | 31536000 |
+-------------------+----------+
1 row in set (0.00 sec)
這麼長的數位,掰著指頭算了半天,居然真的是......一年,環遊世界一圈回來還得接著等!!!
當然,生產環境中,我們很少會等待metadata lock超時,更多的是要想辦法把產生metadata lock的源頭找到,快速提交或者回滾,或者想辦法kill掉。那麼如何找到阻塞的源頭呢?
五、如何快速找到阻塞源頭?
快速解決問題永遠是第一位的,一旦出現長時間的metadata lock,尤其是在存取頻繁的業務表上產生,通常會導致表無法存取,讀寫全被阻塞,此時找到阻塞源頭是第一位的。這裡最重要的表就是前面提到過的
performance_schema.metadata_locks表。
metadata_locks是5.7中被引入,記錄了metadata lock的相關資訊,包括持有物件、型別、狀態等資訊。但5.7預設設定是關閉的(8.0預設開啟),需要通過下面命令開啟設定:
UPDATE performance_schema.setup_instruments SET ENABLED = 'YES', TIMED = 'YES'WHERE NAME = 'wait/lock/metadata/sql/mdl';
如果要永久生效,需要在組態檔中加入如下內容:
[mysqld]
performance-schema-instrument='wait/lock/metadata/sql/mdl=ON'
單純查詢這個表無法得出具體的阻塞關係,也無法得知什麼語句造成的阻塞,這裡要關聯另外兩個表performance_schema.thread和
performance_schema.events_statements_history,thread表可以將執行緒id和show processlist中id關聯,events_statements_history表可以得到事務的歷史sql,關聯後的完整sql如下:
SELECT
locked_schema,
locked_table,
locked_type,
waiting_processlist_id,
waiting_age,
waiting_query,
waiting_state,
blocking_processlist_id,
blocking_age,
substring_index(sql_text,"transaction_begin;" ,-1) AS blocking_query,
sql_kill_blocking_connection
FROM
(
SELECT
b.OWNER_THREAD_ID AS granted_thread_id,
a.OBJECT_SCHEMA AS locked_schema,
a.OBJECT_NAME AS locked_table,
"Metadata Lock" AS locked_type,
c.PROCESSLIST_ID AS waiting_processlist_id,
c.PROCESSLIST_TIME AS waiting_age,
c.PROCESSLIST_INFO AS waiting_query,
c.PROCESSLIST_STATE AS waiting_state,
d.PROCESSLIST_ID AS blocking_processlist_id,
d.PROCESSLIST_TIME AS blocking_age,
d.PROCESSLIST_INFO AS blocking_query,
concat('KILL ', d.PROCESSLIST_ID) AS sql_kill_blocking_connection
FROM
performance_schema.metadata_locks a
JOIN performance_schema.metadata_locks b ON a.OBJECT_SCHEMA = b.OBJECT_SCHEMA
AND a.OBJECT_NAME = b.OBJECT_NAME
AND a.lock_status = 'PENDING'
AND b.lock_status = 'GRANTED'
AND a.OWNER_THREAD_ID <> b.OWNER_THREAD_ID
AND a.lock_type = 'EXCLUSIVE'
JOIN performance_schema.threads c ON a.OWNER_THREAD_ID = c.THREAD_ID
JOIN performance_schema.threads d ON b.OWNER_THREAD_ID = d.THREAD_ID
) t1,
(
SELECT
thread_id,
group_concat( CASE WHEN EVENT_NAME = 'statement/sql/begin' THEN "transaction_begin" ELSE sql_text END ORDER BY event_id SEPARATOR ";" ) AS sql_text
FROM
performance_schema.events_statements_history
GROUP BY thread_id
) t2
WHERE
t1.granted_thread_id = t2.thread_id \G
對於前面的例子執行此sql,得到一個清晰的阻塞關係:
locked_schema: db1
locked_table: t1
locked_type: Metadata Lock
waiting_processlist_id: 28
waiting_age: 227
waiting_query: alter table t1 add cl3 int
waiting_state: Waiting for table metadata lock
blocking_processlist_id: 27
blocking_age: 252
blocking_query: select * from t1
sql_kill_blocking_connection: KILL 27
1 row in set, 1 warning (0.00 sec)
根據顯示結果,processlist_id為27的執行緒阻塞了28的執行緒,我們需要kill 27即可解鎖。
實際上,MySQL也提供了一個類似的檢視來解決metadata lock問題,檢視名稱為sys.schema_table_lock_waits,但此檢視查詢結果有bug,不是很準確,建議大家還是參考上面sql。
六、本文開始的案例最終如何解決?
通過前面的介紹,本文開始的案例產生的過程就很簡單了:使用者執行了一個全表delete,在目標表上加了metadata讀鎖,由於表很大,讀鎖長時間無法釋放,後來另外一個session執行了drop table操作,又需要在表上加metadata寫鎖,由於讀寫鎖互相阻塞,drop操作只能等待delete操作完成才能獲得寫鎖,因此從表面來看,二個命令都長時間沒有響應,其實內部一個在執行,一個在等待。
那怎麼來解決呢?因為從show processlist以及客戶描述可以很清楚的知道故障機制,當時建議客戶將delete操作kill掉,等資料回滾完後再執行drop操作因為delete已經執行了一段時間,回滾過程可能會較長,客戶最終kill delete後順利drop成功。
小結
生產環境大多是dml操作,metadata讀鎖之間不會產生鎖等待,而目前MySQL的ddl操作大多可以online執行,因此即使有寫鎖,也會很快降級為讀鎖,所以ddl執行期間阻塞dml的機率也很小。最容易出現的情況是由於有未完成的事務,導致ddl metadata 寫鎖無法加上,只能在鎖佇列等待,而一旦進入鎖佇列,寫鎖又會阻塞其他的讀鎖,導致資料庫連線快速增長,直至消耗殆盡,最終業務受到影響。
為了儘可能避免類似問題,下面是幾個小建議:
作者:翟振興