溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何解決Druid-由防火墻導(dǎo)致的數(shù)據(jù)庫(kù)空閑連接斷開問(wèn)題

發(fā)布時(shí)間:2021-07-08 17:33:21 來(lái)源:億速云 閱讀:578 作者:chen 欄目:大數(shù)據(jù)

這篇文章主要介紹“如何解決Druid-由防火墻導(dǎo)致的數(shù)據(jù)庫(kù)空閑連接斷開問(wèn)題”,在日常操作中,相信很多人在如何解決Druid-由防火墻導(dǎo)致的數(shù)據(jù)庫(kù)空閑連接斷開問(wèn)題問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”如何解決Druid-由防火墻導(dǎo)致的數(shù)據(jù)庫(kù)空閑連接斷開問(wèn)題”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

問(wèn)題描述

公司一個(gè)新項(xiàng)目上線,處于試運(yùn)行階段,這個(gè)項(xiàng)目雖然是外網(wǎng)可訪問(wèn)的,故部署在了DMZ區(qū),但試運(yùn)行階段只給了公司內(nèi)少部分員工地址和賬號(hào)(其中包括一些領(lǐng)導(dǎo)),故訪問(wèn)量很小,但項(xiàng)目還是挺重要的。
試運(yùn)行階段中,項(xiàng)目應(yīng)用日志中不定期會(huì)報(bào)異常,尤其是在剛上午剛開始使用時(shí),還有空閑一段時(shí)間后再次使用時(shí),具體異常如下:

ERROR [com.alibaba.druid.util.JdbcUtils] - close connection error
java.sql.SQLRecoverableException: IO Error: Broken pipe
    at oracle.jdbc.driver.T4CConnection.logoff(T4CConnection.java:556)
    at oracle.jdbc.driver.PhysicalConnection.close(PhysicalConnection.java:3984)
    at com.alibaba.druid.filter.FilterChainImpl.connection_close(FilterChainImpl.java:167)
    at com.alibaba.druid.filter.stat.StatFilter.connection_close(StatFilter.java:254)
    at com.alibaba.druid.filter.FilterChainImpl.connection_close(FilterChainImpl.java:163)
    at com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl.close(ConnectionProxyImpl.java:115)
    at com.alibaba.druid.util.JdbcUtils.close(JdbcUtils.java:79)
    at com.alibaba.druid.pool.DruidDataSource.discardConnection(DruidDataSource.java:965)
    at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:932)
    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4534)
    at com.alibaba.druid.filter.stat.StatFilter.dataSource_getConnection(StatFilter.java:661)
    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4530)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:884)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:876)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:92)
    at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:205)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:420)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:257)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxyDynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)atxxx.xx.modules.deposit.api.service.DepositApiService

EnhancerBySpringCGLIB

$59c8f6e2.doRecharge()
    at xxx.xx.modules.deposit.FundDepositController.rechargeConfirm(FundDepositController.java:125)
......

Caused by: java.net.SocketException: Broken pipe
    at java.net.SocketOutputStream.socketWrite0(Native Method)
    at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:113)
    at java.net.SocketOutputStream.write(SocketOutputStream.java:159)
    at oracle.net.ns.DataPacket.send(DataPacket.java:210)
    at oracle.net.ns.NetOutputStream.flush(NetOutputStream.java:230)
    at oracle.net.ns.NetInputStream.getNextPacket(NetInputStream.java:312)
    at oracle.net.ns.NetInputStream.read(NetInputStream.java:260)
    at oracle.net.ns.NetInputStream.read(NetInputStream.java:185)
    at oracle.net.ns.NetInputStream.read(NetInputStream.java:102)
    at oracle.jdbc.driver.T4CSocketInputStreamWrapper.readNextPacket(T4CSocketInputStreamWrapper.java:124)
    at oracle.jdbc.driver.T4CSocketInputStreamWrapper.read(T4CSocketInputStreamWrapper.java:80)
    at oracle.jdbc.driver.T4CMAREngine.unmarshalUB1(T4CMAREngine.java:1137)
    at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:290)
    at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:192)
    at oracle.jdbc.driver.T4C7Ocommoncall.doOLOGOFF(T4C7Ocommoncall.java:61)
    at oracle.jdbc.driver.T4CConnection.logoff(T4CConnection.java:543)
    ... 69 more

