背景
之前在壓測一個用java寫的應(yīng)用時,偶爾會報如下的錯誤:
org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction; nested exception is java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30002ms.
at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:305)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:378)
at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:474)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:289)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
at com.quantil.cm.api.service.PurgeTaskService$$EnhancerBySpringCGLIB$$b4db5835.create(<generated>)
排查過程
1. MySQL最大連接數(shù)排查
show variables like '%conn%'
MySQL設(shè)置的最大連接數(shù)為32768聂使,API開啟的連接池也只有100個,因此排除這個原因
2. MySQL連接池泄露
百度找了一下相關(guān)資料贱呐,發(fā)現(xiàn)有可能是因為MyBatis自定義攔截器沒有釋放連接的原因供屉。
因為這個java應(yīng)用也自定義了一個攔截器用來分頁瘟栖,但是連接本地數(shù)據(jù)庫進行壓測的時候并沒有出現(xiàn)這個錯誤横蜒,因此也排除了連接池泄露的原因
3. 源碼研究
直接在IDEA查找關(guān)鍵字Connection is not available
,發(fā)現(xiàn)只有在HikariPool.java的方法getConnection
里面會拋錯throw createTimeoutException(startTime)
方法代碼如下:
public Connection getConnection(final long hardTimeout) throws SQLException
{
suspendResumeLock.acquire();
final long startTime = currentTime();
try {
long timeout = hardTimeout;
do {
PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
if (poolEntry == null) {
break; // We timed out... break and throw exception
}
final long now = currentTime();
if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
timeout = hardTimeout - elapsedMillis(startTime);
}
else {
metricsTracker.recordBorrowStats(poolEntry, startTime);
return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
}
} while (timeout > 0L);
metricsTracker.recordBorrowTimeoutStats(startTime);
throw createTimeoutException(startTime);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
}
finally {
suspendResumeLock.release();
}
}
大概看了一下這個方法做的事情右核,其實就是從連接池里面去borrow
一個連接慧脱,然后返回。如果borrow
了30s還沒borrow
到贺喝,那么就會拋錯了菱鸥。
這個時候真相呼之欲出了宗兼,什么情況會造成獲取連接池連接超時?
- 連接泄露
某些線程拿到了連接后沒有歸還氮采,持續(xù)久了就出問題 - 連接歸還連接池太慢
極端點殷绍,數(shù)據(jù)庫響應(yīng)時間要花1s,10個連接池1秒也只能處理10個請求鹊漠。這個時候如果有500個并發(fā)那得50s才能處理完主到,這不就超時了 - 連接歸還連接池很快,但是借的線程太多
數(shù)據(jù)庫響應(yīng)很快躯概,無奈并發(fā)太高登钥,遠超連接池大小。假設(shè)連接池只有10個連接娶靡,但是有100個線程在borrow
連接的話牧牢,是不是有可能會出現(xiàn)某些倒霉的線程30s一直borrow
不到連接的情況?
本例排除了前兩種可能姿锭,因此猜測是第三種原因塔鳍。 并發(fā)數(shù)遠大于連接池大小,導(dǎo)致有可能出現(xiàn)某些線程30s內(nèi)一直獲取不到連接呻此,從而拋錯
bug本地復(fù)現(xiàn)
既然是懷疑并發(fā)數(shù)大于連接池大小大致的轮纫,那我們將hikari的線程數(shù)設(shè)置成10(設(shè)置小點避免引入數(shù)據(jù)庫的干擾),然后開500并發(fā)來測試本地java應(yīng)用
spring.datasource.hikari.maximum-pool-size=10
ab壓測命令
ab -c 500 -n 50000 -p body.json http://127.0.0.1:8080/api/test
本地數(shù)據(jù)庫復(fù)現(xiàn)
無法復(fù)現(xiàn)焚鲜?蜡感??恃泪?VP恕!1春酢情连!
10個連接,500個并發(fā)都能完美處理過來@佬АH匆ā!
懷疑是因為本地通信太快了锤灿,500個并發(fā)可能還不足以看出來挽拔。(這里沒有再往上調(diào)并發(fā)數(shù),理論上應(yīng)該調(diào)大也能復(fù)現(xiàn)但校,就是不知道得多大螃诅。。。)遠程數(shù)據(jù)庫復(fù)現(xiàn)
換成和java應(yīng)用不在同一個內(nèi)網(wǎng)的外網(wǎng)數(shù)據(jù)庫
ab剛啟動沒一會术裸,java應(yīng)用就拋出了這個可愛的錯誤倘是!
nice!
如何解決
既然問題是因為并發(fā)數(shù)遠大于連接池大小導(dǎo)致的袭艺,那么有以下幾個解決方案:
- 增加連接池大小
簡單暴力搀崭,如果你可以預(yù)見并發(fā)數(shù)的上限的話 - 數(shù)據(jù)庫和api部署在同一個內(nèi)網(wǎng)
加快單個請求的響應(yīng)速度,較少線程等待時間
其實個人以為這個問題基本無解猾编,要是并發(fā)數(shù)無限提高的話還是一樣會出問題瘤睹。
這個問題就和偶爾網(wǎng)絡(luò)抽風(fēng)一樣,概率挺小的答倡,感覺可以不用管~
后話
該錯誤的本質(zhì)原因其實就是線程去borrow
連接超時拋出的轰传,超時的原因說白了就是超時時間內(nèi)一直拿不到可用連接,因此可以從這方面下手去排查苇羡。