Doobie测试超出PostgreSQL最大连接数问题排查
问题根源
连接池重复初始化
测试类中用def定义transactor,导致每个测试方法执行时都会重新初始化一个全新的Hikari连接池。DatabaseConfig.transactor是Resource[IO, HikariTransactor[IO]]类型,每次调用allocated.unsafeRunSync()都会启动独立的连接池实例,每个池默认占用10个连接。多个测试类/方法叠加后,连接数快速超出数据库max_connections限制。资源未及时释放
即使尝试用afterAll,如果没有执行allocated返回的释放动作(即allocated.unsafeRunSync()._2),旧连接池的连接不会被回收,持续占用数据库连接。Doobie Check的连接累积
Doobie的check方法会通过transactor执行元数据查询验证SQL,若每个check对应独立的连接池,连接占用会进一步累积。
解决方案
1. 测试类复用单一连接池并确保资源释放
将测试类中的transactor改为lazy val,只初始化一次连接池,并在测试结束后执行释放逻辑:
class RegionDAOTest extends AsyncFlatSpec with AsyncIOSpec with IOChecker { // 初始化连接池,同时保存释放动作 private val (transactor, releaseTransactor) = DatabaseConfig.transactor.allocated.unsafeRunSync() val dao = new RegionDAO // 测试套件结束后释放连接池 override def afterAll(): Unit = { releaseTransactor.unsafeRunSync() super.afterAll() } "RegionDAO" should "generate a valid SQL for all regions query" in { IO(check(dao.allQuery)) } it should "generate a valid SQL for a single region query" in { IO(check(dao.regionQuery(1))) } it should "generate a valid SQL for insert query" in { val regionCreate = RegionCreate("New Region", 1) IO(check(dao.createQuery(regionCreate))) } it should "generate a valid SQL for update query" in { val country = CountrySimple(1, "Country") val region = Region(1, "Updated Region", country) IO(check(dao.updateQuery(region))) } it should "generate a valid SQL for delete query" in { IO(check(dao.deleteQuery(1))) } }
2. 配置测试专属的Hikari连接池参数
测试环境不需要大连接池,显式缩小maxPoolSize减少单池连接占用:
object DatabaseConfig { // 测试环境可通过环境变量设置更小的池大小 private val maxPoolSize = Properties.envOrElse("HIKARI_MAX_POOL_SIZE", "10").toInt lazy val transactor = (for { url <- Properties.envOrNone("POSTGRES_URL") user <- Properties.envOrNone("POSTGRES_USER") password <- Properties.envOrNone("POSTGRES_PASSWORD") } yield HikariTransactor .newHikariTransactor[IO]( "org.postgresql.Driver", url, user, password, ExecutionContext.global, Some(LogHandler.jdkLogHandler) ) .configure { hikari => hikari.setMaximumPoolSize(maxPoolSize) hikari.setMinimumIdle(1) // 测试环境保持最小空闲连接 hikari.setConnectionTimeout(3000) // 缩短连接超时 hikari } ).getOrElse(throw new IllegalStateException("Missing PostgreSQL environment variables")) }
在测试环境设置环境变量:export HIKARI_MAX_POOL_SIZE=2,将每个连接池的最大连接数限制为2。
3. 统一所有测试类的资源管理逻辑
确保所有使用IOChecker的测试类都遵循上述复用连接池+释放资源的逻辑,避免多个测试类各自创建独立连接池。
4. 实时监控连接数排查泄漏
执行PostgreSQL查询查看实时连接:
SELECT count(*) FROM pg_stat_activity WHERE datname = 'your_test_db';
结合测试执行进度,定位哪些测试/进程在占用连接,确认是否存在未释放的连接池。
验证
修改后重新运行测试,使用上述SQL查询连接数,确认连接数稳定在预期范围内(比如单测试套件占用2-5个连接,多套件累加后不超过数据库max_connections)。
内容的提问来源于stack exchange,提问作者Björn Pedersen

