數據庫引起的網站訪問緩慢卡頓排查經歷

問題描述

有個項目網站訪問異常緩慢卡頓,有時候甚至報404,網站代碼確定沒有問題,那隻好檢查數據庫,看數據庫那邊是否合理或有待優化。

數據庫排查

排查之前基本確定是一張存儲引擎爲 MyISAM 的表的問題(在這裏取表名爲 A),所有的訪問都卡在了 A 表的查詢上面,那麼爲什麼會卡在這裏呢???接下來就開始分析排查

查看慢查詢日誌

一般正式環境中,都會開啓慢查詢日誌,方便後期的維護。
因爲表 A 有 100 萬條左右的記錄,所以雖然對應的列有做索引,但第一反應還是以爲是數據太多查詢慢導致的網站訪問緩慢,所以去查看慢查詢日誌,結果發現基本上沒有 A 表的慢查詢記錄。

基於慢查詢日誌查看結果的思考

這就有點離譜了,爲什麼明明卡在了 A 表,但關於 A 表的慢查詢卻沒有呢??難道是慢查詢記錄有問題?還是說 A 表查詢其實是不慢的?

排除疑惑,確定排查方向

有了上面的疑惑,我就在網上查看相關的慢查詢資料,後來看到了下面這一段話:

只有當一個 SQL 的執行時間(不包括鎖等待的時間 lock_time)大於 long_query_time 的時候,纔會判定爲慢查詢 SQL;但是判定爲慢查詢 SQL 之後,輸出的 Query_time 包括了(執行時間+鎖等待時間),並且也會輸出 Lock_time 時間。當一個 SQL 的執行時間(排除 lock_time)小於 long_query_time 的時候(即使他鎖等待超過了很久),也不會記錄到慢查詢日誌當中的。

看了上面這段話,我恍然大悟。既然卡在了 A 表,又沒有關於 A 表的慢查詢,那唯一的可能就是鎖定時間 lock_time 太長了。那就往這個方向分析吧。

關於 MyISAM 表的鎖機制與分析

MyISAM 表的鎖機制

MySQL 數據表存儲引擎不同,鎖機制也不同。如:MyISAM 和 MEMORY 存儲引擎採用的是表級鎖table-level locking;BDB 存儲引擎採用的是頁面鎖 page-level locking,但也支持表級鎖;InnoDB 存儲引擎既支持行級鎖 row-level locking,也支持表級鎖,但默認情況下是採用行級鎖。
所以 MyISAM 的鎖是表級鎖,一旦鎖定就是整張表鎖定。MyISAM 有讀鎖和寫鎖,具體的鎖定機制如下圖:
MyISAM 表的鎖機制
MyISAM 有 表共享讀鎖 table read lock 和表獨佔寫鎖 table write lock 兩種,有以下幾點特性:

  • MyISAM 表的讀操作,不會阻塞其他用戶對同一個表的讀請求,但會阻塞對同一個表的寫請求;
  • MyISAM 表的寫操作,會阻塞其他用戶對同一個表的讀和寫操作;
  • MyISAM 表的讀、寫操作之間、以及寫操作之間是串行的

一些有助於排查的語句

SHOW STATUS LIKE ‘table%’

通過語句 SHOW STATUS LIKE 'table%'; 可以查看到如下圖的查詢結果:
查詢結果

  • Table_locks_immediate:能夠立即獲得表級鎖的鎖請求次數
  • Table_locks_waited:不能立即獲取表級鎖而需要等待的鎖請求次數

如果 Table_locks_waited 值較高,且存在性能問題,則說明存在着較嚴重的表級鎖爭用情況

當初執行以上語句時,確實看到 Table_locks_waited 非常大,好像有幾千~~~

SHOW OPEN TABLES

這個語句的作用是 列舉在表緩存中當前被打開的非TEMPORARY表,會返回一下字段:

  • Database:含有該表的數據庫。
  • Table:表名稱。
  • In_use:表當前被查詢使用的次數。如果該數爲零,則表是打開的,但是當前沒有被使用。
  • Name_locked:表名稱是否被鎖定。名稱鎖定用於取消表或對錶進行重命名等操作。

如果有很多表,執行該語句後一般會有好多條數據的,但是大部分表 In_use 都是 0,所以我們可以在語句上加個條件 show open tables where in_use >=1;,如果 In_use 有大於等於 1 的,結果會如下:

mysql> show open tables where in_use >=1;
+----------+-------+--------+-------------+
| Database | Table | In_use | Name_locked |
+----------+-------+--------+-------------+
| MyDB     | test  |      1 |           0 |
+----------+-------+--------+-------------+
1 row in set (0.00 sec)

我當時執行該語句後,只返回 A 表的相關記錄,而且 In_use 的值竟然達到上百,Name_locked 的值具體是幾忘了,但肯定不是 0!那就是說 A 表的表鎖很嚴重啊。。。

SHOW PROCESSLIST

PROCESSLIST 命令的輸出結果顯示了有哪些線程在運行,不僅可以查看當前所有的連接數,還可以查看當前的連接狀態幫助識別出有問題的查詢語句等。
如果是 root 帳號,能看到所有用戶的當前連接。如果是其他普通帳號,則只能看到自己佔用的連接。SHOW PROCESSLIST只能列出當前 100 條;如果想全部列出,可以使用 SHOW FULL PROCESSLIST 命令。執行語句會有類似下面的結果:

mysql> SHOW PROCESSLIST;
+--------+-------------+-----------------------+-----------+-------------+-------+---------------------------------------------------------------+------------------+
| Id     | User        | Host                  | db        | Command     | Time  | State                                                         | Info             |
+--------+-------------+-----------------------+-----------+-------------+-------+---------------------------------------------------------------+------------------+
|      1 | system user |                       | NULL      | Connect     | 75478 | Waiting for master to send event                              | NULL             |
|      2 | system user |                       | NULL      | Connect     | 15681 | Slave has read all relay log; waiting for more updates        | NULL             |
| 154517 | dbtb        | 129.227.126.102:36766 | NULL      | Binlog Dump | 17682 | Master has sent all binlog to slave; waiting for more updates | NULL             |
| 256957 | root        | 61.104.40.211:58206   | db_name   | Query       |     0 | starting                                                      | SHOW PROCESSLIST |
+--------+-------------+-----------------------+-----------+-------------+-------+---------------------------------------------------------------+------------------+
4 rows in set

各字段的含義如下:

  • Id:用戶登錄 mysql 時,系統分配的 connection_id,可以使用函數 connection_id() 查看;
  • User:顯示當前用戶,如果不是 root,這個命令就只顯示用戶權限範圍的 sql 語句;
  • Host:顯示這個語句是從哪個 IP 的哪個端口上發的,可以用來跟蹤出現問題語句的用戶;
  • db:顯示這個進程目前連接的是哪個數據庫;
  • Command:顯示當前連接的執行的命令,一般取值爲休眠(sleep),查詢(query),連接(connect)等;
  • Time:顯示這個狀態持續的時間,單位是
  • State:顯示使用當前連接的 sql 語句的狀態,很重要的列。state 描述的是語句執行中的某一個狀態。一個 sql 語句,以查詢爲例,可能需要經過 copying to tmp tablesorting resultsending data 等狀態纔可以完成;
  • Info:顯示這個 sql 語句,是判斷問題語句的一個重要依據

State 的狀態有很多,可以根據具體值去網上查找相關的信息

我當時執行這個語句時,竟然有一大堆 StateWaiting for table metadata lock 的記錄,而且都是 A 表的。那說明 A 表的表鎖真的相當嚴重啊。。。

確認問題所在

通過以上的查看與分析,可以知道網站訪問緩慢卡頓的原因就是 A 表的表鎖太嚴重了,至於爲什麼表鎖嚴重,有以下兩點原因:

  1. A 表的存儲引擎爲 MyISAM
  2. A 表的讀寫太頻繁(因爲業務需要,該表有時候會有增刪操作)

解決方案

既然知道是 A 表的表鎖太嚴重,而且 A 表的存儲引擎爲 MyISAM 。那就往這方面解決就是了,主要有兩個大方向:

  1. 在存儲引擎不變的情況下,儘量減少表鎖的發生
  2. 因爲有讀也有寫,有時候寫操作也會很頻繁,所以可以考慮修改存儲引擎爲 InnoDB

存儲引擎不變

concurrent_insert

通常來說,在 MyISAM 裏讀寫操作是串行的,但當對同一個表進行查詢和插入操作時,爲了降低鎖競爭的頻率,根據 concurrent_insert 的設置,MyISAM 是可以並行處理查詢和插入的:

  • concurrent_insert=0 時,不允許併發插入功能。
  • concurrent_insert=1 時,允許對沒有空洞的表使用併發插入,新數據位於數據文件結尾。
  • concurrent_insert=2 時,不管表有沒有空洞,都允許在數據文件結尾併發插入。

所謂空洞,就是行記錄被刪除以後,只是被標記爲“已刪除”,其存儲空間沒有被回收,也就是說沒有被物理刪除。由另外一個進程,異步對這個數據進行刪除。因爲空間長度問題,刪除以後的物理空間不能被新的記錄所使用,從而形成了空洞。MyISAM 的空洞可以通過命令 OPTIMIZE TABLE table_name 來刪除,但是該命令執行時會鎖表,且效率較低,所以要謹慎使用。

根據上面的說明,把 concurrent_insert 設置爲 2 是一個不錯的選擇,至於由此產生的數據空洞,可以定期使用 OPTIMIZE TABLE 語法優化。

max_write_lock_count

默認情況下,寫操作的優先級要高於讀操作的優先級,即便是先發送的讀請求,後發送的寫請求,此時也會優先處理寫請求,然後再處理讀請求。這就造成一個問題:一旦我發出若干個寫請求,就會堵塞所有的讀請求,直到寫請求全都處理完,纔有機會處理讀請求。此時可以考慮設置 max_write_lock_count,如:

SET GLOBAL max_write_lock_count = 1;

有了這樣的設置,當系統處理一個寫操作後,就會暫停寫操作,給讀操作執行的機會。

low-priority-updates

我們還可以更乾脆點,直接降低寫操作的優先級,給讀操作更高的優先級。

SET GLOBAL low-priority-updates=1

小結

綜合來看,concurrent_insert=2 是絕對推薦的,至於 max_write_lock_count=1low-priority- updates=1,則視情況而定,如果可以降低寫操作的優先級,則使用 low-priority-updates=1,否則使用 max_write_lock_count=1

我嘗試把設置 concurrent_insert=2max_write_lock_count=1 後,網站訪問緩慢卡頓的問題並沒有得到改善,不知道是因爲我操作設置不當,還是因爲這樣操作對當前問題沒有效果

修改存儲引擎

既然無法在保持 MyISAM 引擎不變的情況下解決問題,那我只好把存儲引擎修改成 InnoDB 了。因爲是正式環境的數據,修改的時候要謹慎點。以下是應該的步驟:

  • 找個用戶操作比較少的時間點來進行修改
  • 修改前禁止掉所有的相關用戶操作,如果可以的話直接關閉整個網站
  • 備份 A 表的數據
  • 刪除 A 表,並新建 A 表,新建時記得修改存儲引擎
  • 導入備份好的數據到 A

結語

整個排查的過程是非常迷茫與痛苦的,但是通過這個過程還是學到了很多,所以在此記錄一下。

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