写点什么

DBCP 一个配置,浪费了 MySQL 50% 的性能!

  • 2024-03-26
    北京
  • 本文字数:4080 字

    阅读完需:约 13 分钟

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


数据库连接池配置参数


#jdbcjdbc.mysql.driver=com.mysql.jdbc.Driverjdbc.mysql.url=jdbc:mysql://host:port/my_db?connectTimeout=1000&socketTimeout=1000&serverTimezone=Asia/Shanghaijdbc.mysql.connectionProperties=useUnicode=true;characterEncoding=utf8;rewriteBatchedStatements=true;autoReconnectForPools=true;failOverReadOnly=false;roundRobinLoadBalance=true;allowMultiQueries=true;useLocalSessionState=true
#dbcpdbcp.initialSize=4dbcp.maxActive=12dbcp.maxIdle=12dbcp.minIdle=4dbcp.maxWait=6000dbcp.defaultAutoCommit=truedbcp.timeBetweenEvictionRunsMillis=60000dbcp.numTestsPerEvictionRun=16dbcp.minEvictableIdleTimeMillis=180000dbcp.testWhileIdle=truedbcp.testOnBorrow=falsedbcp.testOnReturn=falsedbcp.validationQuery=select 1dbcp.removeAbandoned=truedbcp.removeAbandonedTimeout=180dbcp.logAbandoned=true
复制代码

实验方法

实验方法:设计一个查询接口,根据主键 ID 查询一条数据。表中一共 12000 条数据,查询 id 的范围为[1,10000]。其中数据库表、sql 如下


表结构如下


CREATE TABLE `task` (  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '唯一标识',  `cluster` varchar(100) NOT NULL DEFAULT '',  `system` varchar(50) NOT NULL COMMENT '系统',  `app_name` varchar(50) NOT NULL COMMENT '应用',  部分字段省略....  PRIMARY KEY (`id`),) ENGINE=InnoDB AUTO_INCREMENT=77 DEFAULT CHARSET=utf8 COMMENT='任务';
复制代码


sql 语句


select field1,field2,...,fieldN from task where id = ${id}
复制代码

实验分组

在其它变量一致的情况下,开启 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。


   public void activateObject(Object obj) throws Exception {        if(obj instanceof DelegatingConnection) {            ((DelegatingConnection)obj).activate();        }        if(obj instanceof Connection) {            Connection conn = (Connection)obj;            // autocommit=false 起作用的地方            if (conn.getAutoCommit() != _defaultAutoCommit) {                conn.setAutoCommit(_defaultAutoCommit);            }            if ((_defaultTransactionIsolation != UNKNOWN_TRANSACTIONISOLATION)                     && (conn.getTransactionIsolation() !=                     _defaultTransactionIsolation)) {                conn.setTransactionIsolation(_defaultTransactionIsolation);            }            if ((_defaultReadOnly != null) &&                     (conn.isReadOnly() != _defaultReadOnly.booleanValue())) {                conn.setReadOnly(_defaultReadOnly.booleanValue());            }            if ((_defaultCatalog != null) &&                    (!_defaultCatalog.equals(conn.getCatalog()))) {                conn.setCatalog(_defaultCatalog);            }        }    }
复制代码

关闭连接 to dbcp(实际上是将连接归还给线程池)


在归还连接时,调用链路为 GenericObjectPool#returnObject > GenericObjectPool#addObjectToPool ,然后执行 PoolableConnectionFactory#passivateObject 的方法,有两个核心步骤进行数据库连接的配置:


1、如果连接不是自动提交且不是只读的,回滚


2、如果连接不是自动提交的,将其设置为自动提交


public void passivateObject(Object obj) throws Exception {        if(obj instanceof Connection) {            Connection conn = (Connection)obj;            // 判断是否需要rollback            if(!conn.getAutoCommit() && !conn.isReadOnly()) {                conn.rollback();            }            conn.clearWarnings();            // 如果连接不是autocommit,设置autocommit=true            if(!conn.getAutoCommit()) {                conn.setAutoCommit(true);            }        }        if(obj instanceof DelegatingConnection) {            ((DelegatingConnection)obj).passivate();        }    }
复制代码

为什么 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 会将后续的操作视为一个事务,直到显式地执行COMMITROLLBACK。频繁切换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%。


一般情况下应用层不需要开启事务的案例:1、单条select2、多条select3、单条insert4、单条update5、单条delete极端场景,就是以上5种案例占数据库所有sql的比例100%,那么你的数据库有50%的CPU是浪费的。
复制代码


我的应用就属于以上极端场景,因此在调整 autocommit 后,配置不变的情况下,承担了原来翻倍的业务增长,为公司节省了数据库成本 10w 元/年

建议

1、dbcp 连接池,将配置 autocommit 设置为 true,与数据库保持一致。在需要事务控制的业务逻辑上,使用 spring 的 @Transactional 注解,或者使用 mybatis 原生的 SqlSession 管理事务等等。


2、其它连接池中间件如 C3P0、HikariCP、BoneCP 等都支持自定义配置 autocommit,也可能存在本文实验的问题。验证方法很简单:将 mysql 数据库 general_log 开启,然后找一个接口调用一下,看看是不是有多余的 5 条 sql 出现。

发布于: 刚刚阅读数: 5
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
DBCP一个配置,浪费了MySQL 50%的性能!_京东科技开发者_InfoQ写作社区