应用层数据库连接池连接数配置探讨

在开发过程中,应用层普遍使用连接池访问数据库,其核心思想是预先建立并维护一组数据库连接,当应用程序需要与数据库交互时,从池中复用一个已存在的连接,使用完毕后归还给连接池池,不需要每次请求都新建和关闭连接。

使用连接池最大的好处性能的提升。由于建立一次数据库连接是一个开销很大的操作,它包含了网络三次握手、数据库服务器的身份验证、建立会话上下文等步骤。这个过程在物理上可能耗时几十甚至几百毫秒。如果每个数据库请求都经历一次完整的连接创建和销毁,那么大量的时间会浪费在连接建立上,进而导致应用响应缓慢,吞吐量下降。因此通过复用连接池中已创建的连接,完全避免了这部分开销,使得应用程序的响应速度得到提升。另外,连接池的使用也增加了应用的健壮性,通过连接池自带的健康检查机制,可以剔除因网络闪断或超时导致的失效连接,防止使用数据库连接时即出现断链。

数据库连接池的使用技术有很多种,像C3P0、DBCP这样子的老牌连接池技术,因性能落后被逐渐弃用了。现在像Spring Boot 2.x及更高版本默认使用HikariCP作为连接池,也是现在Java生态中公认的高性能连接池,优势就是极简的设计理念,专注于连接池的核心功能,避免了不必要的复杂性。国内广泛使用的如阿里巴巴开源的Druid连接池,除基本连接池功能外,还提供了丰富的监控和扩展能力,它整合了C3P0、DBCP和Proxool的优点,支持SQL监控、防火墙、慢SQL日志和性能分析等功能。

1、Druid连接池介绍
1.1 Druid连接概览

Druid是开源的数据库连接池,它结合了C3P0、DBCP、Proxool等DB池的优点,同时加入了日志监控,可以很好的监控DB池连接和SQL的执行情况。

在这里插入图片描述

  • 在druidDataSource中有一个重入锁和衍生的两个condition:一个监控连接池是否为空,一个监控连接池不为空。
  • 在druidDataSource中有两个线程,一个生成连接CreateConnectionThread,一个回收连接DestoryConnectionThread。在创建、获取、回收的时候都会使用这些锁和condition。
  • 每次获取Connection都会调用init,内部使用inited标识DataSource是否已经初始化OK。
  • 每次获取Connection都会需要进行加锁保证线程安全,所有操作都在加锁后执行。
  • 如果连接池内没有连接了,则调用empty.signal(),通知CreateThread创建连接,并且等待指定的时间,被唤醒之后再去查看是否有可用连接。
1.2 连接创建和销毁

Druid连接池的核心是一个生产者-消费者模型,内部通过3个核心线程协作管理连接。

在这里插入图片描述

  • 创建连接线程(CreateConnectionThread):为生产者,负责创建物理连接。当连接池需要新连接时(例如,连接池为空或连接数未达上限),该线程会建立新的数据库连接,并通过notEmpty.signal()通知等待的消费者线程。
  • 销毁连接线程(DestroyConnectionThread)定期运行,主要通过 shrink方法回收和清理连接。它负责检查空闲时间过长的连接、无效连接,并在连接数过多时进行回收,必要时也会通过empty.signal()触发创建新连接。
  • 用户线程:作为消费者,通过getConnection()方法从连接池获取连接。如果池中无空闲连接,用户线程会在 notEmpty 条件上等待,直到生产者线程发出信号。
1.3 连接保活和回收机制
1.3.1 连接保活

