公司一个新项目上线,处于试运行阶段,这个项目虽然是外网可访问的,故部署在了DMZ区,但试运行阶段只给了公司内少部分员工地址和帐号(其中包括一些领导),故访问量很小,但项目仍是挺重要的。
试运行阶段中,项目应用日志中不按期会报异常,尤为是在刚上午刚开始使用时,还有空闲一段时间后再次使用时,具体异常以下:html
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.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)
at xxx.xx.modules.deposit.api.service.DepositApiService$$EnhancerBySpringCGLIB$$59c8f6e2.doRecharge() java
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 morespring
从异常信息能够看出,问题是发生在Druid数据库链接池在关闭物理数据库链接时,报了 SocketException: Broken pipe,但为何在使用时Druid会关闭数据库链接,关闭数据链接又为何会报SocketException呢?这个异常到底对系统有多大的影响呢?下面一步步分析。sql
项目中使用是的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)api
故只会在链接空闲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链接,若是在半小时内没有任何活动,就会被防火墙拆除,这样就会致使链接中断。在拆除链接时,也不会向链接的两端发送任何数据来通知链接已经拆除。
这下数据库链接断开的缘由找到了,那么这就是一个应用与数据库在不一样的网络中,链接须要通过防火墙的场景中会遇到的一个典型问题,怎么可以使应用和数据库之间即便比较空闲也可以保持必定数量的长链接,是亟待解决的。oracle
数据库会话正在执行耗时长的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设置的小于防火墙切断链接时间,就能够保证当链接空闲时自动作保活检测,不会被防火墙切断