生产环境数据库连接池耗尽的全流程排查与性能优化实战

描述

1. 连接池原理与工作机制

1.1 连接池核心概念

数据库连接池是应用程序与数据库之间的缓存连接组件。连接池在应用程序启动时创建一组数据库连接,应用程序从连接池获取连接,使用完毕后归还连接池而非关闭连接,避免反复建立和关闭连接的性能开销。

连接池的核心参数有三个:最小连接数(minimumIdle)、最大连接数(maximumPoolSize)、连接超时(connectionTimeout)。

最小连接数是连接池保持的空闲连接数量。在应用程序启动时,连接池会根据这个值创建相应数量的连接。如果实际使用的连接数小于最小连接数,空闲的连接会保持连接状态但不使用。

最大连接数是连接池允许的最大连接数。当所有连接都被占用时,新请求需要等待可用连接。如果等待时间超过连接超时时间,应用程序会抛出SQLException。

连接超时(connectionTimeout)是获取连接的最大等待时间。默认值为30秒。当连接池中没有可用连接时,请求会进入等待队列,等待时间超过这个值后会抛出异常。

连接池的工作原理是:应用程序启动时,根据最小连接数创建一批连接;请求到来时,从连接池队列取出可用连接标记为占用,使用完毕后清空占用标记归还队列;如果可用连接数为零,新请求进入等待队列;如果等待时间超过连接超时,抛出SQLException。

连接生命周期的状态流转:初始化→空闲→占用→归还→空闲→销毁。连接在占用期间执行SQL,如果SQL执行时间过长,会导致连接被长时间占用;连接归还后等待下一次使用;如果连接空闲时间超过最大生存时间,连接被销毁。

1.2 常见连接池实现

HikariCP是目前最流行的连接池实现,被Spring Boot选为默认连接池。HikariCP以性能著称,官方数据显示其性能是其他连接池的数倍。

HikariCP的优势在于:轻量级,核心代码约1.3万行;零依赖,不需要额外jar包;采用FastList替代ArrayList减少开销;采用字节码增强减少反射调用;连接健康检查高效。

HikariCP的配置项:

 

# application.yml
spring:
datasource:
    url:jdbc//localhost:3306/mydb
    username:root
    password:password
    driver-class-name:com.mysql.cj.jdbc.Driver
    hikari:
      # 连接池名称
      pool-name:HikariPool-MySQL
      # 最小空闲连接数
      minimum-idle:5
      # 最大连接数
      maximum-pool-size:20
      # 连接超时(毫秒)
      connection-timeout:30000
      # 空闲超时
      idle-timeout:600000
      # 最大生存时间
      max-lifetime:1800000
      # 连接测试查询
      connection-test-query:SELECT1
      # 启用连接测试
      validation-timeout:5000

 

HikariCP监控指标:

 

// 通过JMX访问HikariCP MBean
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName mxBeanName = new ObjectName("com.zaxxer.hikari:type=Pool (MySQL)");
HikariPoolMXBean poolProxy = JMX.newMXBeanProxy(mbs, mxBeanName, HikariPoolMXBean.class);

// 获取连接池状态
poolProxy.getActiveConnections();    // 当前活跃连接数
poolProxy.getIdleConnections();     // 当前空闲连接数
poolProxy.getTotalConnections();     // 总连接数
poolProxy.getThreadsAwaitingConnection();  // 等待连接的线程数

 

Druid是阿里巴巴开源的连接池,除了连接管理功能外还提供强大的监控功能。Druid的监控面板可以查看连接池状态、SQL执行耗时、连接泄漏追踪等。Druid的WallFilter提供SQL防火墙功能,防止SQL注入。

Druid配置示例:

 

spring:
  datasource:
    url:jdbc//localhost:3306/mydb
    username:root
    password:password
    driver-class-name:com.mysql.cj.jdbc.Driver
    druid:
      # 连接池配置
      initial-size:5
      min-idle:5
      max-active:20
      max-wait:60000
      # 连接有效性检测
      validation-query:SELECT1
      test-while-idle:true
      test-on-borrow:false
      test-on-return:false
      # 泄漏检测
      remove-abandoned:true
      remove-abandoned-timeout:300
      log-abandoned:true
      # 监控配置
      filter:
        stat:
          enabled:true
          log-slow-sql:true
          slow-sql-millis:2000
        wall:
          enabled:true
          config:
            multi-statement-allow:true

 