为了防止一个数据库连接太久没有使用,而被其它下层的服务关闭,druid中定义了KeepAlive选项,机制上与TCP中的类似。保活机制能够保证连接池中的连接是真实有效的连接,假如遇到特殊情况导致连接不可用时,keepAlive机制将无效连接进行驱逐。保活机制是由守护线程DestroyConnectionThread发起的,启动后守护线程会进入无线循环,根据心跳间隔时间timeBetweenEvictionRunsMillis循环调用DestoryTask线程,默认时间为60s。

  • 当keepAlive开启时:对于空闲时间超过 minEvictableIdleTimeMillis 但小于 maxEvictableIdleTimeMillis 的连接,Druid会执行保活检查。
  • 当keepAlive关闭或不支持时:主要依赖以下参数在获取连接时进行有效性检测:
    • testOnBorrow:申请连接时检测,有效但性能开销较大。
    • testWhileIdle:推荐开启,在回收空闲连接时检测,对性能影响较小。当连接空闲时间大于timeBetweenEvictionRunsMillis,申请连接时会执行 validationQuery 检测。
  • 有效性检测:validationQuery (如 SELECT 1 FROM DUAL)用于检测连接有效性。

1)开启KeepAlive

// 一个连接在连接池中最小生存的时间
dataSurce.setMinEvictableIdleTimeMillis(60 * 1000);单位毫秒
// 开启keepAlive
dataSource.setKeepAlive(true);

有两个参数KeepAlive和MinEvictableIdleTimeMillis

2)DruidDataSource中的两个成员变量

// 存放检查需要抛弃的连接
private DruidConnectionHolder[] evictConnections;
// 用来存放需要连接检查的存活连接
private DruidConnectionHolder[] keepAliveConnections;

如果KeepAlive打开,当一个连接的空闲时间超过keepAliveBetweenTimeMillis时,则会将此连接放入此连接放入keepAliveConnections数组,然后使用validationQuery执行一次查询。

if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
                        keepAliveConnections[keepAliveCount++] = connection;
}if (keepAliveCount > 0) {
     // keep order
     for (int i = keepAliveCount - 1; i >= 0; --i) {
                DruidConnectionHolder holer = keepAliveConnections[i];
                Connection connection = holer.getConnection();
                holer.incrementKeepAliveCheckCount();
                boolean validate = false;
                try {
                    this.validateConnection(connection);
                    validate = true;
                } catch (Throwable error) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("keepAliveErr", error);
                    }
                    // skip
                }

如果本次validationQuery执行失败,则关闭该链接,并丢弃。

1.3.2 数据源收缩

在Druid数据源初始化的时候,会创建一个定时运行的DestroyTask,该任务的主要目的是将已空闲时间满足关闭条件的连接关闭。

1)当前连接存活时长 > 配置的物理连接时间时长,则放入evictConnections

if (phyConnectTimeMillis > phyTimeoutMillis) {
    evictConnections[evictCount++] = connection;
    continue;
}

2)空闲时间 > 最小驱逐时间

                    if (idleMillis >= minEvictableIdleTimeMillis) {
                        if (checkTime && i < checkCount) {
                            evictConnections[evictCount++] = connection;
                            continue;
                        } else if (idleMillis > maxEvictableIdleTimeMillis) {
                            evictConnections[evictCount++] = connection;
                            continue;
                        }
                    }if (evictCount > 0) {
            for (int i = 0; i < evictCount; ++i) {
                DruidConnectionHolder item = evictConnections[i];
                Connection connection = item.getConnection();
                JdbcUtils.close(connection);
                destroyCountUpdater.incrementAndGet(this);
            }
            Arrays.fill(evictConnections, null);
        }

从代码逻辑中可以看到,对于要关闭的空闲连接选择逻辑如下:

  • 对于空闲时间> minEvictableIdleTimeMillis的连接,仅会关闭poolingCount-minIdle个,后面的连接不受影响;
  • 处于> maxEvictableIdleTimeMillis的空闲连接则会直接关闭;
  • timeBetweenEvictionRunsMillis即为该定时任务运行的间隔;
  • minEvictableIdleTimeMillis为可关闭连接的最小空闲时间
1.4 连接数相关的关键配置

Druid连接池中和连接数相关的参数包括连接数数量控制的maxActive、minIdle和initialSize。

  • initialSize:初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时。缺省值为0
  • maxActive:最大连接池数量。缺省值为8
  • minIdle:最小连接池数量。缺省值为0
2、连接池连接数配置问题

连接池的使用有提升应用性能和数据库连接健壮性等优点,但是连接池中连接数设置不当,可能会出现数据库层连接数膨胀,超过数据库层配置的最大连接数,进而引发应用新建连接失败的问题。还存在一些场景下,数据库层已使用的连接数很多达到上千个,但实际活动的连接数非常少,只有几十。尤其是在微服务、数据库信创迁移、应用双活改造过程中,对于数据库层的连接数请求也会相应的调整,如何正确评估以及配置连接池的参数设置也带来了一定的难度。

对于数据库层而言,每个数据库连接请求会占用一定的内存空间,即使是空闲的连接,如果是活动的连接还有动态的内存占用如SQL执行结果缓存,而所能支撑的最大连接数受限于服务器的资源尤其是内存大小。因此应用在配置连接池的连接数时,如果配置不当是有可能超过数据库能支撑的最大连接数,集中在以下场景:

  • 同城双活部署:应用双活架构改造增加双活部署后,数据库层的连接数相应增加;
  • 应用服务器扩容:应用服务器或模块扩容后增加了对应的访问数据库的服务模块,数据库层的连接数相应增加;
  • 应用服务拆分:应用微服务化改造或服务拆分,数据库层的连接数相应增加;
  • 应用服务上云改造:上云改造并行期间,同时访问数据库,数据库层的连接数增加;
  • 数据库拆分:数据库拆分后资源相应调整,数据库层支撑的最大连接数发生变化;
  • 数据库信创改造:不同信创数据库访问方式发生变化,比如集中式和分布式,对应的最大连接数也会发生变化。

在双活部署架构下,不同的数据库的连接方式有所不同:如竖井式部署,在计算连接数配置时只需要计算单中心的应用模块连接数之和;还有一种是双中心的应用同时连接到生产站点的主节点,此时需要计算双中心的总和。总体的原则是要确保数据库层配置的最大连接数大于应用连接池中所需要的连接数(应用->数据库层是漏斗型)。

在这里插入图片描述

对于数据库层而言,最为保守也最为安全的配置是,∑(应用连接池的maxActive)<数据库层最大连接数。但实际上这样的配置很难落地执行,因为最大连接数是应用在极端场景下向上波动的空间,如果对于集中式数据库当连接的应用模块数很多时,单个模块连接的最大连接数上限就很小(比如GaussDB数据库在64G内存下最大连接数配置为2000,当有100个模块访问时,连接池配置的最大连接数只有20)。这种情况下,如果应用模块因为阻塞或交易峰值出现连接数突增时,非常容易超过上限报错。

如果希望减少应用层空闲的连接数,又能适应连接数突增的场景,建议按照以下配置进行设置:

  • 设置合理的minIdle,兼顾应用性能和平时活动连接数的使用情况,如∑(应用连接池的minIdle)<数据库层最大连接数*50%;如果无法满足,则考虑扩大数据库层资源以提高最大连接数配置,或者牺牲一部分应用性能,减少minIdle的值。
  • 适当的放大maxActive配置,满足连接数突增的场景,比如∑(应用连接池的maxActive)<数据库层最大连接数*5。

总体原则是确保应用层平时使用的所有数据库连接数在数据库层最大连接数的监控阈值80%以内,超过告警阈值则考虑扩容数据库服务器资源或者扩容节点以支撑更多的应用连接。需要说明的是,在数据库层看到的空闲连接,可能在连接池中是active状态的,因为应用连接后存在数据库层处理完但应用层还在处理,整个事务并没有结束,数据库连接也就没有释放掉。在实际实施过程中该如何配置以及规范,需要结合实际进行综合考虑了。


参考资料:

  1. Druid连接池负载不均问题分析
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值