從異常信息可以看出,問(wèn)題是發(fā)生在Druid數(shù)據(jù)庫(kù)連接池在關(guān)閉物理數(shù)據(jù)庫(kù)連接時(shí),報(bào)了 SocketException: Broken pipe,但為什么在使用時(shí)Druid會(huì)關(guān)閉數(shù)據(jù)庫(kù)連接,關(guān)閉數(shù)據(jù)連接又為什么會(huì)報(bào)SocketException呢?這個(gè)異常到底對(duì)系統(tǒng)有多大的影響呢?下面一步步分析。

問(wèn)題逐步分析

1、java.net.SocketException: Broken pipe異常是怎么產(chǎn)生的?有什么影響?

項(xiàng)目中使用是的Druid連接數(shù)據(jù)庫(kù),可為什么在系統(tǒng)空閑一段時(shí)間后再使用,會(huì)嘗試關(guān)閉數(shù)據(jù)庫(kù)連接,而且關(guān)閉的時(shí)候還拋了 java.net.SocketException: Broken pipe 呢?
從異常堆棧信息,或者翻看Druid源碼可以知道,異常是發(fā)生在從數(shù)據(jù)庫(kù)連接池中獲取連接,用于后續(xù)數(shù)據(jù)庫(kù)操作時(shí),在執(zhí)行到DruidDataSource.getConnectionDirect(maxWaitMillis)方法時(shí),有如下邏輯:

public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
    //循環(huán)
    for (;;) {
        //maxWaitMillis時(shí)間內(nèi)從連接池獲取一個(gè)連接
        DruidPooledConnection poolalbeConnection = getConnectionInternal(maxWaitMillis);

        //testOnBorrow為true,即從池中獲取連接后需要檢查連接
        if (isTestOnBorrow()) {
            boolean validate = testConnectionInternal(poolalbeConnection.getConnection());
            if (!validate) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("skip not validate connection.");
                }

                Connection realConnection = poolalbeConnection.getConnection();
                discardConnection(realConnection);
                continue;
            }
        } 
        else {
            Connection realConnection = poolalbeConnection.getConnection();
            //如果連接已經(jīng)關(guān)閉,再?gòu)某刂蝎@取一個(gè)
            if (realConnection.isClosed()) {
                discardConnection(null); // 傳入null,避免重復(fù)關(guān)閉
                continue;
            }

            //testWhileIdle為true,即空閑后需要檢查連接
            if (isTestWhileIdle()) {
                //連接空閑時(shí)間(當(dāng)前時(shí)間 - 上次ActiveTime)
                long idleMillis = System.currentTimeMillis()
                                  - poolalbeConnection.getConnectionHolder().getLastActiveTimeMillis();
                
                //連接空閑時(shí)間 > timeBetweenEvictionRunsMillis,檢查連接
                if (idleMillis >= this.getTimeBetweenEvictionRunsMillis()) {
                    boolean validate = testConnectionInternal(poolalbeConnection.getConnection());
                    
                    //連接檢查失敗,打印log,丟棄連接,再獲取一個(gè)連接
                    if (!validate) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("skip not validate connection.");
                        }

                        discardConnection(realConnection);
                        continue;
                    }
                }
            }
        }

        //如果開啟了連接超時(shí)回收
        if (isRemoveAbandoned()) {
            StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
            poolalbeConnection.setConnectStackTrace(stackTrace);
            poolalbeConnection.setConnectedTimeNano(); //設(shè)置當(dāng)前時(shí)間為ConnectedTime
            poolalbeConnection.setTraceEnable(true);

            synchronized (activeConnections) {
                activeConnections.put(poolalbeConnection, PRESENT); //將連接放入activeConnections Map
            }
        }

        if (!this.isDefaultAutoCommit()) {
            poolalbeConnection.setAutoCommit(false);
        }

        return poolalbeConnection;
    }
}

