一般动作
登录
Wiki: BROPEN ▼: Document Index
»
Space: FAQ ▼: Document Index
»
Page: 如何调试数据库连接池的问题
搜索
zh
页面动作
导出 ▼: 以PDF格式导出 以RTF格式导出 以HTML格式导出
更多动作 ▼: 打印预览 察看源码
欢迎进入Wiki » FAQ » 如何调试数据库连接池的问题?

# 如何调试数据库连接池的问题?

在2013-12-23 23:35上被李小翔修改
评论 (0) · 附件 (0) · 记录 · 信息

服务器并发比较大的时候，可能出现数据库连接不够的问题，抛出类似 “Timeout waiting for idle object” 的异常，BroToolkit 提供了一个自定义的 Hibernate 配置类（bropen.toolkit.orm.HibernateConfiguration），除了能用来处理 Oracle 等排序问题外，还可以用来对数据库连接的获取与释放进行跟踪，以便定位是否有长时间未关闭连接的代码。

在此之前，先看看 Web 请求中的数据连接问题。

Grails 采取了经典的 OpenSessionInView 方式来处理数据库连接。通过浏览器访问控制器时，会自动生成一个 Hibernate 的 Session，如果使用了数据库相关的操作，则会自动获取数据库连接，请求结束时自动释放数据库连接。

这种模式下，如果每个请求都能比较快速的执行完毕，通常是不会发生数据库连接溢出的问题；但如果控制器执行时间比较长，并且并发请求比较多的时候，就容易导致连接不够用。比如下面两种情况：

- 控制器中执行大量的运算，导致执行时间过长
- 控制器中执行了请求外部资源的一些操作（比如调用 HttpUtils 或者 WebService 来请求第三方系统的资源），并且这些操作都设置了较长的超时时间（比如HttpUtils里默认30"），如果第三方系统响应太慢或者宕机。

这种情况导致的连接溢出，一般来说主要是优化代码，比如：

- 避免大量运算的并发操作
- 请求外部资源时设置较小的超时时间
- 使用 domain.withNewSession 将数据库相关的操作与请求外部资源的代码段隔离开

当不是Web请求时，比如 Quartz Job、runAsync 等方法，都会采用类似的方式类保证执行前后自动获取和释放连接，因此可以用相同的优化方式处理。

但是，如果程序中使用 DatabaseUtils.getConnection() 方法类获取的数据库连接，则必须调用 DatabaseUtils.closeConnection 来手动释放连接，否则多次调用后，就很容易导致连接池不够用了。

回过头来，如何定位这些占用连接时间过长的程序呢？

1. 确保手动获得数据库连接是使用了API：
   DatabaseUtils.getConnection()
2. 确保 DataSource 中，对应的数据源配置了：
   configClass = bropen.toolkit.orm.HibernateConfiguration
3. 在 Config 里添加日志选项：
   trace 'bropen.toolkit.orm.DatasourceConnectionProvider'
   // 默认已开启
   debug grails.app.filters.BroFrameworkFilters
   如果在运行时，可以在console中执行下面的代码动态开启：
   org.apache.log4j.Logger.getLogger("bropen.toolkit.orm.DatasourceConnectionProvider").level = org.apache.log4j.Level.TRACE;

上面的准备工作完成后（主要是2、3），应用的日志中会输出大量的 TRACE 日志，如：

19:22:29,431 [admin/Foobar@127.0.0.1] [http-bio-8080-exec-6] [TRACE bropen.toolkit.orm.DatasourceConnectionProvider] - getting DEFAULT connection: Thread[http-bio-8080-exec-6,5,main], numActive = 0/8
19:22:29,501 [admin/Foobar@127.0.0.1] [http-bio-8080-exec-6] [TRACE bropen.toolkit.orm.DatasourceConnectionProvider] - close connection: Thread[http-bio-8080-exec-6,5,main]-1493589126, numActive = 1/8

每条日志表明获得或者释放一个数据库连接（有可能不是真实的连接，实际要执行数据库操作的时候，才会获得真正的数据库连接），日志后面的 numActive 就表明了输出日志时，到底有多少个活动（没有释放）的连接，以及设置的最大多少个连接（DataSource 里的 maxActive 参数）。

对控制器的每个http请求，大多数都会有一对获取和关闭连接的日志，如果发现某个请求有大量的获取和关闭数据库链接的日志，也属于不正常的情况，需检查相应的代码并优化。比如 service 类的 transactional 为 true，一个常用的 taglib 里调用了这个 service，则每次调用时都会重新获得一个数据库链接，此时需要把 servie 的 transactional 设为 false，或者也可以将对应的方法改成 static 方法，并使用静态方式调用。