C3P0是历史悠久的连接池实现,被Hibernate早期版本使用。C3P0采用同步锁实现,性能较差,在高并发场景下可能成为瓶颈。

 

// C3P0配置
ComboPooledDataSource cpds = new ComboPooledDataSource();
cpds.setDriverClass("com.mysql.cj.jdbc.Driver");
cpds.setJdbcUrl("jdbc//localhost:3306/mydb");
cpds.setUser("root");
cpds.setPassword("password");

// 连接池参数
cpds.setInitialPoolSize(5);
cpds.setMinPoolSize(5);
cpds.setMaxPoolSize(20);
cpds.setMaxStatements(100);        // 缓存的PreparedStatement数量
cpds.setMaxIdleTime(300);         // 最大空闲时间(秒)
cpds.setCheckoutTimeout(3000);     // 获取连接超时(毫秒)
cpds.setUnreturnedConnectionTimeout(60);  // 连接未返回超时

 

C3P0和DBCP由于性能问题已逐渐被HikariCP取代。新项目建议选择HikariCP,对监控有特殊需求可以选择Druid。

1.3 连接池与数据库性能的关系

连接池是应用层与数据库之间的桥梁,连接池的配置直接影响数据库的承载能力和响应时间。

连接池过小的表现是:并发请求增加时,可用连接迅速耗尽,请求开始排队等待;等待时间超过连接超时后抛出异常;数据库CPU使用率可能很低但响应时间很长。连接池过小限制了应用的并发处理能力。

连接池过大的表现是:数据库连接数大量增加,数据库内存占用增加;数据库并发处理压力增大,CPU使用率升高;连接数过多增加了数据库的管理开销。连接池过大可能压垮数据库。

最优连接数需要根据数据库服务器配置和应用特性确定。一般建议公式:连接数 = (核心数 * 2) + 有效磁盘数。假设8核CPU和1块SSD,最优连接数约为17。这个公式是起点,实际需要通过压测调整。

连接数计算参考表:

数据库类型 推荐最大连接数 备注
MySQL 100-500 取决于innodb_buffer_pool_size
PostgreSQL 100-300 推荐使用PgBouncer
Oracle 100-1000 取决于SGA大小
SQL Server 500-2000 取决于内存

2. 连接池耗尽原因分析

2.1 连接泄漏的场景与原因

连接泄漏是指应用程序获取连接后未正确归还,导致连接一直被占用无法释放。连接泄漏的最终结果是连接池耗尽,新请求无法获取连接。

最常见的泄漏场景是在finally块未执行归还操作。正确代码应该是:

 

Connection conn = null;
try {
    conn = dataSource.getConnection();
    // 使用连接执行SQL
    PreparedStatement ps = conn.prepareStatement(sql);
    ps.executeQuery();
} catch (SQLException e) {
    // 异常处理
} finally {
    if (conn != null) {
        try {
            conn.close();  // 归还连接到连接池
        } catch (SQLException e) {
            // 忽略关闭异常
        }
    }
}

 

如果忘记在finally中归还,或者在finally中归还前就抛出异常导致归还语句未执行,就会造成泄漏。

异常处理导致的泄漏也常见。如果在获取连接后、使用前就抛出异常,连接没有进入使用逻辑,也就不会执行close()。

 

// 错误示例:在获取连接后进行参数校验
Connection conn = dataSource.getConnection();
if (conn == null) {
    throw new RuntimeException("Failed to get connection");
}

// 如果参数校验在conn获取之前,就会导致连接泄漏
if (!validateParams(params)) {
    conn.close();  // 如果忘记这行,就会泄漏
    throw new IllegalArgumentException("Invalid params");
}

 

事务未提交或未回滚也会导致连接占用。

 

// 错误示例:忘记提交事务
Connection conn = dataSource.getConnection();
try {
    conn.setAutoCommit(false);
    // 执行多个SQL
    executeSql1(conn);
    executeSql2(conn);
    // 忘记提交或回滚
    // conn.commit();  // 如果忘记这行,连接会一直占用
} catch (SQLException e) {
    conn.rollback();  // 即使回滚,如果异常发生在commit之前
} finally {
    conn.close();
}

 

ResultSet和Statement未关闭也可能间接导致连接泄漏。

 

// 正确做法:使用try-with-resources
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {
    while (rs.next()) {
        // 处理结果
    }
} // 自动关闭rs、ps、conn

 

连接泄漏的排查方法:

 

// 启用HikariCP的泄漏检测
spring:
  datasource:
    hikari:
      leak-detection-threshold: 60000  # 60秒检测阈值

// Druid的泄漏检测
spring:
  datasource:
    druid:
      remove-abandoned: true
      remove-abandoned-timeout: 60    # 60秒
      log-abandoned: true

 

2.2 连接池配置不当

连接池参数配置不合理会导致性能问题或连接耗尽。

最大连接数设置过小是最常见的配置问题。

 

# 配置过小示例
spring:
datasource:
    hikari:
      maximum-pool-size:5# 对于高并发应用明显不足

# 正确配置
spring:
datasource:
    hikari:
      maximum-pool-size:50# 根据并发量调整

 

如果应用并发量较大,最小连接数无法满足请求,排队等待的请求会堆积,最终超时。

连接最大生存时间(maxLifetime)设置过长或过短都有问题。

 

# maxLifetime设置过长
spring:
datasource:
    hikari:
      max-lifetime:7200000# 2小时,可能超过数据库timeout

# 正确配置
spring:
datasource:
    hikari:
      max-lifetime:1800000# 30分钟,数据库timeout减去30秒

 

设置过长会导致连接在数据库端超时后仍在使用,可能触发连接错误。设置过短会导致频繁创建销毁连接,增加开销。

连接超时(connectionTimeout)设置过短会导致正常请求被误杀。

 

# 设置过短
spring:
datasource:
    hikari:
      connection-timeout:1000# 1秒,正常SQL无法完成

# 正确配置
spring:
datasource:
    hikari:
      connection-timeout:30000# 30秒,考虑SQL执行时间和网络延迟

 

如果SQL执行时间较长,短时间内获取不到连接就会超时。连接超时应该大于正常SQL执行时间的最大值,考虑网络延迟和数据库负载。

最小空闲连接数(minimumIdle)设置过大或过小都有问题。

 

# 设置过小
spring:
datasource:
    hikari:
      minimum-idle:1# 正常负载时连接不足

# 设置过大
spring:
datasource:
    hikari:
      minimum-idle:50# 即使没有请求也保持50个连接

# 正确配置
spring:
datasource:
    hikari:
      minimum-idle:10# 根据正常负载设置

 

设置过大会造成资源浪费,空闲连接占用连接数。设置过小在高并发时需要频繁创建新连接,增加延迟。对于稳定负载的应用,可以设置minimumIdle等于maximumPoolSize。

2.3 SQL执行慢导致连接堆积

SQL执行时间是影响连接周转率的关键因素。假设连接池大小为10,每秒可以处理10个请求,每个SQL执行时间100毫秒;如果SQL执行时间延长到1秒,每秒只能处理10个连接,连接周转率下降10倍。

慢SQL的成因包括:缺少索引导致全表扫描;SQL写法问题如SELECT *、嵌套子查询;表数据量过大;数据库服务器负载高;网络延迟增大。

连接堆积的表现是:应用层连接池可用连接数接近零;数据库端显示大量sleep状态的连接;请求排队增加,响应时间上升;严重时触发连接超时异常。

排查慢SQL的方法包括:开启数据库慢查询日志;使用EXPLAIN分析SQL执行计划;查看数据库监控的慢SQL排行榜;分析应用日志中SQL执行时间。

MySQL慢查询日志配置:

 

# my.cnf
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1

 

PostgreSQL慢查询日志配置:

 

-- 修改配置
ALTER SYSTEM SET log_min_duration_statement = 1000;  -- 1秒
ALTER SYSTEM SET log_statement = 'all';

-- 查看日志文件
SELECT pg_read_file('/var/log/postgresql/postgresql-14-slow.log');

 

EXPLAIN分析示例:

 

EXPLAIN SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > '2026-01-01'
AND c.status = 'active';

-- 分析结果
-- type: ALL 表示全表扫描,需要优化
-- key: 显示使用的索引
-- rows: 扫描的行数
-- Extra: Using filesort/Using temporary 需要优化

 

2.4 数据库本身连接数限制

数据库服务器有最大连接数限制,当应用连接池的总连接数接近这个限制时,新请求无法获取连接。

MySQL的max_connections参数控制最大连接数,默认151。生产环境通常需要调大,但过大的max_connections会消耗过多内存。每个MySQL连接约占用256KB-400KB的内存。

 

-- 查看当前最大连接数
SHOWVARIABLESLIKE'max_connections';

-- 设置最大连接数(临时)
SETGLOBAL max_connections = 500;

-- 永久配置(my.cnf)
[mysqld]
max_connections = 500

-- 查看当前连接数
SHOWSTATUSLIKE'Threads_connected';
SHOWPROCESSLIST;

 

PostgreSQL的max_connections默认100。PostgreSQL采用进程模型,每个连接启动一个服务器进程,过大的连接数会消耗大量系统资源。PostgreSQL推荐使用连接池中间件如PgBouncer处理大量连接。

 

-- 查看当前最大连接数
SHOW max_connections;

-- 修改postgresql.conf
max_connections = 200

-- 查看当前连接数
SELECT count(*) FROM pg_stat_activity;

 

Oracle的processes参数控制最大进程数,包括用户进程和后台进程。

 

-- 查看当前设置
SHOW PARAMETER processes;

-- 修改
ALTER SYSTEM SET processes = 500 SCOPE = SPFILE;

 

连接数限制问题的表现是:应用连接池获取连接时抛出too many connections异常;数据库端无法新建连接;已建立的连接正常但新建连接失败。

3. 排查方法论与工具

3.1 连接状态监控

连接池通常提供管理接口或监控端点,可以实时查看连接池状态。

HikariCP通过JMX或Actuator端点暴露连接池信息。

 

# 启用Actuator端点
spring:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,hikaricp
  metrics:
    enable:
      hikaricp: true

 

监控指标:

 

# 通过Actuator查询HikariCP指标
curl http://localhost:8080/actuator/metrics/hikaricp.connections.active
curl http://localhost:8080/actuator/metrics/hikicp.connections.idle
curl http://localhost:8080/actuator/metrics/hikaricp.connections

# 通过JMX访问
jconsole
# 连接到Java应用后,查找com.zaxxer.hikari:type=Pool节点

 

Druid提供Web监控页面。

 

# 启用Druid监控
spring:
datasource:
    druid:
      stat-view-config:
        enabled:true
        url-pattern:/druid/*
        reset-enable:true
        login-password:admin
        login-username:admin

 

连接池状态的典型异常模式:

模式 特征 原因
连接池饱和 totalConnections等于maximumPoolSize,threadsAwaitingConnection大于0 并发过高或SQL执行过慢
连接泄漏 totalConnections持续增长接近maximumPoolSize 代码未正确关闭连接
连接耗尽 threadsAwaitingConnection大于0且持续增长 配置不当或数据库限制

告警阈值建议:activeConnections持续超过maximumPoolSize的80%应发出预警;threadsAwaitingConnection大于0应发出告警;totalConnections接近maximumPoolSize应立即告警。

3.2 线程堆栈分析

线程堆栈分析是排查连接泄漏和死锁问题的重要手段。当应用出现连接等待或挂起时,获取线程堆栈可以定位问题代码。

使用jstack(Java)获取线程堆栈:

 

# 获取线程堆栈
jstack -l PID > threaddump.txt

# 强制获取(线程可能hung住)
jstack -F PID > threaddump.txt

# 多次dump对比
jstack -l PID > threaddump_$(date +%H%M%S).txt

 

堆栈分析的重点:查找处于WAITING或BLOCKED状态的线程,分析等待原因;查找持有锁的线程,分析是否导致其他线程阻塞;查找SQL执行相关的线程,分析SQL执行时间。

典型的连接泄漏堆栈特征:

 

"http-nio-8080-exec-1" #123 daemon prio=5 os_prio=0 tid=0x12345678
   java.lang.Thread.State: WAITING
    at java.lang.Object.wait(Native Method)
    at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:xxx)
    ...
    at com.example.service.OrderService.getOrders(OrderService.java:45)

 

查看等待连接的线程:

 

# 筛选等待连接的线程
grep -A 20 "waiting for connection" threaddump.txt

# 查找HikariPool相关调用
grep -B 5 -A 15 "HikariPool" threaddump.txt

 

典型的死锁堆栈特征:

 

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting for lock on java.lang.Object@12345678
  owned by Thread-2

"Thread-2":
  waiting for lock on java.lang.Object@87654321
  owned by Thread-1

 

jstack -l会自动检测并报告死锁。

3.3 数据库会话分析

数据库端的会话信息可以反映连接池的连接使用情况。

MySQL会话查询:

 

SHOW PROCESSLIST;

-- 详细版本
SHOWFULLPROCESSLIST;

-- Information Schema方式
SELECT * FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep'
ORDERBYTIMEDESC;

-- 按用户统计连接数
SELECTuser, COUNT(*) ascount
FROM information_schema.PROCESSLIST
GROUPBYuser;

-- 按数据库统计连接数
SELECT db, COUNT(*) ascount
FROM information_schema.PROCESSLIST
GROUPBY db;

 

PROCESSLIST关键列说明:

说明
Id 连接ID
User 用户名
Host 客户端地址
db 当前数据库
Command 连接状态(Sleep/Query)
Time 持续时间(秒)
State 当前状态
Info 执行的SQL

TIME列持续增长的连接可能是慢SQL或连接泄漏。

 

-- 查找长时间运行的查询
SELECT * FROM information_schema.PROCESSLIST
WHERE COMMAND = 'Query'
AND TIME > 60
ORDER BY TIME DESC;

-- 查找未释放的连接
SELECT * FROM information_schema.PROCESSLIST
WHERE COMMAND = 'Sleep'
AND TIME > 300;

 

PostgreSQL会话查询:

 

-- 查看所有会话
SELECT * FROM pg_stat_activity;

-- 查找活动查询
SELECT pid, usename, query_start, state, query
FROM pg_stat_activity
WHERE state = 'active'
ORDERBY query_start;

-- 查找等待中的会话
SELECT pid, usename, wait_event_type, wait_event
FROM pg_stat_activity
WHERE wait_event ISNOTNULL;

-- 杀死长时间运行的查询
SELECT pg_cancel_backend(pid);  -- 温和取消
SELECT pg_terminate_backend(pid);  -- 强制终止

 

3.4 慢查询日志

慢查询日志是定位SQL性能问题的基本工具。

MySQL慢查询日志分析:

 

# 使用mysqldumpslow分析
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

# 使用pt-query-digest(Percona Toolkit)
pt-query-digest /var/log/mysql/slow.log

# 分析结果示例
# Query 1: 0.1234s
# SELECT * FROM orders WHERE status = ?
# Time: 2026-03-20T1030
# EXPLAIN: type=ALL, rows=1000000

 

PostgreSQL慢查询日志分析:

 

-- 使用pgBadger分析
pgbadger /var/log/postgresql/postgresql-14-slow.log

-- 手动分析
SELECT query, calls, mean_time, total_time
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;

 

4. 优化方案与配置建议

4.1 连接池参数调优

HikariCP推荐配置(针对8核CPU和8GB内存的服务器):

 

spring:
  datasource:
    hikari:
      # 连接池大小计算:(核心数 * 2) + 有效磁盘数 = 17
      # 考虑到有SSD,可以设置稍大一些
      maximum-pool-size:20

      # 最小空闲连接数
      # 稳定负载设置等于maximum-pool-size
      # 波动负载设置小于maximum-pool-size
      minimum-idle:10

      # 连接超时:考虑SQL执行时间、网络延迟
      connection-timeout:30000

      # 空闲超时:设置为maxLifetime的一半
      idle-timeout:600000

      # 最大生存时间:数据库timeout减去30秒
      # MySQL默认timeout为8小时
      max-lifetime:1800000

      # 连接测试查询
      connection-test-query:SELECT1

      # 泄漏检测阈值
      leak-detection-threshold:60000

 

最大连接数的计算方法:

 

# 应用并发线程数 × 每个线程的连接数 + 预留连接数
# 假设应用最大并发100,每线程1个连接,预留10个连接
max_connections = 100 + 10 = 110

# 考虑数据库服务器能力
# MySQL: max_connections = min(110, 500)
# PostgreSQL: max_connections = min(110, 100) 推荐使用PgBouncer

 

连接超时的设置原则:connectionTimeout应大于SQL最大执行时间;考虑网络延迟和数据库负载;过短会误杀正常请求,过长会增加用户等待时间。

空闲连接管理的设置原则:minimumIdle应设置足够应对正常负载;如果流量稳定可以设置minimumIdle=maximumPoolSize;如果流量波动大可以设置较小的minimumIdle,让连接池弹性伸缩。

4.2 SQL优化

SQL优化是解决连接池耗尽问题的根本途径。

索引优化的原则:分析慢查询的执行计划;为WHERE、JOIN、ORDER BY涉及的列创建索引;避免过多索引影响写入性能;定期分析表统计信息优化器选择。

 

-- 创建索引示例
CREATEINDEX idx_orders_customer_id ON orders(customer_id);
CREATEINDEX idx_orders_status ON orders(status);
CREATEINDEX idx_orders_created_at ON orders(created_at);

-- 复合索引示例
CREATEINDEX idx_orders_cust_status ON orders(customer_id, status);

-- 查看索引使用情况
EXPLAINSELECT * FROM orders WHERE customer_id = 123;

-- 分析表统计信息
ANALYZETABLE orders;

 

SQL写法优化:

 

-- 避免SELECT *
SELECT order_id, customer_id, total_amount, created_at
FROM orders WHERE order_id = 123;

-- 使用LIMIT分页
SELECT * FROM orders
ORDERBY created_at DESC
LIMIT0, 20;

-- 避免嵌套子查询,改用JOIN
-- 低效
SELECT * FROM orders
WHERE customer_id IN (
    SELECTidFROM customers WHEREstatus = 'active'
);

-- 高效
SELECT o.* FROM orders o
INNERJOIN customers c ON o.customer_id = c.id
WHERE c.status = 'active';

-- 使用批量操作
INSERTINTO order_items (order_id, product_id, quantity)
VALUES (1, 101, 2), (1, 102, 1), (1, 103, 3);

 

事务优化原则:保持事务短小,减少锁定时间;避免在事务中执行不必要的SQL;合理使用隔离级别,不是所有场景都需要高隔离级别;使用只读事务优化只读查询。

 

// 正确的事务处理
@Transactional
public void createOrder(OrderDTO orderDTO) {
    // 验证
    validateOrder(orderDTO);

    // 插入订单
    Order order = new Order();
    order.setCustomerId(orderDTO.getCustomerId());
    order.setStatus("pending");
    orderRepository.save(order);

    // 扣减库存(必须在事务内)
    inventoryService.decreaseStock(orderDTO.getItems());

    // 更新订单状态
    order.setStatus("confirmed");
    orderRepository.save(order);
}

 

4.3 数据库参数调整

数据库参数的合理配置可以提升连接处理能力和稳定性。

MySQL关键参数:

 

[mysqld]
# 连接相关
max_connections = 500
wait_timeout = 28800
interactive_timeout = 28800
max_connect_errors = 10000

# 缓冲池配置
innodb_buffer_pool_size = 4G  # 建议为系统内存的60-80%
innodb_buffer_pool_instances = 4

# 连接优化
thread_cache_size = 50
table_open_cache = 4000
sort_buffer_size = 2M
join_buffer_size = 2M

# 日志配置
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1

 

PostgreSQL关键参数:

 

-- 连接配置
ALTERSYSTEMSET max_connections = 200;
ALTERSYSTEMSET superuser_reserved_connections = 3;

-- 内存配置
ALTERSYSTEMSET shared_buffers = '2GB';  -- 建议为系统内存的25%
ALTERSYSTEMSET effective_cache_size = '6GB';  -- 建议为系统内存的75%
ALTERSYSTEMSET work_mem = '64MB';
ALTERSYSTEMSET maintenance_work_mem = '512MB';

-- 查询优化
ALTERSYSTEMSET random_page_cost = 1.1;  -- SSD设置为1.1,机械硬盘设置为4
ALTERSYSTEMSET effective_io_concurrency = 200;  -- SSD设置为200

 

连接池中间件是解决大量连接问题的方案。PgBouncer可以在应用和数据库之间提供连接池功能,应用连接PgBouncer,PgBouncer复用少量真实连接访问数据库。

PgBouncer配置:

 

[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

# 连接池模式
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3

# 超时配置
server_idle_timeout = 600
server_lifetime = 3600
server_connect_timeout = 15

 

5. 实战案例与证据链

5.1 典型耗尽场景复现

某电商系统在促销期间出现大量数据库连接超时错误,导致部分用户下单失败。故障持续约15分钟,影响订单量约2000单。

故障现象:应用日志中出现大量SQLException,消息包含timeout、connection字样;Druid监控显示连接池activeConnections达到最大值30,threadsAwaitingConnection持续大于0;数据库端SHOW PROCESSLIST显示约25个连接处于Sleep状态。

排查过程:获取应用线程堆栈发现大量线程在等待获取连接,堆栈显示都在执行订单创建相关的SQL;查询Druid监控的SQL统计,发现有一个更新订单状态的SQL平均执行时间从正常的50毫秒上升到800毫秒;进一步分析该SQL,发现缺少索引导致全表扫描。

 

-- 问题SQL
UPDATE orders SETstatus = 'paid', paid_at = NOW()
WHERE order_id = ?;

-- 分析执行计划
EXPLAINUPDATE orders SETstatus = 'paid', paid_at = NOW()
WHERE order_id = ?;
-- 结果:type=ALL, key=NULL, rows=1000000

-- 发现缺少主键索引
SHOWINDEXFROM orders;
-- 结果:order_id字段没有索引

 

故障根因:订单表在促销前缺少分表索引,数据量超过1000万行后查询变慢;促销期间并发量增加,耗时的SQL占用连接时间增长;可用连接迅速耗尽,新请求开始排队等待。

5.2 优化前后性能对比

针对故障原因,团队实施了三项优化措施:添加订单表索引;调整连接池参数;优化慢SQL。

索引优化:为订单表添加(order_id, status)组合索引;将订单查询从全表扫描改为索引扫描。

 

-- 添加索引
ALTER TABLE orders ADD INDEX idx_order_id_status (order_id, status);

-- 验证索引
EXPLAIN SELECT * FROM orders WHERE order_id = 123;
-- 结果:type=ref, key=idx_order_id_status, rows=1

 

连接池调优:将maximumPoolSize从30调整到50;将minimumIdle从5调整到20;将connectionTimeout从10秒调整到30秒。

 

spring:
  datasource:
    druid:
      max-active: 50
      min-idle: 20
      max-wait: 30000

 

慢SQL优化:分析其他慢SQL并优化;使用EXPLAIN验证优化效果;建立慢SQL监控告警机制。

 

-- 批量优化其他慢SQL
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_orders_created_at ON orders(created_at);

-- 优化订单查询
SELECT o.*, c.name as customer_name
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
WHERE o.order_id = ?;

 

优化效果:同等促销负载下,连接池activeConnections峰值从30降低到18,threadsAwaitingConnection降为0;数据库连接超时错误消失,订单成功率从98.2%提升到99.8%;系统平均响应时间从450毫秒降低到120毫秒。

5.3 连接池监控仪表板

建立连接池监控仪表板可以实时掌握连接池状态。

Prometheus监控配置:

 

# prometheus.yml
scrape_configs:
  - job_name: 'spring-boot-actuator'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['localhost:8080']

 

Grafana仪表板查询:

 

# 连接池使用率
hikaricp_connections_active / hikaricp_connections * 100

# 等待连接的线程数
hikaricp_threads_awaiting_connection

# 连接获取时间
hikaricp_connection_timeout_total

# 连接泄漏数
hikaricp_leased_connections

 

5.4 连接池高可用设计

连接池的高可用设计可以避免单点故障导致的数据库访问中断。

主备切换方案:

 

# Spring Boot多数据源配置
spring:
datasource:
    # 主库
    primary:
      url:jdbc//master:3306/mydb
      username:root
      password:password
      hikari:
        pool-name:HikariPool-Primary
        maximum-pool-size:20
    # 备库
    secondary:
      url:jdbc//slave:3306/mydb
      username:root
      password:password
      hikari:
        pool-name:HikariPool-Secondary
        maximum-pool-size:10

 

读写分离方案:

 

// 读写分离路由配置
@Configuration
publicclass DataSourceConfig {
    @Bean
    public DataSource routingDataSource(
            @Qualifier("masterDataSource") DataSource masterDataSource,
            @Qualifier("slaveDataSource") DataSource slaveDataSource) {

        RoutingDataSource routingDataSource = new RoutingDataSource();
        Map targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource);
        targetDataSources.put("slave", slaveDataSource);
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(masterDataSource);

        return routingDataSource;
    }
}

// 使用AOP实现读写分离
@Aspect
@Component
publicclass ReadWrite分离Aspect {
    @Around("execution(* com.example.repository.*Repository.*(..))")
    public Object routeRead(JoinPoint joinPoint) {
        DataSourceContextHolder.setDataSource("slave");
        try {
            return joinPoint.proceed();
        } finally {
            DataSourceContextHolder.clear();
        }
    }
}

 

5.5 连接池性能测试

在生产环境部署前,应该进行连接池性能测试,确保配置能够满足业务需求。

性能测试脚本示例:

 

// 使用JMH进行连接池性能测试
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Thread)
publicclass ConnectionPoolBenchmark {
    private DataSource dataSource;

    @Setup
    public void setup() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc//localhost:3306/mydb");
        config.setUsername("root");
        config.setPassword("password");
        config.setMaximumPoolSize(20);
        config.setMinimumIdle(5);
        config.setConnectionTimeout(30000);
        config.setIdleTimeout(600000);
        config.setMaxLifetime(1800000);
        dataSource = new HikariDataSource(config);
    }

    @Benchmark
    public Object getConnection(Blackhole blackhole) throws SQLException {
        try (Connection conn = dataSource.getConnection()) {
            blackhole.consume(conn);
            return conn;
        }
    }

    @TearDown
    public void tearDown() {
        if (dataSource instanceof HikariDataSource) {
            ((HikariDataSource) dataSource).close();
        }
    }
}

 

性能测试指标:

指标 目标值 说明
吞吐量 >1000 ops/s 每秒获取连接次数
平均响应时间 <5ms 获取连接的平均时间
99%响应时间 <50ms 99%分位数响应时间
错误率 0% 获取连接失败率

5.6 常见错误场景与解决方案

连接池耗尽问题的常见错误场景及对应的解决方案:

场景一:连接池配置过小

 

# 错误配置
spring:
datasource:
    hikari:
      maximum-pool-size:5# 过小

# 正确配置
spring:
datasource:
    hikari:
      maximum-pool-size:50# 根据并发量调整

 

场景二:SQL执行时间过长

 

// 错误代码
try {
    conn = dataSource.getConnection();
    Thread.sleep(10000);  // 模拟慢SQL
    conn.createStatement().executeQuery("SELECT * FROM huge_table");
} finally {
    conn.close();
}

// 正确代码:优化SQL执行时间
// 1. 添加索引
// 2. 使用分页查询
// 3. 设置查询超时
PreparedStatement ps = conn.prepareStatement(sql);
ps.setQueryTimeout(30);  // 30秒超时

 

场景三:连接未关闭

 

// 错误代码
Connection conn = dataSource.getConnection();
executeQuery(conn);  // 使用后未关闭

// 正确代码:使用try-with-resources
try (Connection conn = dataSource.getConnection()) {
    executeQuery(conn);
}  // 自动关闭

 

场景四:数据库连接数限制

 

-- 检查MySQL最大连接数
SHOW VARIABLES LIKE 'max_connections';

-- 修改最大连接数(临时)
SET GLOBAL max_connections = 500;

-- 修改最大连接数(永久,需在my.cnf配置)
[mysqld]
max_connections = 500

 

总结

数据库连接池耗尽是影响应用可用性的严重问题。问题的根因通常包括连接泄漏、连接池配置不当、SQL执行慢、数据库连接数限制。

排查连接池问题需要结合应用层和数据库层的数据。应用层关注连接池监控和线程堆栈,数据库层关注会话信息和慢查询日志。数据综合分析才能定位真正原因。

优化工作需要从多个维度入手。代码层面确保连接正确释放,推荐使用try-with-resources;配置层面合理设置连接池参数,考虑业务特性和数据库能力;SQL层面消除性能瓶颈,添加必要索引;数据库层面确保资源配置充足,调整服务器参数。

预防胜于治疗。建议建立连接池监控告警机制,及时发现异常;定期分析慢查询日志,持续优化SQL性能;做好容量规划,确保连接池配置与业务规模匹配;建立连接泄漏检测机制,在泄漏早期发现并修复。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分