问题背景

近期客户生产环境报:查询数据库发现有大量sleep的连接(800多个),询问为什么不关闭这些没用的连接。这里说明一下,生产环境的业务量还是很大的,每分钟有几千的并发。
sleep连接过多会造成的影响(这里搬运ai的回答):

  1. 内存消耗(主要影响)
    Sleep 连接本身 不消耗 CPU,但会占用 线程 ID 和 文件描述符(FD)。如果系统 max_connections 较高(如默认 151),890 个 Sleep 连接可能导致:新连接被拒绝(Too many connections 错误)。
    线程切换开销增加(尤其在并发高的场景)。
  2. CPU 和线程调度
    Sleep 连接本身 不消耗 CPU,但会占用 线程 ID 和 文件描述符(FD)。如果系统 max_connections 较高(如默认 151),890 个 Sleep 连接可能导致:新连接被拒绝(Too many connections 错误)。
    线程切换开销增加(尤其在并发高的场景)。
  3. 其他资源
    文件描述符(FD):每个连接占用 1 个 FD,可能耗尽系统限制(ulimit -n)。临时表/缓冲区:某些后台操作(如临时表、排序缓冲区)可能因连接数过多而竞争资源。

概念理解

MySQL配置参数wait_timeout概念

定义:MySQL 服务器主动关闭空闲(Sleep)连接前的等待时间(单位:秒)。
默认值:通常为 28800秒(8小时)。
作用:当一个MySQL连接在wait_timeout秒内没有任何活动时,服务器会自动关闭该连接;释放资源:通过关闭空闲连接来释放服务器资源,避免长期不用的连接占用内存和连接数。
修改方法:

-- 将 wait_timeout 缩短(需评估业务场景)
SET GLOBAL wait_timeout = 600;  -- 10分钟

Hikari配置参数(项目使用hikari作为数据库连接池)

  • connection-timeout (默认: 30000ms)
    功能:从连接池获取连接的最大等待时间
    细节:
    如果所有连接都在使用中,且池已达到maximum-pool-size,新请求将等待此超时时间超时后抛出SQLException
    设为0表示无限等待(生产环境不推荐)
  • idle-timeout (默认: 600000ms/10分钟)
    功能:空闲连接存活时间
    细节:
    超过此时间的空闲连接将被释放
    最小有效值=10000ms(10秒)
    设为0表示禁用空闲连接回收(不推荐)
  • maximum-pool-size (默认: 10)
    功能:连接池最大连接数(包括使用中和空闲连接)
    最佳实践:
    通常建议公式:
    (核心数 * 2) + 有效磁盘数
    例如4核服务器: (4*2)+1 = 9
    maximum-pool-size: 10
  • minimum-idle (默认: 与maximum-pool-size相同)
    功能:连接池保持的最小空闲连接数
    特殊场景:
    设为0表示不保留空闲连接(适合突发流量场景)
    生产环境通常设为maximum-pool-size的1/4~1/2
  • max-lifetime (默认: 1800000ms/30分钟)
    功能:连接最大存活时间(从创建到销毁)
    关键点:
    到期后连接会被销毁(即使正在使用)
    必须比数据库的wait_timeout小2-3分钟
  • keepalive-time (默认: 0/禁用)
    功能:定期对空闲连接发送保活查询(如SELECT 1)
    推荐配置:
    keepalive-time: 30000 # 每30秒保活一次

配置示例(这里给出一个参考配置)

hikari:
  pool-name: "prod-pool"
  maximum-pool-size: 20
  minimum-idle: 5
  idle-timeout: 60000
  max-lifetime: 1740000  # 29分钟(MySQL wait_timeout=30分钟)
  connection-timeout: 1000
  leak-detection-threshold: 5000
  data-source-properties:
    cachePrepStmts: true
    prepStmtCacheSize: 500

那么MySQL数据库既配置了wait_timeout,连接又配置了idle-timeout,那么关闭数据库连接具体要以哪个为准呢?
下面是我的应用目前生产环境的hikari配置:

spring:
  datasource:
    hikari:
      connection-timeout: 20000
      idle-timeout: 120000
      maximum-pool-size: 200
      minimum-idle: 10
      register-mbeans: true

实践出真知

  1. 单独启动一个应用(应用内有定时任务的话先关闭,或者启动后有直接连接数据库操作的业务的话也先关闭,保持一个干净无访问的状态)
    使用下面命令查询数据库sleep的连接信息:
SELECT user, host, command, time, info FROM information_schema.processlist WHERE command = 'Sleep' ORDER BY time DESC;

在这里插入图片描述
正常情况下这里的条数应该是hikari连接池配置的minimum-idle数量(我这里多了1条不知道哪里的连接,不必理会),既hikari连接池会初始化minimum-idle设置的空闲连接,可以避免突然的并发访问带来的数据库连接消耗。
2. 并发量小使用空闲连接处理请求的情况
使用jmeter模拟20条并发访问:
在这里插入图片描述
查询sleep连接数:
在这里插入图片描述

没有变化,说明这10个空闲的连接足以处理完这20条并发请求。注意:这里的空闲连接在处理完一个请求后可以被立即继续使用用来处理其它的请求,除非所有空闲的连接都在被使用时,又有新的请求进来,这是才会去连接池建立新的连接(下面会证明)。
3. 并发量大空闲连接不够用的情况
使用jmeter模拟500条并发访问:
在这里插入图片描述
查询sleep连接数:
在这里插入图片描述

这里看到sleep的连接增加了8条,说明10个空闲连接已经不够用了
过2分钟后再查询连接数:
在这里插入图片描述

发现连接数从新变回minimum-idle。由此说明这些连接完全有hikari连接池管理,当minimum-idle的连接不够使用时会新建连接,这些连接使用完到达idle-timeout时间后由hikari主动发送关闭指令给到数据库关闭(发送 COM_QUIT 给 MySQL),然后继续维持minimum-idle的空闲连接。实际上跟MySQL设置的wait_timeout没有关系了,这是由于应用使用了连接池管理的好处,如果没有使用连接池,这些sleep的连接只能有数据库控制,到wait_timeout时间才能关闭。
4. 一些验证hikari连接池是否在应用中生效的方法(这个我没用,因为上述压测的方式已经验证它在正常运行)
(1) 添加监控代码

import com.zaxxer.hikari.HikariDataSource;

@RestController
public class PoolController {

    @Autowired
    private DataSource dataSource;

    @GetMapping("/pool-status")
    public String poolStatus() {
        if (dataSource instanceof HikariDataSource) {
            HikariDataSource hikari = (HikariDataSource) dataSource;
            return String.format(
                "Pool: %s, Active: %d, Idle: %d, Total: %d",
                hikari.getPoolName(),
                hikari.getHikariPoolMXBean().getActiveConnections(),
                hikari.getHikariPoolMXBean().getIdleConnections(),
                hikari.getHikariPoolMXBean().getTotalConnections()
            );
        }
        return "Not using HikariCP!";
    }
}

(2) 启动时打印配置
在 @PostConstruct 中输出配置:

@SpringBootApplication
public class MyApp {
    @Autowired
    private DataSource dataSource;

    @PostConstruct
    public void init() {
        if (dataSource instanceof HikariDataSource) {
            HikariConfig config = ((HikariDataSource) dataSource).getHikariConfig();
            System.out.println("=== HikariCP Config ===");
            System.out.println("jdbcUrl: " + config.getJdbcUrl());
            System.out.println("minIdle: " + config.getMinimumIdle());
            System.out.println("maxPoolSize: " + config.getMaximumPoolSize());
            System.out.println("idleTimeout: " + config.getIdleTimeout());
        }
    }
}

总结和解决思路

通过上述验证,可以得出系统hikari在正常运行,至于数据库还是产生大量sleep连接,只能说明系统并发量太高了。
一些想法:

  1. 先跟客户沟通看客服能否接受现在的sleep连接数,毕竟他们生产环境给到数据库的内存还是很充足的。
  2. 如果想这些sleep的连接立即关闭,可以修改连接池idle-timeout的设置,设置的小一点,如:10秒,这样连接可以很快关闭,但是不建议。试想高并发下,空闲连接是可以快速关闭,给客户一种数据库岁月静好的假象,但是业务量是实际存在的,这样每次都要频繁的关闭和创建新的连接,这个开销是非常大的,搞不好会拖垮整个系统。
  3. 优化并发量较大的系统接口,缩短每个连接的处理速度,提高复用率,这样就会少创建新的连接。具体可以从业务层面,数据库查询、索引优化等入手。
  4. 添加缓存,减少数据库的访问。把那些数据库查询频繁的操作放到缓存中,不走数据库,从根本上解决问题

一些拙见,记录下自己解决问题的一个过程,不对的地方感谢指正。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