DBCP 一个配置,浪费了 MySQL 50% 的性能!
1. 引言
研究背景
数据库性能的重要性
数据库性能优化对于保证应用的响应速度和处理大量数据的能力至关重要。它可以显著减少查询时间,提高事务处理效率,降低硬件成本,并确保系统稳定性与可扩展性。优化后的数据库能够更好地服务于用户需求,增强客户满意度,对企业的长期发展和竞争力具有深远影响。
连接池在数据库性能中的作用
1. 降低连接开销:连接池预先创建并管理一组数据库连接,避免了频繁建立和关闭连接的开销,提高了应用程序的响应速度。
2. 提高资源利用率:通过复用已存在的连接,连接池使得数据库资源(如内存和连接数)得到更高效的利用。
3. 管理连接生命周期: 连接池能够监控连接的健康状态,自动剔除失效的连接,并根据需要创建新的连接,确保连接的可用性。
4. 事务管理: 连接池可以协助管理数据库事务,保证在同一连接中执行的操作能够满足事务的原子性、一致性、隔离性和持久性(ACID 属性)。
5. 配置灵活性: 连接池提供多种配置选项,如最小/最大连接数、连接超时时间等,帮助开发人员根据具体应用需求调整资源分配策略。
研究问题及目的
在应用压测过程中,发现数据库的 TPS 不高的情况下, 数据库 CPU 就很高,而且有个数据库的事务指标跟应用的特点不匹配。因此经过不断的试验、研究,最终发现是数据库连接池的 autocommit 配置导致的。因此,本篇文章的主要探讨:
通过实验验证 autocommit=false 的性能影响
通过源码分析解释性能影响的原因
2. 实验设计与方法
注:mysql 服务端的 autocommit 默认值是****ON ,后续章节若无特殊说明,autocommit 指应用侧 dbcp 的配置
实验环境说明
硬件配置:MySQL 5.7 4C16G
软件版本和配置:spring 4.1.3.RELEASE + mybatis 3.2.7 + mybatis-spring 1.2.2 + dbcp 1.4 + mydql 5.7
数据库连接池配置参数
实验方法
实验方法:设计一个查询接口,根据主键 ID 查询一条数据。表中一共 12000 条数据,查询 id 的范围为[1,10000]。其中数据库表、sql 如下
表结构如下
sql 语句
实验分组
在其它变量一致的情况下,开启 autocommit 与关闭 autocommit 分别进行测试。通过压力机对接口进行梯度发压,对比 mysql CPU 使用率
实验结果
数据汇总
autocommit=false
数据库性能监控
应用侧接口性能监控
autocommit=true
数据库性能监控
应用侧性能监控
实验结论
autocommit=true(默认配置)支持的 TPS 是 18K,此时 CPU 使用率在 98%左右,而 autocommit=false 能支持的 TPS 是 8.5K,此时 CPU 使用率也在 98%左右。
明显看出,autocommit=false 的配置,导致数据库性能下降了一倍。
3. 源码分析与讨论
根据上文的实验看出,连接池的 autocommit 属性对于性能的消耗是巨大的,接下来我们一步一步深究一下其原因。
注:没有特殊说明,流程图、时序图等,都是基于 autocommit=false 画出的
mybatis 执行 sql 流程图
源码位于 org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke
由下图可知,mybatis 封装的 sql 执行步骤,还是离不开原生 jdbc 的三段式:获取连接、执行 sql、关闭连接。不过由于框架的封装,很多细节隐藏到了其它中间件,比如获取连接、关闭连接,底层都是由 dbcp 处理的。因此 autocommit 如何作用,我们还要继续深入 dbcp 的源码。
源码分析 autocommit=false 如何起作用的
连接的管理,都是由 dbcp 实现的,而 dbcp 依赖了 commons-pool 框架。
获取连接 from dbcp
在获取连接时,执行 GenericObjectPool#borrowObject 方法,即从连接池中获取一个可用的连接对象(可以是新建,也可以是从队列中获取闲置的),获取连接之后需要激活连接,代码为_factory.activateObject,这里的_factory 是 org.apache.commons.dbcp.PoolableConnectionFactory,其 activateObject 方法如下。conn.getAutoCommit()是获取连接的 autocommit(默认 true),_defaultAutoCommit 是连接池的配置项,被项目配置为 false。由于二者不一致,需要将连接的 autocommit 设置为 true,此时 mysql 服务器远端也会被设置为 false。
关闭连接 to dbcp(实际上是将连接归还给线程池)
在归还连接时,调用链路为 GenericObjectPool#returnObject > GenericObjectPool#addObjectToPool ,然后执行 PoolableConnectionFactory#passivateObject 的方法,有两个核心步骤进行数据库连接的配置:
1、如果连接不是自动提交且不是只读的,回滚
2、如果连接不是自动提交的,将其设置为自动提交
为什么 autocommit=false 会消耗一半的性能?
我们先来看一下,应用程序执行一条 sql 在 mysql general_log 的显示。下表是 dbcp 的 autocommit=false,执行一条查询语句时,mysql general_log 显示的 sql 明细
那么 autocommit=true 时,general_log 如何显示呢?如下表,仅有一条业务 sql!!!
至此,终于破案了。因为 autocommit 的频繁开启关闭,会导致以下问题:
1.性能开销:每次改变autocommit
的状态都需要执行额外的操作,这会增加 CPU 的工作负载。
2.事务管理:在autocommit
关闭的情况下,MySQL 会将后续的操作视为一个事务,直到显式地执行COMMIT
或ROLLBACK
。频繁切换autocommit
模式意味着频繁地开始和结束事务,这可能会导致事务日志的增长和额外的磁盘 I/O 操作。
3.锁定资源:在事务处理期间,可能会锁定一些资源,直到事务提交或回滚。频繁切换autocommit
模式可能会导致锁定时间变长,增加了死锁的可能性,影响并发性能。
4.网络开销:如果更改autocommit
状态的操作是在应用程序与数据库服务器之间进行的,那么这也会增加网络通信的开销。
4. 结论与建议
结论
前提条件:在 spring+mybatis+dbcp(autocommit=false)+mysql(autocommit 默认 true)的框架下
1.每次调用 SqlSessionTemplate(属于 mybatis-spring)的 sql 方法,应用程序与 mysql 之间会多出 5 次网络 io,mysql 多执行 5 个 sql
2.在极端场景下,mysql 性能下降 50%。
我的应用就属于以上极端场景,因此在调整 autocommit 后,配置不变的情况下,承担了原来翻倍的业务增长,为公司节省了数据库成本 10w 元/年
建议
1、dbcp 连接池,将配置 autocommit 设置为 true,与数据库保持一致。在需要事务控制的业务逻辑上,使用 spring 的 @Transactional 注解,或者使用 mybatis 原生的 SqlSession 管理事务等等。
2、其它连接池中间件如 C3P0、HikariCP、BoneCP 等都支持自定义配置 autocommit,也可能存在本文实验的问题。验证方法很简单:将 mysql 数据库 general_log 开启,然后找一个接口调用一下,看看是不是有多余的 5 条 sql 出现。
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/8ace0dd426f80020dd97d2bd7】。文章转载请联系作者。
评论