踩坑DruidDataSource導致的服務卡死


在我們項目中,Druid提供了一個高效、功能強大、可擴展性好的數據庫連接池。我們使用他來替代C3P0作為數據庫的連接池;

翻車背景

平台私有化給一個三十人的小團隊使用,某天有人反饋平台無響應,接口全部超時無響應;

排查過程
  1. 連上服務器,發現服務狀態都健康,內存CPU等都很穩定;

  2. jstack查看線程狀態,發現所有容器工作線程都是wait狀態,如下:

    "XNIO-1 task-5" #178 prio=5 os_prio=0 tid=0x000000002a03e000 nid=0x350c waiting on condition [0x000000004013a000]
       java.lang.Thread.State: WAITING (parking)
            at sun.misc.Unsafe.park(Native Method)
            - parking to wait for  <0x00000006c4f36dd0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
            at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
            at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
            at com.alibaba.druid.pool.DruidDataSource.takeLast(DruidDataSource.java:2029)
            at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1557)
            at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:1337)
            at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1317)
            at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1307)
            at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:109)
            at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:158)
            at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:116)
            at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:79)
            at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:82)
            at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:68)
    
    

    可以看見所有線程都卡在了DruidDataSource.takeLast方法的2029行,我們查看源碼找到這行代碼:

        // maxWait是 獲取連接等待超時時間 默認是-1,即不超時,此時會走到takeLast方法
    	if (maxWait > 0) {
            holder = pollLast(nanos);
        } else {
            holder = takeLast();
        }
    
    	DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
            try {
                while (poolingCount == 0) {
                    emptySignal(); // send signal to CreateThread create connection
    
                    if (failFast && isFailContinuous()) {
                        throw new DataSourceNotAvailableException(createError);
                    }
    
                    notEmptyWaitThreadCount++;
                    if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) {
                        notEmptyWaitThreadPeak = notEmptyWaitThreadCount;
                    }
                    try {
                        // 因為數據庫的連接都沒有釋放且被占用,連接池中無可用連接,導致請求被阻塞了
                        notEmpty.await(); // signal by recycle or creator
                    } finally {
                        notEmptyWaitThreadCount--;
                    }
                    notEmptyWaitCount++;
    
                    if (!enable) {
                        connectErrorCountUpdater.incrementAndGet(this);
                        throw new DataSourceDisableException();
                    }
                }
            } catch (InterruptedException ie) {
                notEmpty.signal(); // propagate to non-interrupted thread
                notEmptySignalCount++;
                throw ie;
            }
    
            decrementPoolingCount();
            DruidConnectionHolder last = connections[poolingCount];
            connections[poolingCount] = null;
    
            return last;
        }
    

找到問題所在后,我們找到服務排查為什么會出現這樣的情景,發現服務里使用了多數據源,這就導致了必須自己創建DataSource,繼續查看創建DataSource的地方

    @Bean("masterDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build();
    }

這里創建的時候直接使用了DruidDataSource的默認配置,沒有手動修改任何配置,而在DruidDataSource中maxwait默認值為-1:

    public final static int                            DEFAULT_MAX_WAIT                          = -1;
    protected volatile int                             maxActive                                 = DEFAULT_MAX_ACTIVE_SIZE;8個

這就會導致代碼分叉到takeLast上,然后剛好如果所有線程都被使用且沒有正常釋放,那么就會導致一直await卡死;

場景復現

那么問題的發生場景就很明了了,需要兩個條件,

  • maxWait使用默認值-1,即不會超時

  • poolingCount == 0,即線程池可用數量為0

    其余參數均使用默認值;

我們嘗試使用Jmeter來復現問題;

Jmeter設置100個並發,在300秒內啟動,觀察服務;發現在並發13個左右(多次)線程就會導致服務卡死,此時Jstack查看線程發現所有線程均在wait;

因為maxActive默認值為8,所以超過8個后,一旦數據庫連接不及時釋放,則會導致poolingCount ==0,此時剛好maxWait為-1,則進入takeLast復現此問題

嘗試解決

在網上搜索‘ druid的takeLast導致卡死’,可以看到很多人遇到了同樣的問題;而且在官方的issue也可以查到相關的提交;如下:

https://github.com/alibaba/druid/issues/2376

https://github.com/alibaba/druid/issues/1160

可以看到里面的回復大多是貼上一兩個配置,其實這樣都是不可解決問題的;知其然知其所以然,我們必須弄明白問題的本事在哪里,才可以解決問題;

就此問題本身來說,看着代碼思考,那么我設置下這個參數使得參數不為默認值(-1),讓他避免這個問題不就可以了嗎?

dataSource.setMaxWait(100);

我們設置超時時間(毫秒)為100,此時我們通過壓測再來驗證一下這個問題,結果發現繼續報錯,錯誤為獲取不到連接;

因為連接默認只有8個,設置超時時間后,因為連接不能及時的釋放,所以有可能拿不到連接;

### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 101, active 8, maxActive 8, creating 0
	at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:150)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
	at sun.reflect.GeneratedMethodAccessor268.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
	... 104 common frames omitted
Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 101, active 8, maxActive 8, creating 0

所以,我們還需要配置一下連接數來保證可用,以下為druid的配置說明,可以酌情使用:

屬性 說明 建議值
url 數據庫的jdbc連接地址。一般為連接oracle/mysql。示例如下:
mysql : jdbc:mysql://ip:port/dbname?option1&option2&…
oracle : jdbc:oracle:thin:@ip:port:oracle_sid
username 登錄數據庫的用戶名
password 登錄數據庫的用戶密碼
initialSize 啟動程序時,在連接池中初始化多少個連接 10-50已足夠
maxActive 連接池中最多支持多少個活動會話
maxWait 程序向連接池中請求連接時,超過maxWait的值后,認為本次請求失敗,即連接池 100
沒有可用連接,單位毫秒,設置-1時表示無限等待
minEvictableIdleTimeMillis 池中某個連接的空閑時長達到 N 毫秒后, 連接池在下次檢查空閑連接時,將回收該連接,要小於防火牆超時設置 見說明部分
net.netfilter.nf_conntrack_tcp_timeout_established的設置
timeBetweenEvictionRunsMillis 檢查空閑連接的頻率,單位毫秒, 非正整數時表示不進行檢查
keepAlive 程序沒有close連接且空閑時長超過 minEvictableIdleTimeMillis,則會執 true
行validationQuery指定的SQL,以保證該程序連接不會池kill掉,其范圍不超過minIdle指定的連接個數
minIdle 回收空閑連接時,將保證至少有minIdle個連接. 與initialSize相同
removeAbandoned 要求程序從池中get到連接后, N 秒后必須close,否則druid 會強制回收該連接,不管該連接中是活動還是空閑, 以防止進程不會進行close而霸占連接。 false,當發現程序有未正常close連接時設置為true
removeAbandonedTimeout 設置druid 強制回收連接的時限,當程序從池中get到連接開始算起,超過此 應大於業務運行最長時間
值后,druid將強制回收該連接,單位秒。
logAbandoned 當druid強制回收連接后,是否將stack trace 記錄到日志中 true
testWhileIdle 當程序請求連接,池在分配連接時,是否先檢查該連接是否有效。(高效) true
validationQuery 檢查池中的連接是否仍可用的 SQL 語句,drui會連接到數據庫執行該SQL, 如果
正常返回,則表示連接可用,否則表示連接不可用
testOnBorrow 程序 申請 連接時,進行連接有效性檢查(低效,影響性能) false
testOnReturn 程序 返還 連接時,進行連接有效性檢查(低效,影響性能) false
poolPreparedStatements 緩存通過以下兩個方法發起的SQL: true
public PreparedStatement prepareStatement(String sql)
public PreparedStatement prepareStatement(String sql,
int resultSetType, int resultSetConcurrency)
maxPoolPrepareStatementPerConnectionSize 每個連接最多緩存多少個SQL 20
filters 這里配置的是插件,常用的插件有: stat,wall,slf4j
監控統計: filter:stat
日志監控: filter:log4j 或者 slf4j
防御SQL注入: filter:wall
connectProperties 連接屬性。比如設置一些連接池統計方面的配置。
druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
比如設置一些數據庫連接屬性

了解了每個配置的作用,那么我們只需要合理的進行配置即可解決這個問題。以下為經過壓測驗證后的我們服務的配置,供參考:

// 初始連接數
dataSource.setInitialSize(10);
// 最小連接池數量
dataSource.setMinIdle(10);
// 最大連接池數量
dataSource.setMaxActive(100);
// 配置獲取連接等待超時時間  毫秒
dataSource.setMaxWait(100);
//緩存通過以下兩個方法發起的SQL:
dataSource.setPoolPreparedStatements(true);
//每個連接最多緩存多少個SQL
dataSource.setMaxPoolPreparedStatementPerConnectionSize(50);
//檢查空閑連接的頻率,單位毫秒, 非正整數時表示不進行檢查
dataSource.setTimeBetweenEvictionRunsMillis(-1);
//池中某個連接的空閑時長達到 N 毫秒后, 連接池在下次檢查空閑連接時,將回收該連接,要小於防火牆超時設置
dataSource.setMinEvictableIdleTimeMillis(300000);
//當程序請求連接,池在分配連接時,是否先檢查該連接是否有效。(高效)
dataSource.setTestWhileIdle(true);
// 程序 申請 連接時,進行連接有效性檢查(低效,影響性能)
dataSource.setTestOnBorrow(false);
//程序 返還 連接時,進行連接有效性檢查(低效,影響性能)
dataSource.setTestOnReturn(false);
// 要求程序從池中get到連接后, N 秒后必須close,否則druid 會強制回收該連接,不管該連接中是活動還是空閑, 以防止進程不會進行close而霸占連接。
dataSource.setRemoveAbandoned(true);
// 設置druid 強制回收連接的時限,當程序從池中get到連接開始算起,超過此值后,druid將強制回收該連接,單位秒。
// 結合業務來看,存在jpa極大事務;不好設置 暫時為設置兩分鍾
dataSource.setRemoveAbandonedTimeout(120);
//當druid強制回收連接后,是否將stack trace 記錄到日志中
dataSource.setLogAbandoned(true);

題外話:

對於此類問題,官方給的回復是我們沒有正確的關閉連接,導致連接泄漏;

這樣通過配置的方法雖然解決了問題,但是為什么其他連接池就沒有此類問題;也不需要人為的來通過配置避免此'BUG'呢?不得而知


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM