您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何解決Druid-由防火墻導致的數據庫空閑連接斷開問題”,在日常操作中,相信很多人在如何解決Druid-由防火墻導致的數據庫空閑連接斷開問題問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何解決Druid-由防火墻導致的數據庫空閑連接斷開問題”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
公司一個新項目上線,處于試運行階段,這個項目雖然是外網可訪問的,故部署在了DMZ區,但試運行階段只給了公司內少部分員工地址和賬號(其中包括一些領導),故訪問量很小,但項目還是挺重要的。
試運行階段中,項目應用日志中不定期會報異常,尤其是在剛上午剛開始使用時,還有空閑一段時間后再次使用時,具體異常如下:
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
從異常信息可以看出,問題是發生在Druid數據庫連接池在關閉物理數據庫連接時,報了 SocketException: Broken pipe,但為什么在使用時Druid會關閉數據庫連接,關閉數據連接又為什么會報SocketException呢?這個異常到底對系統有多大的影響呢?下面一步步分析。
項目中使用是的Druid連接數據庫,可為什么在系統空閑一段時間后再使用,會嘗試關閉數據庫連接,而且關閉的時候還拋了 java.net.SocketException: Broken pipe 呢?
從異常堆棧信息,或者翻看Druid源碼可以知道,異常是發生在從數據庫連接池中獲取連接,用于后續數據庫操作時,在執行到DruidDataSource.getConnectionDirect(maxWaitMillis)方法
時,有如下邏輯:
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException { //循環 for (;;) { //maxWaitMillis時間內從連接池獲取一個連接 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(); //如果連接已經關閉,再從池中獲取一個 if (realConnection.isClosed()) { discardConnection(null); // 傳入null,避免重復關閉 continue; } //testWhileIdle為true,即空閑后需要檢查連接 if (isTestWhileIdle()) { //連接空閑時間(當前時間 - 上次ActiveTime) long idleMillis = System.currentTimeMillis() - poolalbeConnection.getConnectionHolder().getLastActiveTimeMillis(); //連接空閑時間 > timeBetweenEvictionRunsMillis,檢查連接 if (idleMillis >= this.getTimeBetweenEvictionRunsMillis()) { boolean validate = testConnectionInternal(poolalbeConnection.getConnection()); //連接檢查失敗,打印log,丟棄連接,再獲取一個連接 if (!validate) { if (LOG.isDebugEnabled()) { LOG.debug("skip not validate connection."); } discardConnection(realConnection); continue; } } } } //如果開啟了連接超時回收 if (isRemoveAbandoned()) { StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); poolalbeConnection.setConnectStackTrace(stackTrace); poolalbeConnection.setConnectedTimeNano(); //設置當前時間為ConnectedTime poolalbeConnection.setTraceEnable(true); synchronized (activeConnections) { activeConnections.put(poolalbeConnection, PRESENT); //將連接放入activeConnections Map } } if (!this.isDefaultAutoCommit()) { poolalbeConnection.setAutoCommit(false); } return poolalbeConnection; } }
簡單來說,在從Druid獲取數據庫連接時,可以進行test,這段代碼中包含testOnBorrow
(借出時檢查)和testWhileIdle
(空閑時檢查)的邏輯,此項目在配置文件中
testOnBorrow = false
testWhileIdle = true
timeBetweenEvictionRunsMillis = 60000(60s)
故只會在連接空閑60s后再次使用時進行檢測,其實就是執行一個SQL,而在執行SQL時如果失敗了,就會調用JdbcUtils.close(realConnection)
關閉連接,在關閉這個連接時拋了SocketException異常,但其實這個異常倒不會對希望獲取Connection執行SQL查詢的程序造成太大影響,因為JdbcUtils.close()方法中捕獲了這個異常,打印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是什么意思呢?
其實就是與數據庫建立的tcp連接因為某些原因斷開了,而導致了“管道破裂”。一般數據庫連接池會與數據庫保持長連接,在需要的時候省去建立連接的過程,直接使用,而為什么這些空閑的連接會被斷開呢?被誰斷開了?
一開始百思不得其解,想著是因為Oracle數據庫主動斷開了連接嗎?因為某些原因,比如從服務器到數據庫的連接太多?明顯不是,這個項目還在試運行階段,用的人不多,且觀察Druid的連接池監控,一般建立的連接也就幾個
后來和同事討論的過程中得知別的項目組也發生過類似的情況,而他們和這個項目的共同之處就在于服務都是在DMZ區,外網可訪問,而數據庫在內網,需要通過防火墻才能訪問到數據庫。于是去找負責維護網絡、防火墻的同事了解,原來防火墻有一個TCP超時時間,目前設置的為半小時,其意義是,對于通過防火墻的所有TCP連接,如果在半小時內沒有任何活動,就會被防火墻拆除,這樣就會導致連接中斷。在拆除連接時,也不會向連接的兩端發送任何數據來通知連接已經拆除。
這下數據庫連接斷開的原因找到了,那么這就是一個應用與數據庫在不同的網絡中,連接需要經過防火墻的場景中會遇到的一個典型問題,怎么能夠使應用和數據庫之間即使比較空閑也能夠保持一定數量的長連接,是亟待解決的。
數據庫會話正在執行耗時長的SQL
切斷連接之前,連接對應的Oracle會話正在執行一個耗時特別長的SQL,比如存儲過程而在此過程中沒有任何數據輸出到客戶端,這樣當SQL執行完成之后,向客戶端返回結果時,如果TCP連接已經被防火墻中斷,這時候顯然會出現錯誤,連接中斷,那么會話也就會中斷。但是客戶端還不知道,會一直處于等待服務器返回結果的狀態。
如果客戶端沒有針對這種執行耗時長的SQL的連接回收機制,那么客戶端這個連接將一直處于等待狀態,如果客戶端不斷執行這種耗時長SQL,那么客戶端堆積的等待連接將越來越多。
Druid連接池的removeAbandoned
相關配置以及邏輯,就是為了解決這種連接回收設置的。
數據庫會話空閑
切斷連接之前,Oracle會話一直處于空閑狀態,在防火墻中斷之后,客戶端向Oracle服務器提交SQL時,由于TCP連接已經中斷,這時客戶端偵測到連接中斷,那么客戶端就會報ORA-03113/ORA-03114這類錯誤,然后會話中斷。但是在Oracle服務器端,會話一直在處于等待客戶端消息的狀態。
而對于Druid這種有testOnBorrow、testWhileIdle的檢測機制,且檢測失敗可以重新建立連接的連接池,空閑的被防火墻切斷的連接在后續會被不斷重建,而在數據庫服務器端,則連接越來越多,即會話數越來越多,甚至最終超過了數據為最大連接數。
這是一個臨時解決方法,比如將防火墻的連接超時時間調整為8小時,這樣可以盡量避免空閑連接的切斷,但無法完全避免,因為無法預計連接會被空閑多久,如果你的系統不是總有人訪問的話,那么連接遲早會因為空閑而被切斷,導致一些不可預計的問題,而調大超時時間只是緩解而已
tcp的keepalive,其實就是用來保持tcp連接的,其原理簡單說就是如果一個TCP連接在指定的時間內沒有任何活動,會發送一個探測包到連接的對端,檢測連接的對端是否仍然存在,如果對端一定時間內仍沒有對探測的響應,會再次發送探測包,發送幾次后,仍然沒有響應,就認為連接已經失效,關閉本地連接。
tcp keepalive并不是默認開啟的,在開發程序時可以設置tcp keepalive為true,這樣tcp連接在一定時間內沒有任何數據報文傳輸則啟動探測,這個時間一般是操作系統規定,Linux系統中可以通過設置net.ipv4.tcp_keepalive_time
來修改,默認是7200秒,即2小時。當然在編程時也可以設置這個時間用于當前socket,但是Java的Socket API中好像只有設置keepalive=true,并沒法設置tcp_keepalive_time
當設置了tcp keepalive之后,只要tcp探測包發送的時間小于防火墻的連接超時時間,防火墻就會檢查到連接中仍然有數據傳輸,就不會斷開這個連接。
使用JDBC創建的數據庫tcp連接是沒有設置keepalive的,這點可以通過Linux的netstat或ss命令在數據庫客戶端(即應用端)驗證
使用命令netstat -ano 或 ss -ano,其中參數o都是顯示timer計時器,timer計時器在連接建立狀態下可以對連接保活計時
netstat命令對沒有開啟keepalive的tcp連接顯示為:off (0.00/0/0)
ss命令對沒有keepalive的tcp連接,不會顯示timer計時器
Oracle提供了類似tcp keepalive的機制,也就是DCD(Dead Conneciton Detection)。在$ORACLE_HOME/network/admin/sqlnet.ora文件中增加如下一行:
sqlnet.expire_time=NNN
這里NNN為分鐘數,Oracle數據庫會在會話IDLE時間超過這個指定的時間時,檢測這個會話的對端(即客戶端)是否還有效。避免客戶端由于異常退出,導致會話一直存在。
同樣的如果DCD的時間比防火墻切斷空閑連接的時間短,連接也可以一直保持
以上幾種方法要么是利用tcp連接keepalive特性,要么是采用數據庫端的空閑連接檢測,我們的程序中也可以主動做這種心跳檢測
Druid數據庫連接池從1.0.28開始,添加了druid.keepAlive屬性,默認關閉
打開druid.keepAlive之后,當連接池空閑時,池中的minIdle數量以內的連接,空閑時間超過minEvictableIdleTimeMillis,則會執行keepAlive操作,即執行druid.validationQuery指定的查詢SQL,一般為select * from dual,只要minEvictableIdleTimeMillis設置的小于防火墻切斷連接時間,就可以保證當連接空閑時自動做保活檢測,不會被防火墻切斷
到此,關于“如何解決Druid-由防火墻導致的數據庫空閑連接斷開問題”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。