此外，通过下面的代码也可以获得当前的活动连接数：

BeanUtils.getBean("dataSourceUnproxied").getNumActive()

如果服务器请求数特别少的时候，这里的活动连接数还总是大于1，那多半就可能有连接未释放的问题。

如果 DataSource 中没有配置连接池不够时自增长（dbcp.whenExhaustedAction = 2），则当服务器抛出 Timeout waiting for idle object 异常时，日志中会自动打出一大段 TRACE 日志，这些日志里包含了所有未被释放的、获得数据库连接时的堆栈信息。

或者，可以在 console 里运行下面的代码手动输出同样的日志信息，以供随时检查：

ctx.getBean("sessionFactory")?.getConnectionProvider().logTraceConnections()

如果配置了多数据源，上面代码里的 sessionFactory 可以加上其他数据源的名称作为后缀，以便打印对应的数据源的信息。

示例堆栈信息如下：

19:39:09,419 [admin/Foobar@127.0.0.1] [http-bio-8080-exec-10] [TRACE bropen.toolkit.orm.DatasourceConnectionProvider] - maxActive = 8, numActive = 0, numTrace = 2
Thread[http-bio-8080-exec-9,5,main]-1189288877: 2013-12-23 19:39:06 - 3344ms:
0: java.lang.Thread.getStackTrace(Thread.java:1479)
1: bropen.toolkit.orm.DatasourceConnectionProvider.traceConnectionGet(DatasourceConnectionProvider.java:180)
2: bropen.toolkit.orm.DatasourceConnectionProvider.getConnection(DatasourceConnectionProvider.java:85)
3: org.hibernate.connection.ConnectionProvider$getConnection.call(Unknown Source)
4: org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:45)
5: org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:108)
6: org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:112)
7: bropen.toolkit.utils.grails.DatabaseUtils.getConnection(DatabaseUtils.groovy:195)
8: bropen.toolkit.utils.grails.DatabaseUtils.getConnection(DatabaseUtils.groovy)
9: bropen.toolkit.utils.grails.DatabaseUtils$getConnection.call(Unknown Source)
10: org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:45)
11: org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:108)
12: org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:112)
13: Script1.run(Script1.groovy:3)
14: groovy.lang.GroovyShell.evaluate(GroovyShell.java:518)
15: groovy.lang.GroovyShell.evaluate(GroovyShell.java:556)
16: groovy.lang.GroovyShell.evaluate(GroovyShell.java:527)
17: groovy.lang.GroovyShell$evaluate.call(Unknown Source)
18: org.grails.plugins.console.ConsoleService.eval(ConsoleService.groovy:57)
.......

从上面的堆栈信息中，就可以很容易看出曾经是哪个线程（http-bio-8080-exec-9）、通过怎样的调用过程（堆栈）拿到了一个数据库连接，并且从 19:39:06 到现在，已执行了 3344ms 却一直没有释放。

如果堆栈信息提供的信息还不够完整，还可以从日志里去查找对应时间点的线程执行情况，如下面的日志说明 HTTP 线程 http-bio-8080-exec-3 已执行完成，总耗时29ms：
.... [http-bio-8080-exec-3] [DEBUG grails.app.filters.BroFrameworkFilters] - 4, 29 + 0 = 29ms for GET portal/index
如果发现线程执行完成，但是数据库连接未释放，则可能是有手动获得的数据库连接未关闭，需要在 finally 里执行关闭API：DatabaseUtils.closeConnection()

不过，如果是通过 DatabaseUtils.getSql 打开的数据库连接，关闭后可能仍然会出现在上面的堆栈日志中，好在相关的代码都比较好查，使用完毕后，在 finally 里将 sql 对象 close 即可。

最后，如果定位到了问题所在，该手动关闭的数据库连接也调用 API 正常关闭了，之前所提到的几种代码优化也都做了，仍然时不时出现 “Timeout waiting for idle object” 异常，可以在 DataSouce 中的 hibernate.properties[_xxxx] 中设置 dbcp.whenExhaustedAction = 2，即连接池满后，继续创建新连接。

标签： grails hibernate BroToolkit
在2013-12-23 22:08上被李小翔创建

- 评论 （0）
- 附件 （0）
- 记录
- 信息

# 快速链接

- 首页
- 新闻动态
- FAQ

Copyright © 2013 北京博瑞开源软件有限公司
京ICP备12048974号