当Tomcat启用Context reload="true"时,JDBC连接池的连接会耗尽

30

我正在Eclipse Juno中开发一个Java EE Web应用程序。我已经配置了Tomcat来使用JDBC连接池(org.apache.tomcat.jdbc.pool)以及PostgreSQL数据库。 以下是我项目的META-INF/context.xml文件中的配置:

<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <!-- Configuration for the Tomcat JDBC Connection Pool -->
    <Resource name="jdbc/someDB"
        type="javax.sql.DataSource"
        auth="Container"
        factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
        driverClassName="org.postgresql.Driver"
        url="jdbc:postgresql://localhost:5432/somedb"
        username="postgres"
        password="12345"
        maxActive="100"
        minIdle="10"
        initialSize="10"
        validationQuery="SELECT 1"
        validationInterval="30000"
        removeAbandoned="true"
        removeAbandonedTimeout="60"
        abandonWhenPercentageFull="50" />
</Context>

我使用Eclipse将我的应用程序部署到Tomcat,并在Tomcat的context.xml中设置了属性reloadable为"true",以便在检测到更改时自动重新加载Web应用程序:

<Context reloadable="true">

我注意到每次发生上述自动重新加载时,PostgreSQL数据库会多预留10个连接(因为在webapp的context.xml中initialSize="10")。 因此,在发生10次更改后,会抛出PSQLException异常:

org.postgresql.util.PSQLException: FATAL: sorry, too many clients already
...

如果我手动重启Tomcat - 一切都很好,只有10个连接被保留。

有人知道解决此问题的方法吗?这样就可以将reloadable设置为“true”进行开发,而不会在每次重新加载上下文时池化更多的连接。

非常感谢任何帮助。

P.S. Apache Tomcat版本7.0.32


1
很可能是 https://dev59.com/9F7Va4cB1Zd3GeqPGhgs 的重复内容。 - Isaac
1
“@Isaac”:“这个问题已经在Tomcat 7.0.11中得到了修正”,但我的版本是7.0.32,仍然出现相同的结果。所以基本上算是一个bug? - informatik01
可能存在回归问题。如果您绝对确定您正在释放所有连接,但问题仍然存在,那么我建议重新打开错误报告。 - Isaac
@Isaac 是的,在 finally 块中关闭所有的 ResultSets、Statements/PreparedStatements 和 Connections。无论如何,感谢您的帮助。 - informatik01
context.xml 中有一个错别字:validatonQuery="SELECT 1",validation 缺少了一个 I。在复制/粘贴过程中注意到了这个问题,但没有机会编辑问题。 - Clerenz
@Clerenz 谢谢,已修复。 - informatik01
1个回答

37

解决方案(简短版)

为了解决这个问题,在context.xml文件中的Resource元素中添加一个名为closeMethod的属性(详见这里),并将其值设置为"close"。

以下是我/META-INF/context.xml文件的正确内容:

<Context>
    <!-- Configuration for the Tomcat JDBC Connection Pool -->
    <Resource name="jdbc/someDB"
        type="javax.sql.DataSource"
        auth="Container"
        factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
        driverClassName="org.postgresql.Driver"
        url="jdbc:postgresql://localhost:5432/somedb"
        username="postgres"
        password="12345"
        maxActive="100"
        minIdle="10"
        initialSize="10"
        validationQuery="SELECT 1"
        validationInterval="30000"
        removeAbandoned="true"
        removeAbandonedTimeout="60"
        abandonWhenPercentageFull="50"
        closeMethod="close" />
</Context>

注意属性closeMethod。我测试过了,现在连接的数量严格按照context.xml文件中定义的来保持! 注意
有一个与JNDI相关的问题需要注意。请参阅更新3以获取完整描述。

长篇回答

好的,我找到了上述解决方案,感谢Apache Tomcat贡献者Konstantin Kolinko。我在ASF Bugzilla上报告了这个问题作为Apache Tomcat的一个bug,但事实证明它并不是一个bug(参见更新1)。

更新1(2012年12月3日)也称为“新的希望”

嗯,事实证明它仍然是一个bug。Apache Tomcat 7的发布经理Mark Thomas确认了这一点(引用):

这是jdbc-pool中的一个内存泄漏bug。PoolCleaner实例保留了对ConnectionPool的引用,阻止其被垃圾回收。
...
这个问题已经在trunk和7.0.x中修复,并将在7.0.34及以后的版本中包含。