簡(jiǎn)單來(lái)說(shuō),在從Druid獲取數(shù)據(jù)庫(kù)連接時(shí),可以進(jìn)行test,這段代碼中包含testOnBorrow(借出時(shí)檢查)和testWhileIdle(空閑時(shí)檢查)的邏輯,此項(xiàng)目在配置文件中

testOnBorrow = false
testWhileIdle = true
timeBetweenEvictionRunsMillis = 60000(60s)

故只會(huì)在連接空閑60s后再次使用時(shí)進(jìn)行檢測(cè),其實(shí)就是執(zhí)行一個(gè)SQL,而在執(zhí)行SQL時(shí)如果失敗了,就會(huì)調(diào)用JdbcUtils.close(realConnection)關(guān)閉連接,在關(guān)閉這個(gè)連接時(shí)拋了SocketException異常,但其實(shí)這個(gè)異常倒不會(huì)對(duì)希望獲取Connection執(zhí)行SQL查詢的程序造成太大影響,因?yàn)镴dbcUtils.close()方法中捕獲了這個(gè)異常,打印log,并沒有上拋

public static void close(Connection x) {
    if (x == null) {
        return;
    }
    try {
        x.close();
    } catch (Exception e) {
        LOG.debug("close connection error", e);
    }
}

那么java.net.SocketException: Broken pipe是什么意思呢?
其實(shí)就是與數(shù)據(jù)庫(kù)建立的tcp連接因?yàn)槟承┰驍嚅_了,而導(dǎo)致了“管道破裂”。一般數(shù)據(jù)庫(kù)連接池會(huì)與數(shù)據(jù)庫(kù)保持長(zhǎng)連接,在需要的時(shí)候省去建立連接的過(guò)程,直接使用,而為什么這些空閑的連接會(huì)被斷開呢?被誰(shuí)斷開了?

2、為什么數(shù)據(jù)庫(kù)TCP連接會(huì)被斷開?

一開始百思不得其解,想著是因?yàn)镺racle數(shù)據(jù)庫(kù)主動(dòng)斷開了連接嗎?因?yàn)槟承┰?,比如?a title="服務(wù)器" target="_blank" href="http://www.kemok4.com/">服務(wù)器到數(shù)據(jù)庫(kù)的連接太多?明顯不是,這個(gè)項(xiàng)目還在試運(yùn)行階段,用的人不多,且觀察Druid的連接池監(jiān)控,一般建立的連接也就幾個(gè)
后來(lái)和同事討論的過(guò)程中得知?jiǎng)e的項(xiàng)目組也發(fā)生過(guò)類似的情況,而他們和這個(gè)項(xiàng)目的共同之處就在于服務(wù)都是在DMZ區(qū),外網(wǎng)可訪問(wèn),而數(shù)據(jù)庫(kù)在內(nèi)網(wǎng),需要通過(guò)防火墻才能訪問(wèn)到數(shù)據(jù)庫(kù)。于是去找負(fù)責(zé)維護(hù)網(wǎng)絡(luò)、防火墻的同事了解,原來(lái)防火墻有一個(gè)TCP超時(shí)時(shí)間,目前設(shè)置的為半小時(shí),其意義是,對(duì)于通過(guò)防火墻的所有TCP連接,如果在半小時(shí)內(nèi)沒有任何活動(dòng),就會(huì)被防火墻拆除,這樣就會(huì)導(dǎo)致連接中斷。在拆除連接時(shí),也不會(huì)向連接的兩端發(fā)送任何數(shù)據(jù)來(lái)通知連接已經(jīng)拆除。
這下數(shù)據(jù)庫(kù)連接斷開的原因找到了,那么這就是一個(gè)應(yīng)用與數(shù)據(jù)庫(kù)在不同的網(wǎng)絡(luò)中,連接需要經(jīng)過(guò)防火墻的場(chǎng)景中會(huì)遇到的一個(gè)典型問(wèn)題,怎么能夠使應(yīng)用和數(shù)據(jù)庫(kù)之間即使比較空閑也能夠保持一定數(shù)量的長(zhǎng)連接,是亟待解決的。

