druid釋放空閑連接的問題


一、問題背景


  最近在某個項目的生產環境碰到一個數據庫連接問題,使用的連接池是alibaba的druid_1.1.10,問題表現為:DBA監測到應用集群到oracle的連接數總會在半夜降低,並且大大低於每個節點druid配置的minIdle總和。
   一開始懷疑此問題產生的原因是oracle側主動關閉了連接,但很難去驗證這個點,一方面是和DBA溝通起來比較麻煩,另一方面是沒有確切的證據,純粹靠猜想很難服眾,所以退而求其次,嘗試在druid連接池上去找原因。既然是半夜這種交易量小的時間點降低連接數,那么應該和druid對空閑連接的處理有關。
  在github拉取了druid源碼后,載入idea,使用minEvictableIdleTimeMillis進行了全局搜索,在結果列表中找到了一些可能與連接回收有關的類,最終定位到了DruidDataSource的內部類DestoryTask,簡單的掃了一眼代碼之后,基本就能確定DestroyTask是用於負責檢測和銷毀空閑連接的類了。
  由於druid源碼編譯還得花時間研究,我直接搭建了一個簡單的springboot工程,引入druid后對DruidDataSource的init()方法打斷點,啟動應用開始一步步調試...
 

二、源碼分析


  DruidDataSource init時會啟動一個銷毀連接的線程,由於destoryScheduler為空,因此創建了DestroyConnectionThread線程去執行,如下圖:

   DestroyConnectionThread做的事情很簡單,就是每隔固定的時間去執行一下DestoryTask的run方法,執行的間隔時間基於druid配置timeBetweenEvictionRunsMillis的值:

 

   DestoryTask的run方法調用shrink方法,該方法是空閑連接檢查的核心方法,至於removeAbandoned方法是用於回收借出去但一直未歸還的連接(這種連接可能導致連接泄露),它與druid的配置removeAbandoned有關,這里就不細講了:

  shrink方法邏輯如下:

public void shrink(boolean checkTime, boolean keepAlive) {
    //加鎖
    try {
        lock.lockInterruptibly();
    } catch (InterruptedException e) {
        return;
    }

    int evictCount = 0; //需要剔除的個數
    int keepAliveCount = 0; //需要保持會話的個數
    try {
        if (!inited) {
            return;
        }
	 	//要檢查的個數=連接池當前連接個數 - 最小空閑連接數
        final int checkCount = poolingCount - minIdle; 
        //檢查時間點
        final long currentTimeMillis = System.currentTimeMillis(); 
		//遍歷當前連接池的所有連接
        for (int i = 0; i < poolingCount; ++i) {
            DruidConnectionHolder connection = connections[i];

            //DestroyThread調用shrink時,checkTime=true,keepAlive基於配置的值(默認為false)
            if (checkTime) {
                //phyTimeoutMillis參數(默認值為-1)設定了一條物理連接的存活時間,
                //不同的數據庫對一個連接有最大的維持時間,比如mysql是8小時,設置該
                //參數是為了防止應用獲取某連接時,該連接在數據庫側已關閉而導致異常。
                if (phyTimeoutMillis > 0) {
                    //如果某條連接已超過phyTimeoutMillis,則將其放入需要剔除的連接數組evictConnections中
                    long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
                    if (phyConnectTimeMillis > phyTimeoutMillis) {
                        evictConnections[evictCount++] = connection;
                        continue;
                    }
                }

                //獲取連接空閑時間
                long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
				
                //如果某條連接空閑時間小於minEvictableIdleTimeMillis,則不用繼續檢查剩下的連接了
                if (idleMillis < minEvictableIdleTimeMillis) {
                    break;
                }

                //判斷此連接的狀態,將其放入不同處理的連接數組中
                if (checkTime && i < checkCount) {
                    //這里checkTime有點多余,一定為true,因為它是if(checkTime)分支中的邏輯
                    //如果此連接仍在checkCount范圍之內,即它是一個多出最小空閑連接數的連接,
                    //那么就將它加入到需要剔除的連接數組evictConnections中
                    evictConnections[evictCount++] = connection;
                } else if (idleMillis > maxEvictableIdleTimeMillis) {
                    //如果連接空閑時間已經大於maxEvictableIdleTimeMillis,也將它加入到需要
                    //剔除的連接數組evictConnections中
                    evictConnections[evictCount++] = connection;
                } else if (keepAlive) {
                    //如果連接超過checkCount范圍,並且空閑時間小於maxEvictableIdleTimeMillis,
                    //並且開啟了keepAlive,那么就將它加入到需要維持的連接數組keepAliveConnections中
                    keepAliveConnections[keepAliveCount++] = connection;
                }
            } else {
                //對於不需要checkTime的情形,就非常簡單了,將比minIdle連接數多的連接放入
                //需要剔除的連接數組evictConnections中
                if (i < checkCount) {
                    evictConnections[evictCount++] = connection;
                } else {
                    break;
                }
            }
        }

        //剔除連接和需要維持的連接都作為被移出連接,然后對連接池中的connections元素進行移動,
        //使得有用的連接重新放在連接數組connections的頭部,並將其余元素置為null
        int removeCount = evictCount + keepAliveCount;
        if (removeCount > 0) {
            System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
            Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
            poolingCount -= removeCount;
        }
        keepAliveCheckCount += keepAliveCount;
    } finally {
        lock.unlock();
    }

    //處理需要剔除的連接數組evictConnections,對其中的連接進行關閉,
    //並維護監控指標:destroyCountUpdater,然后將evictConnections清空
    if (evictCount > 0) {
        for (int i = 0; i < evictCount; ++i) {
            DruidConnectionHolder item = evictConnections[i];
            Connection connection = item.getConnection();
            JdbcUtils.close(connection);
            destroyCountUpdater.incrementAndGet(this);
        }
        Arrays.fill(evictConnections, null);
    }

    //處理需要維持連接的連接數組keepAliveConnections
    if (keepAliveCount > 0) {
        //維護監控指標
        this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount); 
        for (int i = keepAliveCount - 1; i >= 0; --i) {
            DruidConnectionHolder holer = keepAliveConnections[i];
            Connection connection = holer.getConnection();
            //更新連接的keepAlive檢查計數器
            holer.incrementKeepAliveCheckCount(); 

            boolean validate = false;
            try {
                //使用配置的validationQuery Sql檢查當前連接是否有效,validateConnection
                //方法非常簡單,如果檢查過程中拋出異常都會被此處catch住並處理
                this.validateConnection(connection);
                validate = true;
            } catch (Throwable error) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("keepAliveErr", error);
                }
                // skip
            }
			
            if (validate) {
                //如果連接有效性檢查成功,則更新連接的最近活躍時間,並嘗試將連接放回連接池,
                //put(holder)不一定保證放回成功,在連接池已滿的情況下將不會放入,方法中通過
                //使用條件變量以及poolingPeak等機制保證了連接不會被泄露
                holer.lastActiveTimeMillis = System.currentTimeMillis();
                put(holer);
            } else {
                //如果連接有效性檢查失敗,則關閉此連接
                JdbcUtils.close(connection);
            }
        }
        //清空連接數組keepAliveConnections
        Arrays.fill(keepAliveConnections, null);
    }
}

 

三、驗證結論


  根據調試過程中的源碼分析,可知druid_1.1.10判斷連接是否銷毀還是保活的邏輯如下(只討論checkTime為true的情況):
 

 

  到這里,我們就可以下一個結論了:druid對於空閑連接還是有可能回收的,只要它未開啟keepAlive並且閑置時間過長就會回收空閑連接,從而使得連接池中的連接數小於配置的minIdle值。

  為了驗證結論,我開啟了druid monitor的web頁面訪問,然后在如下的頁面中去觀察池中連接的情況:

 

  與druid空閑連接回收的相關參數配置如下圖:

 

  首先不開啟keepAlive功能(druid也是默認關閉的),在應用啟動的時候,從druid monitor中觀察到連接池中的連接數如下:

  等待大約2~3分鍾之后(再此期間不要發起任何數據庫請求),再次觀察連接池中的連接數,可以發現連接數為0:

 

  接着配置"spring.datasource.druid.keep-alive=true"以打開keepAlive,重啟應用並重復上述過程,結果如下:

 

  可以發現keepAlive起作用了,池中連接數維持在20,結論得到驗證。接着回過頭去查看了一下maxEvictableIdleTimeMillis這個參數的默認值為25200000,剛好7個小時,差不多能和DBA監測到的連接降低時間對上。

 

四、其他發現


  在解決問題的過程中,參考了官方文檔以及他人在druid項目中提的issue,經歷了懷疑問題、確認問題、解決問題三個階段,不過個人在調試過程中仍然發現有如下問題:
(1)官方的配置文檔中對屬性minEvictableIdleTimeMillis做了如下描述:

 

  然而實際上代碼體現出來的邏輯並不是這么一回事,maxEvictableIdleTimeMillis更像起到了決定性的作用。

(2)timeBetweenEvictionRunsMillis、minEvictableIdleTimeMillis、maxEvictableIdleTimeMillis這三者設置的大小如果滿足一定條件,也會導致keepAlive失效。根據源碼,如果在某一輪掃描中(間隔時間timeBetweenEvictionRunsMillis),檢測到連接的空閑時間小於minEvictableIdleTimeMillis,那么這些連接不需要keepAlive,自然也不會更新lastActiveTimeMillis,這里存在一個臨界條件,使得連接空閑時間同時大於minEvictableIdleTimeMillis和maxEvictableIdleTimeMillis,這個臨界條件觸發的前提是:
//1.滿足下面不等式
maxEvictableIdleTimeMillis - minEvictableIdleTimeMillis <= timeBetweenEvictionRunsMillis
//2.連接一直處於未使用狀態,那么在空閑時間小於minEvictableIdleTimeMillis之前,連接的lastActiveTimeMillis都不會被更新

  下面是我的一個測試,druid相關配置情況如圖:

 

  啟用應用並靜靜等待1~2分鍾,通過druid monitor查看連接池狀態:

 

  通過瀏覽器調用一個http查詢接口,連接池連接數恢復:

 

  靜靜等待1~2分鍾,可以看到連接池中的連接又被清空:

 

 

  結論:雖然maxEvictableIdleTimeMillis這個參數我們一般不配置,它的默認值也比較大(7小時),但是實際在配置druid時,還是建議考慮keepAlive失效的因素,作為配置的一個考量。

 

五、參考資料



免責聲明!

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



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