閱讀本文大概需要 2.8 分鐘。
MySQL 對我說 “Too young, too naive!"
▌大概過程
在測試環境 Docker 容器中,在跨進程調用服務的時候,A 應用通過 Dubbo 調用 B 應用的 RPC 接口,發現 B 應用接口超時錯誤,接着通過 debug 和日誌,發現具體耗時的地方在於一句簡單 SQL 執行,但是耗時超過 1000ms。
通過查看數據庫的進程列表,發現是有死鎖鎖表了,很多進程狀態 status 處於 'sending data',最後爲鎖住的表添加索引,並且 kill 掉阻塞的請求,解除死鎖,服務速度恢復正常。
下面記錄的是大致排查過程:
通過觀察業務代碼,確認沒有內存溢出或者其它事務問題,於是只能考慮 Docker 環境的數據庫和 jvm 底層詳情了。
▌使用 Druid 監控 SQL 執行狀態
通過日誌,發現有一句 SQL 嚴重超時,一句簡單 SQL,原本是批量插入多條記錄,爲了定位問題,測試時 Mybatis 只插入一條記錄,但即便如此,還是耗時 10 秒。
於是打算使用阿里巴巴的數據庫連接池 Druid 進行監控,監控 SQL 效果如下:
在 SQL 監控 Tab 中,可以看到執行 SQL 的具體情況,包括某條 SQL 語句執行的時間(平均、最慢)、SQL 執行次數、SQL 執行出錯的次數等。
上面顯示的是正常情況下,時間單位是 ms,正常的 SQL 一般在 10ms 之內,數據量大的控制在 30ms 之內,這樣用戶的使用體驗感纔會良好。
所以說之前的 1000ms,是不可接受的結果。
▌通過 JMC 遠程監控 Tomcat
JMC(java mission control) 是 jdk 自帶的一個監控工具,在 jdk 的 bin 目錄下(java 大法好,該目錄下有很多實用的工具)。
此處加了一個 tomcat 無驗證模式:
#在tomcat的conf目錄下的catalina.sh增加如下java啓動參數:-Dcom.sun.management.jmxremote=true-Dcom.sun.management.jmxremote.port=8888-Dcom.sun.management.jmxremote.ssl=false-Dcom.sun.management.jmxremote.authenticate=false-XX:+UnlockCommercialFeatures -XX:+FlightRecorder
下面是自己本地調試的截圖
然後打開 jmc,創建一個 JMX 連接,輸入對應的 ip 和 JMX 端口。接着可以設定一段時間內的飛行監控,監測這一分鐘內 jvm 具體參數
當時調試的時候,發現內存使用、CPU 佔用率、線程狀態也挺正常的,沒有發現明顯的異常錯誤,效果如下圖:
唯一比較耗時的是在代碼 tab 頁中,當時發現了大量的 I/O,比上圖的比例還高,當時大概佔了 80%,查看調用樹,很多循環 tcp socket 連接。
考慮到應用中本來就有很多需要 io 以及 netty 也需要 tcp 連接,所以大概排除了 jvm 虛擬機的問題,然後就去排查 MySQL 的問題。
▌排查 MySQL
在瞭解 MySQL 鎖概念的時候,由於現在使用的比較多的是 InnoDB,所以可以着重看看 InnoDB 鎖問題。
直接執行 SQL 語句
通過 DEBUG 代碼,從 mybatis 中取出映射後的SQL語句,在 MySQL 客戶端直接執行 SQL 和 Explain 查看執行計劃,速度都很快,排除了 SQL 語句的問題。
查看 MySQL 線程列表
show processlist;
從圖中可以看出,有些線程的狀態處於 sending data,查閱資料:所謂的“Sending data”並不是單純的發送數據,而是包括“收集 + 發送 數據”。
然後後面一列 info 顯示的是具體信息,是查詢用來生成主鍵 ID 的函數,之前速度都很快,爲啥突然就這麼慢呢,於是回過頭去查看該函數:
select next_value into ret_val from `xxx` where table_name = tableName for update;update `xxx` set current_value=current_value+step,next_value = next_value+step where table_name=tableName;
select for update,給這個表加了排它鎖,阻止其它事務取得相同數據集的共享讀鎖和排他寫鎖,同時,這個序列表表中,用來檢索的字段沒有加索引,在 InnoDB 行鎖機制中:
由於 MySQL 的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行的記錄,但是如果是使用相同的索引鍵(在我們的場景中,就是查詢時用到的 table_name),是會出現鎖衝突的。
所以瞭解到其它團隊因爲查詢這個表產生事務問題,造成死鎖,這個序列表被鎖住了。
由於這個自增序列表每個團隊都在使用,所以當時測試環境中,經常有 dao 層超時錯誤,最終將這些阻塞的線程 kill 掉,爲序列表加了索引,解決了問題。
▌小結
下次遇到 MySQL 執行耗時的情況,排除了代碼問題之後,要去看數據庫是否有死鎖的情況存在,觀察有沒有被阻塞的線程,排查被阻塞的線程具體 info,定位到具體問題。
·END·
程序員的成長之路
路雖遠,行則必至
本文原發於 同名微信公衆號「程序員的成長之路」,回覆「1024」你懂得,給個讚唄。
回覆 [ 520 ] 領取程序員最佳學習方式
回覆 [ 256 ] 查看 Java 程序員成長規劃