3、防火墻切斷數(shù)據(jù)庫(kù)連接會(huì)造成的影響

數(shù)據(jù)庫(kù)會(huì)話正在執(zhí)行耗時(shí)長(zhǎng)的SQL
切斷連接之前,連接對(duì)應(yīng)的Oracle會(huì)話正在執(zhí)行一個(gè)耗時(shí)特別長(zhǎng)的SQL,比如存儲(chǔ)過(guò)程而在此過(guò)程中沒有任何數(shù)據(jù)輸出到客戶端,這樣當(dāng)SQL執(zhí)行完成之后,向客戶端返回結(jié)果時(shí),如果TCP連接已經(jīng)被防火墻中斷,這時(shí)候顯然會(huì)出現(xiàn)錯(cuò)誤,連接中斷,那么會(huì)話也就會(huì)中斷。但是客戶端還不知道,會(huì)一直處于等待服務(wù)器返回結(jié)果的狀態(tài)。
如果客戶端沒有針對(duì)這種執(zhí)行耗時(shí)長(zhǎng)的SQL的連接回收機(jī)制,那么客戶端這個(gè)連接將一直處于等待狀態(tài),如果客戶端不斷執(zhí)行這種耗時(shí)長(zhǎng)SQL,那么客戶端堆積的等待連接將越來(lái)越多。
Druid連接池的removeAbandoned相關(guān)配置以及邏輯,就是為了解決這種連接回收設(shè)置的。

數(shù)據(jù)庫(kù)會(huì)話空閑
切斷連接之前,Oracle會(huì)話一直處于空閑狀態(tài),在防火墻中斷之后,客戶端向Oracle服務(wù)器提交SQL時(shí),由于TCP連接已經(jīng)中斷,這時(shí)客戶端偵測(cè)到連接中斷,那么客戶端就會(huì)報(bào)ORA-03113/ORA-03114這類錯(cuò)誤,然后會(huì)話中斷。但是在Oracle服務(wù)器端,會(huì)話一直在處于等待客戶端消息的狀態(tài)。
而對(duì)于Druid這種有testOnBorrow、testWhileIdle的檢測(cè)機(jī)制,且檢測(cè)失敗可以重新建立連接的連接池,空閑的被防火墻切斷的連接在后續(xù)會(huì)被不斷重建,而在數(shù)據(jù)庫(kù)服務(wù)器端,則連接越來(lái)越多,即會(huì)話數(shù)越來(lái)越多,甚至最終超過(guò)了數(shù)據(jù)為最大連接數(shù)。

解決方法

1、調(diào)大防火墻的連接切斷時(shí)長(zhǎng)

這是一個(gè)臨時(shí)解決方法,比如將防火墻的連接超時(shí)時(shí)間調(diào)整為8小時(shí),這樣可以盡量避免空閑連接的切斷,但無(wú)法完全避免,因?yàn)闊o(wú)法預(yù)計(jì)連接會(huì)被空閑多久,如果你的系統(tǒng)不是總有人訪問(wèn)的話,那么連接遲早會(huì)因?yàn)榭臻e而被切斷,導(dǎo)致一些不可預(yù)計(jì)的問(wèn)題,而調(diào)大超時(shí)時(shí)間只是緩解而已

2、tcp keepalive功能

tcp的keepalive,其實(shí)就是用來(lái)保持tcp連接的,其原理簡(jiǎn)單說(shuō)就是如果一個(gè)TCP連接在指定的時(shí)間內(nèi)沒有任何活動(dòng),會(huì)發(fā)送一個(gè)探測(cè)包到連接的對(duì)端,檢測(cè)連接的對(duì)端是否仍然存在,如果對(duì)端一定時(shí)間內(nèi)仍沒有對(duì)探測(cè)的響應(yīng),會(huì)再次發(fā)送探測(cè)包,發(fā)送幾次后,仍然沒有響應(yīng),就認(rèn)為連接已經(jīng)失效,關(guān)閉本地連接。
tcp keepalive并不是默認(rèn)開啟的,在開發(fā)程序時(shí)可以設(shè)置tcp keepalive為true,這樣tcp連接在一定時(shí)間內(nèi)沒有任何數(shù)據(jù)報(bào)文傳輸則啟動(dòng)探測(cè),這個(gè)時(shí)間一般是操作系統(tǒng)規(guī)定,Linux系統(tǒng)中可以通過(guò)設(shè)置net.ipv4.tcp_keepalive_time來(lái)修改,默認(rèn)是7200秒,即2小時(shí)。當(dāng)然在編程時(shí)也可以設(shè)置這個(gè)時(shí)間用于當(dāng)前socket,但是Java的Socket API中好像只有設(shè)置keepalive=true,并沒法設(shè)置tcp_keepalive_time
當(dāng)設(shè)置了tcp keepalive之后,只要tcp探測(cè)包發(fā)送的時(shí)間小于防火墻的連接超時(shí)時(shí)間,防火墻就會(huì)檢查到連接中仍然有數(shù)據(jù)傳輸,就不會(huì)斷開這個(gè)連接。

使用JDBC創(chuàng)建的數(shù)據(jù)庫(kù)tcp連接是沒有設(shè)置keepalive的,這點(diǎn)可以通過(guò)Linux的netstat或ss命令在數(shù)據(jù)庫(kù)客戶端(即應(yīng)用端)驗(yàn)證
使用命令netstat -anoss -ano,其中參數(shù)o都是顯示timer計(jì)時(shí)器,timer計(jì)時(shí)器在連接建立狀態(tài)下可以對(duì)連接?;钣?jì)時(shí)
netstat命令對(duì)沒有開啟keepalive的tcp連接顯示為:off (0.00/0/0)
ss命令對(duì)沒有keepalive的tcp連接,不會(huì)顯示timer計(jì)時(shí)器

3、Oracle數(shù)據(jù)庫(kù)的DCD

Oracle提供了類似tcp keepalive的機(jī)制,也就是DCD(Dead Conneciton Detection)。在$ORACLE_HOME/network/admin/sqlnet.ora文件中增加如下一行:

sqlnet.expire_time=NNN

這里NNN為分鐘數(shù),Oracle數(shù)據(jù)庫(kù)會(huì)在會(huì)話IDLE時(shí)間超過(guò)這個(gè)指定的時(shí)間時(shí),檢測(cè)這個(gè)會(huì)話的對(duì)端(即客戶端)是否還有效。避免客戶端由于異常退出,導(dǎo)致會(huì)話一直存在。
同樣的如果DCD的時(shí)間比防火墻切斷空閑連接的時(shí)間短,連接也可以一直保持

4、程序不定時(shí)執(zhí)行查詢

以上幾種方法要么是利用tcp連接keepalive特性,要么是采用數(shù)據(jù)庫(kù)端的空閑連接檢測(cè),我們的程序中也可以主動(dòng)做這種心跳檢測(cè)

Druid數(shù)據(jù)庫(kù)連接池從1.0.28開始,添加了druid.keepAlive屬性,默認(rèn)關(guān)閉
打開druid.keepAlive之后,當(dāng)連接池空閑時(shí),池中的minIdle數(shù)量以內(nèi)的連接,空閑時(shí)間超過(guò)minEvictableIdleTimeMillis,則會(huì)執(zhí)行keepAlive操作,即執(zhí)行druid.validationQuery指定的查詢SQL,一般為select * from dual,只要minEvictableIdleTimeMillis設(shè)置的小于防火墻切斷連接時(shí)間,就可以保證當(dāng)連接空閑時(shí)自動(dòng)做?;顧z測(cè),不會(huì)被防火墻切斷

到此,關(guān)于“如何解決Druid-由防火墻導(dǎo)致的數(shù)據(jù)庫(kù)空閑連接斷開問(wèn)題”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI