使用Java应用程序检测和解决数据库连接泄漏


在处理客户的生产设置时,我们有机会分析他的代码和配置。客户面临的问题是,在高流量期间,应用程序变得无响应,并且不接受任何进一步的请求。唯一的解决方法是重新启动应用程序。该应用程序已部署在Red Hat Fuse 6.3.0上。

数据库团队分析说,在数据库上创建的连接太多,这也影响到数据库。经过代码和配置后,我们发现该应用程序代码未使用任何数据库连接池实现。

第一步是使用dbcp驱动程序配置数据库连接池。由于兼容性问题,我们将传统的Apache DBCP 1.4与commons-pool 1库一起使用。最初的配置如下。这些配置是非常标准的配置,对于大多数生产环境来说已经足够了。所有这些配置详细信息都提到了commons-dbcp doc

<bean id="XXX" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
      <property name="driverClassName" value="oracle.jdbc.OracleDriver"/>
      <property name="url" value="${oracle.url}"/>
      <property name="username" value="${oracle.user}"/>
      <property name="password" value="${oracle.password}"/>
      <property name="initialSize" value="10"/>
      <property name="minIdle" value="10"/>
      <property name="maxIdle" value="30"/>
      <property name="maxActive" value="50"/>
      <property name="testOnBorrow" value="true"/>
      <property name="testOnReturn" value="true"/>
      <property name="testWhileIdle" value="true"/>
      <property name="validationQuery" value="SELECT 1 FROM DUAL"/>
      <property name="connectionProperties" value="oracle.jdbc.ReadTimeout=10000"/>
          <property name="timeBetweenEvictionRunsMillis" value="-1"/>
      </bean>

在也配置了连接池之后,应用程序被卡住,并且无法响应进一步的传入请求。但是,数据库团队观察到的与数据库的连接减少了。一种奇怪的行为是,如果我们分析从应用程序到数据库的连接数,则计数减少为0。而在连接池中,minIdle和InitialSize设置为10。在应用程序卡住时,我们发现该应用程序与数据库的连接为0。

netstat -anp|grep [application_pid]|grep [database_port]

因此,他的应用程序中某处存在连接泄漏。连接泄漏意味着某些数据库请求/事务未正确关闭或未提交,最后这些连接被永久放弃并永久关闭。但是有许多应用程序(超过50个)与数据库进行交互,并且不清楚连接泄漏在哪里。同样,要遍历所有应用程序并检查代码和配置也将很困难,并且至少要花几周的时间,因为这也需要进行全面的测试。

为了缩小此问题并查找连接泄漏的可能原因,我们也可以设置以下dbcp配置。

<property name="logAbandoned" value="true"/>
<property name="removeAbandoned" value="true"/>
<property name="removeAbandonedTimeout" value="30"/>
<property name="timeBetweenEvictionRunsMillis" value="30000" />

在这里,将removeAbandoned设置为true时,尝试删除已放弃的连接,并在几秒钟内在配置的removeAbandonedTimeout中将它们重新返回到池中。将此设置为true可以从编写失败但无法关闭连接的应用程序中恢复数据库连接。该logAbandoned属性也因为它可以记录完整的堆栈跟踪,这可能是泄漏的连接非常重要,从而可以识别中连接应用程序泄露是非常有用的。Stack-Trace本身已登录到终端。在Red Hat Fuse中,我们可以看到由logAbandoned 记录的这些堆栈跟踪记录在karaf终端中,而不是在应用程序日志或fuse.log文件中。所有这些属性均已提及commons-dbcp doc

该timeBetweenEvictionRunsMillis 属性也可以是有益的,它以毫秒为单位设置。设置后,将运行一个单独的线程,以在每个配置的毫秒中删除空闲的对象退出者线程。它的默认值为-1,这意味着该空闲对象退出线程不会处于活动和运行状态,只有将其设置为正整数,它才有效。有时,介于两者之间或什至数据库之间的防火墙会永久性地关闭空闲连接,而使用timeBetweenEvictionRunsMillis 可以帮助这些连接返回到池中。

上面的配置removeAbandoned 和timeBetweenEvictionRunsMillis 只是一种避免应用程序无响应的解决方法,这些操作可能会延迟应用程序无响应。

为了进一步缩小问题的范围,在发行时,我们捕获了Red Hat Fuse的线程转储,该应用程序是使用jstack实用程序安装的。此jstack实用程序仅可用于JDK安装,不适用于基于JRE的安装。因此,如果没有可用的jstack实用程序,请尝试使用与基于JRE的Java安装相同版本的JDK下载。具有相同的版本很重要,否则可能会由于JDK的jstack实用程序和JRE中运行的Application JVM之间的兼容性冲突而导致主要/次要版本错误。可在JDK安装的bin文件夹中找到此jstack实用程序。

jstack -l [application_pid] > threaddump.txt

请注意,我们在20秒的间隔中捕获了3到4个线程转储样本,在10或20秒的间隔中具有不同的样本有助于识别长时间运行的线程。当我们在任何我们喜欢的IDE中打开threaddump.txt时,都发现了以下堆栈跟踪。

"qtp308220548-1262" #1262 prio=5 os_prio=0 tid=0x00007fc7bc192800 nid=0x70aa in Object.wait() [0x00007fc6f24d9000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at org.apache.commons.pool.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:1118)
    - locked <0x00000007382b9f30> (a org.apache.commons.pool.impl.GenericObjectPool$Latch)
    at org.apache.commons.dbcp.PoolingDataSource.getConnection(PoolingDataSource.java:106)
    at org.apache.commons.dbcp.BasicDataSource.getConnection(BasicDataSource.java:1044)
    at Proxy4fb1bf6f_84b1_4ec9_94ae_0661ba1b9af6.getConnection(Unknown Source)
    at org.apache.camel.component.jdbc.JdbcProducer.processingSqlBySettingAutoCommit(JdbcProducer.java:80)
    at org.apache.camel.component.jdbc.JdbcProducer.process(JdbcProducer.java:67)
    at org.apache.camel.util.AsyncProcessorConverterHelper$ProcessorToAsyncProcessorBridge.process(AsyncProcessorConverterHelper.java:61)
    at org.apache.camel.processor.SendProcessor.process(SendProcessor.java:145)
    at org.apache.camel.management.InstrumentationProcessor.process(InstrumentationProcessor.java:77)
    at org.apache.camel.processor.RedeliveryErrorHandler.process(RedeliveryErrorHandler.java:468)
    at org.apache.camel.processor.CamelInternalProcessor.process(CamelInternalProcessor.java:196)
    at org.apache.camel.processor.Pipeline.process(Pipeline.java:121)
    at org.apache.camel.processor.Pipeline.process(Pipeline.java:83)
    at org.apache.camel.processor.CamelInternalProcessor.process(CamelInternalProcessor.java:196)
    at org.apache.camel.component.cxf.jaxrs.CxfRsInvoker.asyncInvoke(CxfRsInvoker.java:93)
    - locked <0x000000074cf27b40> (a org.apache.cxf.transport.http.Servlet3ContinuationProvider$Servlet3Continuation)
    at org.apache.camel.component.cxf.jaxrs.CxfRsInvoker.performInvocation(CxfRsInvoker.java:68)
    at org.apache.cxf.service.invoker.AbstractInvoker.invoke(AbstractInvoker.java:96)
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:189)
    at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:99)

从org.apache.commons.pool.impl.GenericObjectPool.borrowObject 操作中,我们发现线程正在等待验证来自连接池的连接。

此外,如果我们经过stacktrace,我们会发现该线程源自camel-jdbc生产者,并且进一步源自CXF JAXRS组件。我们在线程转储中观察到这可能是一个有问题的应用程序。

最重要的是,这些配置有助于我们发现问题并解决问题。

最后,使用try-with-resources进行数据库连接也有助于自动关闭资源,但是dbcp 1.4版本与Java6更兼容。自动资源管理(try-with-resources)可以与dbcp 1.4一起使用,但是它需要进行更多的测试以确保没有其他中断,因此对于dbcp 1.4(这是一个传统库),我们避免使用它。

在finally子句中在启动时和关闭连接之后随处可见日志语句也肯定会有所帮助,因为通过负载测试,我们可以比较启动日志和关闭日志的数量,它们之间的任何差异都意味着连接泄漏且连接未关闭。


原文链接:http://codingdict.com