所以,如果您使用的是旧版本的Tomcat(低于7.0.34),请使用上述解决方案。否则,从Apache Tomcat 7.0.34版本开始,不应该再出现我描述的问题。(请参见更新2)

更新2(2014-01-13)也称为“问题的回归”

似乎在我的错误报告中最初描述的问题,即使在目前最新的Apache Tomcat版本7.0.50中仍然存在,我还使用Tomcat 7.0.47进行了复现(感谢Miklos Krivan的指出)。尽管现在Tomcat有时能够在重新加载后关闭额外的连接,有时在重新加载后连接数会增加,然后保持稳定,但最终这种行为仍然不可靠。
我仍然可以重现最初描述的问题(虽然不那么容易:可能与连续重新加载的频率有关)。看起来只是时间的问题,即如果Tomcat在重新加载后有足够的时间,它会更或多或少地管理连接池。正如Mark Thomas在他的评论中提到的那样:“根据closeMethod的文档,该方法的存在仅仅是为了加快释放资源的速度,否则这些资源将由GC释放。”(引用结束),看起来速度是决定性因素。
当使用Konstantin Kolinko提出的解决方案(使用closeMethod="close")时,一切都正常工作,并且保留的连接数严格按照context.xml文件中定义的方式进行。因此,目前看来使用closeMethod="close"是唯一真正的方法,可以避免在上下文重新加载后耗尽连接。
更新3(2014-01-13)即“Tomcat发布经理的回归”
UPDATE 2中描述的行为背后的谜团已经解开。在我收到Tomcat发布经理Mark Thomas的回复后,现在更多的细节已经得到澄清。希望这是最后一次更新。所以,正如UPDATE 1中提到的那样,这个bug确实已经被修复了。我在这里引用Mark回复中的重要部分(重点是我的):
在调查这个错误时发现的实际内存泄漏已在7.0.34版本及以后进行了修复,参见评论#4至#6。
连接在重新加载时未关闭的问题是由于J2EE规范对JNDI资源的规定,因此这部分错误报告是无效的。我将此错误的状态恢复为已修复,以反映已修复了实际存在的内存泄漏。
为了进一步解释为什么在重新加载后立即关闭连接的失败是无效的,J2EE规范没有提供容器告知资源不再需要的机制。因此,容器所能做的只是清除对资源的引用,并等待垃圾回收(这将触发池和相关连接的关闭)。垃圾回收由JVM决定的时间来进行,所以连接在上下文重新加载后关闭需要一个不确定的时间,因为垃圾回收可能需要一段时间才会发生。
Tomcat添加了Tomcat特定的JNDI属性closeMethod,可以在上下文停止时触发对JNDI资源的显式关闭。如果等待垃圾回收清理资源是不可接受的,那么可以简单地使用这个参数。Tomcat默认情况下不使用这个参数,因为它可能对某些JNDI资源产生意外和不需要的副作用。
如果您希望看到为告知JNDI资源不再需要提供标准机制,那么您需要向J2EE专家组进行游说。

结论

只需使用本文开头提出的解决方案(但是,以防万一,请记住可能会出现与JNDI相关的问题,这在理论上是可能的)。


替代方案

Michael Osipov建议使用他的CloseableResourceListener,它可以防止在Web应用程序卸载过程中由未关闭的资源引起的内存泄漏问题。所以你也可以试试看。


免责声明 UPDATES的别名是受到星球大战电影系列的启发。所有权利归其各自所有者所有。

1
不幸的是,Tomcat 7.0.35也有同样的问题,但closeMethod="close"值解决了我的问题。 - Miklos Krivan
1
Tomcat 7.0.47也有同样的问题,但closeMethod="close"可以完美解决。 - Miklos Krivan
1
什么是“一些JNDI资源的意外和不必要的副作用”,它适用于postgres连接吗? - user3427419
2
简短的回答是“取决于情况”。closeMethod的默认值为“close”。调用JndiResource.close()(如果存在此方法)时,资源不再需要时将产生预期的效果,但由于“close()”不属于任何标准API,因此我们无法知道其行为将会如何。因此,调用此方法的结果可能是“意外和不必要的”。关于postgres,如果您使用Tomcat内置的连接池,则“close()”将在连接池上调用,这几乎肯定是您想要的。 - Mark Thomas
1
@gbenroscience,你的数据源是如何配置的?它是通过编程方式由Tomcat管理的吗?还是被任何IoC容器管理?请告诉我具体细节和位置。 - informatik01
显示剩余14条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接