Springboot并發(fā)調(diào)優(yōu)之大事務和長連接
1、背景
在當前這個快速開發(fā)的環(huán)境下,很多時候我們的應用都是測試好好的,正式環(huán)境并發(fā)一高就一團糟。不了解并發(fā)相關參數(shù),看不懂壓測報告,是很多程序猿的基本狀態(tài)。本文重點分享長事務以及長連接導致的并發(fā)排查和優(yōu)化思路和示例。
長事務會導致長連接,長連接未必是因為長事務,因果關系先搞清楚。
主要相關技術(shù):
- SpringBoot: 2.5.12
- mybatis-spring-boot-starter: 2.1.2
- druid-spring-boot-starter: 1.2.9
- mysql-connector-java: 8.0+
- tomcat: 9.0.54
2、主要參數(shù)釋義:
2.1 tomcat主要并發(fā)參數(shù)釋義
server: port: 8080 compression: enabled: true tomcat: accept-count: 511 max-connections: 8192 threads: max: 200 basedir: /u01/app/base/logs/tomcat connection-timeout: 60000 keep-alive-timeout: 60000
**threads.max:**表示服務器最大有多少個線程處理請求,默認200,實際上這個參數(shù)超過服務器核心數(shù)太多反而會降低服務器cpu處理速度,對于計算密集型和IO密集型應分開考慮設置該參數(shù)。
**max-connections:**表示服務器與客戶端可以建立多少個連接數(shù),即持有的連接數(shù)。tomcat缺省是8192個連接數(shù),cpu未必有時間給你處理,但是可以保持連接。這個參數(shù)是客戶感知型參數(shù)。
accept-count: 與服務器內(nèi)核相關,是客戶端傳入給服務器內(nèi)核,請求的backlog值,該值與服務器內(nèi)核參數(shù)net.core.somaxconn
取小后的值為最終起效的TCP內(nèi)核全隊列值。它表示在max-connections值達到預設的值后,服務器內(nèi)核還能建立的連接數(shù),這個連接保存在內(nèi)核,還未被上層應用(tomcat)取走。該值在tomcat中默認是100,在Centos7.x版本中內(nèi)核net.core.somaxconn
是128。如果超過max-connections和accept-count總和,新的連接會被拒絕,即直接拒絕服務(直接返回connection refused)。
查看CentOS的net.core.somaxconn參數(shù):sysctl -a|grep net.core.somaxconn
2.2 數(shù)據(jù)庫連接池參數(shù)
datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver #連接池屬性 initial-size: 5 max-active: 140 min-idle: 10 # 配置獲取連接等待超時的時間 max-wait: 30000 connect-properties.slowSqlMillis: 2000
max-active: 數(shù)據(jù)庫連接池數(shù)據(jù)連接最大數(shù)量,即連接池物理打開數(shù)據(jù)庫的最大數(shù)量。這個參數(shù)一般開發(fā)人員都會錯誤的設置,首先這個值不是越大越好,最起碼它得小于數(shù)據(jù)庫本身配置的最大連接數(shù),如果超過后再向數(shù)據(jù)庫發(fā)起連接,就會在數(shù)據(jù)庫層面拋出類似"too many connection"的錯誤。mysql數(shù)據(jù)庫默認最大連接數(shù)為151。一般配置數(shù)據(jù)庫連接池應用組件的時候,不要超過這個數(shù),并且需要留一部分連接數(shù)給維護人員使用。
2.3 數(shù)據(jù)庫連接數(shù)
上文2.2已經(jīng)提到數(shù)據(jù)庫連接數(shù),它決定了數(shù)據(jù)庫支持的最大并發(fā)數(shù)。
查看mysql的最大連接數(shù):
show variables like '%max_connections%';
查看mysql目前的連接數(shù):
show global status like 'Max_used_connections';
如果你的應用配置連接數(shù)超過數(shù)據(jù)庫預設的最大數(shù),并且客戶端不斷并發(fā)的發(fā)起數(shù)據(jù)庫連接,連接池數(shù)量就會不斷創(chuàng)建與數(shù)據(jù)庫的物理連接,如果該連接是各種原因?qū)е碌臄?shù)據(jù)庫長連接(例如:長事務),那么一旦超過數(shù)據(jù)庫的最大值,應用就會報連接數(shù)太多的錯誤。
3、測試程序
兩個對外暴露的url:
- 5000毫秒的長連接操作
- 100毫秒的短連接操作
Controller:
@GetMapping("/slowGetAll") public Result<Object> slowGetAll(){ return Result.ok(testUserService.queryAll()); } @GetMapping("/fastGetAll") public Result<Object> fastGetAll(){ return Result.ok(testUserService.fastGetAll()); }
service: 向數(shù)據(jù)庫里插入兩個用戶,并查詢最后一個用戶的信息,在兩次insert操作中間加入一個耗時操作。
@Transactional(rollbackFor = Exception.class) @Override public List<TestUser> fastGetAll() { TestUser user1 = getTestUser("李四", "jerry"); this.testUserDao.insert(user1); slowMethod(customProperties.getFastMillis()); TestUser user2 = getTestUser("張三", "tom"); this.testUserDao.insert(user2); return this.testUserDao.queryByBlurry(user2); } @Transactional(rollbackFor = Exception.class) @Override public List<TestUser> queryAll() { TestUser user1 = getTestUser("王五", "jack"); this.testUserDao.insert(user1); slowMethod(customProperties.getSlowMillis()); TestUser user2 = getTestUser("趙柳", "amy"); this.testUserDao.insert(user2); return this.testUserDao.queryByBlurry(user2); } private void slowMethod(int milliseconds){ try { int i = globalCount.incrementAndGet(); System.out.println("slowMethod start -->"+i); Thread.sleep(milliseconds); System.out.println("slowMethod end -->"+i); } catch (InterruptedException e) { e.printStackTrace(); } }
注意,這里的長連接使用使用Thread.sleep實現(xiàn),不是真正的數(shù)據(jù)庫長事務。
4、jmeter測試
開啟400線程,測試10輪,分兩組:
- 快速處理的短連接4000次。
- 慢速處理的長連接4000次。
4.1、快速組
druid連接池主要結(jié)果分析:
指標 | 值 | 解釋 |
---|---|---|
事務時間分布 | 0,0,3735,265,0,0,0 | 事務運行時間分布,分布區(qū)間為[0-1 ms, 1-10 ms, 10-100 ms, 100-1 s, 1-10 s, 10-100 s, >100 s] |
連接持有時間分布 | 0,0,0,3583,417,0,0,0 | 連接持有時間分布,分布區(qū)間為[0-1 ms, 1-10 ms, 10-100 ms, 100ms-1s, 1-10 s, 10-100 s, 100-1000 s, >1000 s] |
在JMeter中的吞吐量為:305.4/second
4.2、慢速組
druid連接池主要結(jié)果分析:
指標 | 值 | 解釋 |
---|---|---|
事務時間分布 | 0,0,3599,401,0,0,0 | 事務運行時間分布,分布區(qū)間為[0-1 ms, 1-10 ms, 10-100 ms, 100-1 s, 1-10 s, 10-100 s, >100 s] |
連接持有時間分布 | 0,0,0,0,4000,0,0,0 | 連接持有時間分布,分布區(qū)間為[0-1 ms, 1-10 ms, 10-100 ms, 100ms-1s, 1-10 s, 10-100 s, 100-1000 s, >1000 s] |
在JMeter中的吞吐量為:26.3/second
4.3、對照分析
事務時間分布:非數(shù)據(jù)庫操作的耗時對事務時間分布是沒有影響的,快速組和慢速組分布區(qū)間基本相同。
連接持有時間分布:快速組大約90%的時間分布在100ms-1s
,慢速組100%分布在1-10 s
,符合預期的設置。
吞吐量:快速組比慢速組快了接近10倍。
小小結(jié)論:在慢速組中,雖然事務的分別時間較短,但是發(fā)起該事務的連接一直沒有被釋放,導致并發(fā)能力斷崖式下降。
5、問題與優(yōu)化
5.1、問題
在本次測試中,影響應用并發(fā)性能主要體現(xiàn)在長連接持有時間,當服務器處理某個請求耗時較長會導致并發(fā)能力直線下降,這個耗時可能會因為數(shù)據(jù)庫長事務、長計算、或發(fā)起對外慢速同步的API請求等等原因?qū)е隆?/p>
5.2 、排查
通過druid monitor監(jiān)控可以查看很多與數(shù)據(jù)庫連接的參數(shù)和實際發(fā)生狀態(tài),本例中,主要需要找到事務時間分布和連接持有時間即可初步定位問題,然后通過SQL監(jiān)控找到發(fā)生的SQL語句逐步排查定位到JAVA代碼塊,找到代碼塊一般都能分析出實際的問題。
5.3、核心
在整個應用生態(tài)中,最寶貴的資源就是數(shù)據(jù)庫連接,數(shù)據(jù)庫相關業(yè)務密集的系統(tǒng)中,首先需要保證盡可能少的持有數(shù)據(jù)庫連接。所以才催生出數(shù)據(jù)庫連接池這些技術(shù)。第二寶貴的是磁盤IO,所有對磁盤IO的操作盡可能少,所以催生出數(shù)據(jù)庫索引、緩存等技術(shù),對于需要直接操作磁盤IO的計算來說,能用順序讀或?qū)?,就不要用隨機讀或?qū)憽?/p>
5.4、調(diào)優(yōu)
優(yōu)化并發(fā)需從幾個方面出發(fā):
- 服務器本身的參數(shù),比如CPU核數(shù),高性能磁盤位置等,通過服務器參數(shù)對應設置前文所述的幾個應用參數(shù),比如服務器CPU核心數(shù)為16個,一般通用業(yè)務應用下,缺省的200個線程已經(jīng)足夠。
- 一旦確認服務器配置等基本的參數(shù),并拍腦袋設置了一些應用參數(shù)后,就需要啟動壓測測試各參數(shù)配比下最好的服務器和應用性能。
- 確認你的業(yè)務系統(tǒng)是計算密集型還是IO密集型,針對性的編寫測試用例。
- 最后確認業(yè)務系統(tǒng)乃至服務器的性能瓶頸鏈,輸出整體性能報告。在實際生產(chǎn)中,一旦檢測到實際并發(fā)與性能報告相差太大就可以啟動排查程序。
6、優(yōu)化實驗
6.1 手動事務
代碼優(yōu)化背景和目標:
- 首先長連接的事實不可改變,因為有可能這個計算或調(diào)用就是需要耗費5秒的時間,如果能減少,則屬于另外的優(yōu)化邏輯;
- 在這個背景下,我們改善的目標是減少數(shù)據(jù)庫連接的持有時間;
- 從實例代碼里,真正需要事務的操作是兩個insert,slowMethod和query查詢都不需要,所以啟動手動事務就可以減少數(shù)據(jù)庫連接的持有時間。
- 使用Springboot的手動事務模板。
6.2、優(yōu)化第一組測試
代碼優(yōu)化如下:
@Resource private TransactionTemplate transactionTemplate; @Override public List<TestUser> optimizedGetAll() { slowMethod(customProperties.getSlowMillis()); TestUser user1 = getTestUser("王五", "jack"); TestUser user2 = getTestUser("趙柳", "amy"); transactionTemplate.execute(status -> { this.testUserDao.insert(user1); this.testUserDao.insert(user2); return Boolean.TRUE; }); return this.testUserDao.queryByBlurry(user2); }
druid連接池主要結(jié)果分析:
指標 | 值 | 解釋 |
---|---|---|
事務時間分布 | 0,0,2295,1626,79,0,0 | 事務運行時間分布,分布區(qū)間為[0-1 ms, 1-10 ms, 10-100 ms, 100-1 s, 1-10 s, 10-100 s, >100 s] |
連接持有時間分布 | 2307,1041,2735,1838,79,0,0,0 | 連接持有時間分布,分布區(qū)間為[0-1 ms, 1-10 ms, 10-100 ms, 100ms-1s, 1-10 s, 10-100 s, 100-1000 s, >1000 s] |
在JMeter中的吞吐量為:37.3/second
測試分析:
- 首先連接持有時間大幅度下降,原先慢速組100%的樣本數(shù)據(jù)在
1-10 s
區(qū)間,現(xiàn)在改區(qū)間只有0.98%的數(shù)據(jù)。 - 我們發(fā)現(xiàn)連接持有時間分布樣本數(shù)據(jù)總和是8000,而不是原本慢速組的4000,并且在
0-1 ms, 1-10 ms, 10-100 ms, 100ms-1s
都有分布,說明一個問題,query查詢單獨占用了數(shù)據(jù)庫連接。 - 我們知道數(shù)據(jù)庫連接是寶貴資源,query單獨占用連接是否有問題?從上面的測試結(jié)果分析,query雖然單獨占用資源,但是時間很短,并且也是復用了連接池的資源,相當于把長事務分攤到短操作中。是否還有更優(yōu)配置呢?
- 所以我們還需要做一組測試,把query放入事務中試試看。本次連接持有時間分布總樣本中位數(shù)參考值是 1458262 ms。
- 最后,吞吐量有所提升,總體上這個優(yōu)化方案是有效果的。
6.3、優(yōu)化第二組測試
代碼優(yōu)化如下:
@Override public List<TestUser> optimizedGetAll2() { slowMethod(customProperties.getSlowMillis()); TestUser user1 = getTestUser("王五", "jack"); TestUser user2 = getTestUser("趙柳", "amy"); return transactionTemplate.execute(new TransactionCallback<List<TestUser>>() { @Override public List<TestUser> doInTransaction(TransactionStatus status) { testUserDao.insert(user1); testUserDao.insert(user2); return testUserDao.queryByBlurry(user2); } }); }
druid連接池主要結(jié)果分析:
指標 | 值 | 解釋 |
---|---|---|
事務時間分布 | 0,0,3778,222,0,0,0 | 事務運行時間分布,分布區(qū)間為[0-1 ms, 1-10 ms, 10-100 ms, 100-1 s, 1-10 s, 10-100 s, >100 s] |
連接持有時間分布 | 0,0,2278,1611,111,0,0,0 | 連接持有時間分布,分布區(qū)間為[0-1 ms, 1-10 ms, 10-100 ms, 100ms-1s, 1-10 s, 10-100 s, 100-1000 s, >1000 s] |
在JMeter中的吞吐量為:38.0/second
測試分析:
- 對比一二組,首先事務分布時間是改善的,但是并不明顯;
- 本次連接持有時間分布總樣本中位數(shù)參考值是 1474400 ms,對比一二組,第二組比第一組多耗費 16138 ms;
- 在該樣本的情況下,第一組實際持有連接的時間更短,壓力更為平均。
- 將不必要的query放入事務本身就是不推薦,本示例中query查詢的數(shù)據(jù)量較小,如果數(shù)據(jù)量較大,從理論上分析,會明顯影響事務的提交。
- 吞吐量基本無變化,這個是符合預期的。
- 所以這次優(yōu)化,推薦使用第一組的方式。
7、總結(jié)
- 在系統(tǒng)初次上線前,可以考慮編寫測試用例用壓測的方式拿到服務器的性能指標。
- 在系統(tǒng)完成開發(fā)后,可以考慮對簡單、中等、高復雜度的API分別進行壓測摸清API的性能體現(xiàn),以及對比服務器性能指標,有可能在上線前就能排查出問題。
- 在系統(tǒng)上線后,如果遇到應用性能下降或并發(fā)問題,可以通過觀察連接池和SQL分析定位大致的java代碼塊,解決問題最終還是需要針對性編寫測試用例再次復盤測試。
- 大部分情況下,系統(tǒng)缺省的參數(shù)都夠你使用,任何一次參數(shù)優(yōu)化調(diào)整都應該有理論支撐和實踐證明,云調(diào)參張口就來誰都會。
- 并發(fā)調(diào)優(yōu)一定要抓住關鍵目標,找到核心資源,分析并發(fā)瓶頸鏈路。
- 數(shù)據(jù)庫連接池還有很多參數(shù)是可以幫助調(diào)優(yōu)分析的,本文沒有介紹,jmeter的壓測的方式也是多種多樣,應根據(jù)不同的場景調(diào)整。
- 并發(fā)調(diào)優(yōu)是一個復雜的過程,從應用所有的關聯(lián)點都可能出現(xiàn)問題,本文只是從開發(fā)角度,針對請求長連接以及數(shù)據(jù)庫長事務做了簡單的分析和推測。
到此這篇關于Springboot并發(fā)調(diào)優(yōu)之大事務和長連接的文章就介紹到這了,更多相關Springboot并發(fā)調(diào)優(yōu)之大事務和長連接內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java ArrayList與LinkedList及HashMap容器的用法區(qū)別
這篇文章主要介紹了Java ArrayList與LinkedList及HashMap容器的用法區(qū)別,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-07-07springcloud項目快速開始起始模板的實現(xiàn)
本文主要介紹了springcloud項目快速開始起始模板思路的實現(xiàn),文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12maven倉庫中心mirrors配置多個下載中心(執(zhí)行最快的鏡像)
這篇文章主要介紹了maven倉庫中心mirrors配置多個下載中心(執(zhí)行最快的鏡像),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07