記一次壓測排查死鎖
概述
應用背景
應用是公司內部的基礎設施平臺,會接收到多個內部平臺的數據上報,考慮到后期可能接入平臺增多,故對應用展開壓力測試查看應用的在高并發情況下表現。壓測接口主要是上報數據的接口,觀察接口的穩定性。
環境
機器:2-core、4G DGRAM + 30G Disk
JDK1.8,-xms 2G -xmx2G
-
Tomcat
Tomcat接受請求過程:在accept隊列中接收連接(當客戶端向服務器發送請求時,如果客戶端與OS完成三次握手建立了連接,則OS將該連接放入accept隊列);在連接中獲取請求的數據,生成request;調用servlet容器處理請求;返回response。
accept-count為socket請求連接隊列數量,maxThreads為線程池最大數量,maxConnections為最大同時連接數
- 接受和處理的最大連接數maxConnections:500
- 請求處理最大線程數maxThreads:28
- 隊列長度accpet-count:200(默認值為100)
-
druid
- 初始大小initial-size: 2
- 最大數據庫連接數max-active:20
- 最小空閑數量min-idle:2
- 最大等待毫秒數max-wait:600
測試指標
- JVM內存運行穩定,無OOM,沒有不合理大對象
- CPU、內存、網絡、磁盤、文件句柄占用平穩
- 無頻繁線程鎖、線程數平穩
- 業務線程負載均衡
- 異常率小于0.1%
現象
TPS:Transaction Per Second 事務每秒
QPS: Query Per Second 請求每秒
當用戶一次操作(一個連接)只請求一個接口,TPS和QPS沒有任何區別
當TPS達到30左右,無論如何增添線程數(用戶數),TPS不會再上升
排查
首先進行top,系統負載Load avg平穩,CPU使用率平穩(不高),一般計算密集型應用 CPU 使用率偏高 load 偏低,IO 密集型相反。內存占用在70%左右,相對平穩。
使用jstat -gcutil查看沒有頻繁fullGC youngGC。
至此,我開始懷疑是不是代碼的質量寫的有問題。
- 接著按慣例我還是再用jstack查看堆棧,結果發現好多個線程在Waiting狀態,從下往上查看,主要是在申請數據庫連接時候getConnection()。
在最開始,有多個線程在獲取數據源時候卡住,導致數據庫連接池連接被占滿,而后所有線程全部處于等待狀態,引發死鎖。
最后定位到id生成器的一段代碼上面去
然后向上定位,發現原來事務級別為3,REQUERIES-NEW,最后發現代碼位于自定義的ID生成器上面
先概括一下死鎖原因,Spring事務傳播引發連接池死鎖。
當服務需要分布式id時,會首先從數據庫中獲取一個start_id,然后將start_id更新成start_id+step。那么從start_id~start_id+step段內對的所有id,都屬于當前這個服務了。如果start_id用完了,就會按照相同的流程重新申請一個start_id。
線程本身開啟事務(每個事務占用一個數據庫連接),然后使用id生成器申請Id,Id生成器發現Id不夠用,于是再開啟一個事務向數據庫拿id,發現連接不夠用了,于是等待連接池別的線程釋放連接,而別的線程也在等待id生成器的id,形成互相等待局面——死鎖。
Spring事務級別3,其實是TransactionDefinition.PROPAGATION_REQUIRES_NEW:創建一個新的事務,如果當前存在事務,則把當前事務掛起。也就是說無論如何都創建一個事務
解決
-
改變事務隔離級別,PROPAGATION_REQUIRES_NEW到PROPAGATION_REQUIRED
- PROPAGATION_REQUIRED(默認):如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。
-
增加連接池 getConnection 最大等待時間的配置。
如果沒有獲取到連接一定時間則會拋出異常,結束這個線程。至于如何配置,不同的連接池的配置項不同,具體可參考對應的連接池官方文檔配置。如果防止部分連接執行時間太長或者數據源泄露,還可以加上Connection最大存活時間配置。
-
不使用同一個數據庫連接池
正常來說,id生成的數據庫實例應該單獨配置實例
-
增加事務超時時間配置。(一般情況下不推薦,因為如果sql執行時間超過了超時時間,事務也會等待對應的sql執行完后結束,而在下一次執行sql時候報錯)
通過spring事務注解時候,加上超時時間的屬性配置。
@Transactional(timeout = 60) //代表事務60秒超時
本文純粹是作者對工作中同小組遇到的壓測排查記錄,感謝同事wangxi