數(shù)據(jù)庫連接池爆滿如何排查
1. 什么是連接池
現(xiàn)如今在做程序開發(fā)的時候,尤其是涉及到數(shù)據(jù)庫的時候,都會用連接池來管理數(shù)據(jù)庫連接。其中Java里面有比較出名Druid,以Go語言里面的Gorm框架也會自帶連接池管理。數(shù)據(jù)庫連接池,其實(shí)就是存儲數(shù)據(jù)庫連接的池子,本質(zhì)是一種資源復(fù)用技術(shù)。

2. 為什么要使用連接池
使用連接池的核心目的是提升應(yīng)用程序的性能和效率。建立和關(guān)閉數(shù)據(jù)庫連接的過程通常是非常消耗資源的,尤其是在高并發(fā)環(huán)境或頻繁訪問數(shù)據(jù)庫的場景中,這開銷會對應(yīng)用性能造成顯著的負(fù)面影響通過連接池,應(yīng)用程序可以重復(fù)利用已有的數(shù)據(jù)庫連接,減少了每次連接建立和銷毀的開銷,從而加快系統(tǒng)的響應(yīng)速度和提高了系統(tǒng)的吞吐能力。同時,連接池還能對連接數(shù)量進(jìn)行有效管理,防止因連接過多而導(dǎo)致數(shù)據(jù)庫過載或性能下降的問題,從而保證系統(tǒng)運(yùn)行的穩(wěn)定性和資源的合理利用。
3. 連接池常見參數(shù)
參數(shù)  | 含義  | 
max-active  | 最大連接數(shù)(默認(rèn)8)  | 
max-wait  | 獲取連接時的最大等待時間  | 
min-evictable-idle-time-millis  | 連接保持空閑而不被釋放的最小時間  | 
min-idle  | 最小連接池數(shù)量  | 
initial-size  | 連接池初始化時建立物理連接的個數(shù)  | 
time-between-eviction-runs-millis  | 配置間隔多久才進(jìn)行一次檢測,Destroy線程會檢測連接,如果連接空閑時間大于等于minEvictableIdleTimeMillis則關(guān)閉物理連接  | 
4. 連接池爆滿排查路徑
連接池的使用雖然可以帶來很大程度上性能的話優(yōu)化,但是在使用MySQL數(shù)據(jù)庫時,尤其是在高并發(fā)的場景下,數(shù)據(jù)庫連接數(shù)過多會導(dǎo)致連接池耗盡,進(jìn)而影響應(yīng)用程序的正常運(yùn)行。所以對于數(shù)據(jù)庫連接池爆滿問題的排查,也相當(dāng)重要,在排查這類問題的時候可以按照下圖的總體思路來進(jìn)行。

5. 案例分析
(1) 數(shù)據(jù)模擬
現(xiàn)有一張用戶表t_user,要對這張表的數(shù)據(jù)做一些查詢操作,表結(jié)構(gòu)如下:
CREATE TABLE `t_user` (
    `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵自增',
    `name` varchar(32) NOT NULL DEFAULT '' COMMENT '姓名',
    `create_time` varchar(32) NOT NULL DEFAULT '' COMMENT '創(chuàng)建日期,yyyy-MM-dd HH:mm:ss',
    `age` int(4) NOT NULL DEFAULT 0 COMMENT '年齡',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB CHARSET = utf8;插入兩條記錄:
insert into t_user (name,age,create_time) values ("zhangsan",18,"2024-11-20 12:07:46");
insert into t_user (name,age,create_time) values ("zlisi",20,"2024-11-20 12:07:46");結(jié)果如下:

(2) 工程模擬
假設(shè)現(xiàn)在有一個spring boot的web工程connPoolDemo,提供了兩個查詢接口,分別是根據(jù)ID查詢用戶全體信息以及根據(jù)ID查詢用戶的姓名。spring boot工程使用的連接池是Druid,工程配置application.yml如下:
server:
  port: 8080
spring:
  application:
    name: @artifactId@
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:8086/camps?serverTimeznotallow=GMT%2B8&characterEncoding=utf8&&allowMultiQueries=true&useSSL=false
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      web-stat-filter:
        # 是否啟用StatFilter默認(rèn)值true
        enabled: true
        # 添加過濾規(guī)則
        url-pattern: /*
        # 忽略過濾的格式
        exclusions: /druid/*,*.js,*.gif,*.jpg,*.png,*.css,*.ico
      stat-view-servlet:
        # 是否啟用StatViewServlet默認(rèn)值true
        enabled: true
        # 訪問路徑為/druid時,跳轉(zhuǎn)到StatViewServlet
        url-pattern: /druid/*
        # 是否能夠重置數(shù)據(jù)
        reset-enable: false
        # 需要賬號密碼才能訪問控制臺,默認(rèn)為root
        login-username: druid
        login-password: druid
        # IP白名單
        allow: 127.0.0.1
        # IP黑名單(共同存在時,deny優(yōu)先于allow)
      min-idle: 1                                          # 最小連接數(shù)
      max-active: 2                                        # 最大連接數(shù)(默認(rèn)8)
      max-wait: 1000                                       # 獲取連接時的最大等待時間
      min-evictable-idle-time-millis: 300000               # 一個連接在池中最小生存的時間,單位是毫秒
      time-between-eviction-runs-millis: 60000             # 多久才進(jìn)行一次檢測需要關(guān)閉的空閑連接,單位是毫秒
      connect-timeout: 10000    # 默認(rèn)是10s
      socket-timeout: 1100000   # 默認(rèn)是10s
mybatis:
  mapper-locations: classpath:mapper/*.xml
#開啟駝峰命名
  configuration:
    map-underscore-to-camel-case: false
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl注意需要把上述配置中的這兩個配置connect-timeout和socket-timeout配置大一些,因?yàn)閖dbc會根據(jù)這兩個配置來進(jìn)行發(fā)包測試,默認(rèn)是10s,如果超過10s,連接沒有響應(yīng),就會強(qiáng)行斷開連接。后面會模擬一個長連接占用的情況來占滿連接池,所以這里需要把這兩個參數(shù)設(shè)置的大一些。另外為了方便測試,我們把最大連接數(shù)配置小一點(diǎn),上述yml文件中最大連接數(shù)max-active配置為2。
(3) 模擬長連接占用
connPoolDemo對外提供兩個接口,工程的controller代碼如下:
package com.camps.connpooldemo.controller;
import com.camps.connpooldemo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.camps.connpooldemo.service.impl.ConnPoolServiceImpl;
@RestController
public class ConnPoolController {
    // 示例:使用 HikariCP 作為連接池
    @Autowired
    private ConnPoolServiceImpl connPoolServiceImpl;
    @GetMapping("/getUser")
    public User getUser(@RequestParam("id") Long userID) {
        return connPoolServiceImpl.getUser(userID);
    }
    @GetMapping("/getName")
    public String getName(@RequestParam("id") Long userID) {
        return connPoolServiceImpl.getName(userID);
    }
}都是提供get請求,一個是通過ID獲取用戶全量信息,一個是通ID獲取用戶名。再來看對應(yīng)的mapper代碼:
<?xml versinotallow="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.camps.connpooldemo.mapper.UserMapper">
    <resultMap id="userMap"type="com.camps.connpooldemo.model.User">
        <result column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="age" property="age"/>
        <result column="sleep_time" property="sleepTime"/>
        <result column="create_time" property="createTime"/>
    </resultMap>
    <sql id="Vo_Column_List">
        `id`,
        `name`,
        `age`,
        `sleep_time`,
        `create_time`
    </sql>
    <select id="getUser" resultType="com.camps.connpooldemo.model.User" resultMap="userMap">
        select * from t_user where id = #{id} limit 1;
    </select>
    <select id="getName" resultType="com.camps.connpooldemo.model.User" resultMap="userMap">
        select sleep(60) as sleep_time,name from t_user where id = #{id} limit 1;
    </select>
</mapper>注意在mapper.xml中,根據(jù)用戶ID來獲取用戶名的sql,select sleep(60),name from t_user where id = #{id} limit 1;在這條sql又一個sleep(60),讓這次查詢延遲60s執(zhí)行。用這個延遲執(zhí)行來模擬后臺的一些長連接占用情況,以致連接被長期占用,短時間內(nèi)無法釋放。
(4) 請求測試
啟動服務(wù),服務(wù)啟動成功,控制臺如下:

由于我們使用的是Druid,并且在application.yml文件中配置了Druid的可視化信息,所以我們可以通過頁面的形式來查看我們的連接情況,訪問如下地址登陸:http://localhost:8080/druid/login.html,先登錄。

登陸的用戶名和密碼就是我們在上述application.yml文件中配置的druid的用戶名和密碼:;

登陸之后點(diǎn)擊上面的數(shù)據(jù)源頁簽可以看到數(shù)據(jù)源的地址,連接等信息,這里我們重點(diǎn)關(guān)注連接信息:

可以看到最大連接數(shù)以及最小空閑連接數(shù)跟我們在application.yml配置的都是一樣的。
模擬請求:
在瀏覽器開兩個頁簽同時訪問http://localhost:8080/getName?id=1這個地址,即發(fā)起兩次獲取用戶名的請求。

可以看到這兩個頁面都處于等待轉(zhuǎn)圈等待的情況,因?yàn)楂@取姓名這個請求在后臺在Mysql執(zhí)行查詢的時候需要延遲60s在執(zhí)行,會處于等待狀態(tài)同時瀏覽器打開第三個頁簽發(fā)起獲取用戶信息的請求:http://localhost:8080/getUser?id=1,可以看到頁面直接報錯。

等待60s之后,前兩個請求用戶名的請求此時返回了數(shù)據(jù)zhangsan,如下圖所示:

(5) 連接池爆滿現(xiàn)象識別
看后臺程序,控制臺報錯如下:

獲取連接池超時,可以初步斷定問題為連接池滿了,獲取不到連接所致。
(6) 根因分析
進(jìn)一步分析sql情況,查看當(dāng)前數(shù)據(jù)庫連接的session情況,數(shù)據(jù)庫執(zhí)行以命令:
show processlist;
可以看到當(dāng)前兩條連接,都是執(zhí)行的查詢操作,并且分別執(zhí)行了45s和41s都是屬于慢查詢,再結(jié)合前面啟動服務(wù)時我們在頁面觀測到的當(dāng)前服務(wù)配置的最大連接數(shù)是2,所以這兩個慢查詢就將連接池里的連接用完了,并且短時間內(nèi)不會釋放,所以在第三個請求查詢用戶全量信息的發(fā)起的時候,會獲取不到連接,報了獲取連接超時錯誤。
(7) 方案優(yōu)化
從上面的分析我們就知道了報錯的根本原因就是連接池連接耗盡導(dǎo)致的。而且連接池的最大連接數(shù)配置的是2,很小,這樣優(yōu)化起來就很好做了,我們增大連接池的連接配置,重啟服務(wù)再次測試,現(xiàn)連接調(diào)整為100,配置完重啟可以看到線上顯示druid的連接池最大連接已經(jīng)是100了:

接下來,再次重復(fù)上面的測試,在兩個頁簽中發(fā)起獲取用戶名的請求:http://localhost:8080/getName?id=1,在第三個頁簽中發(fā)起獲取用戶全量信息的請求:http://localhost:8080/getUser?id=1。此時可以看到前兩個頁面跟上次測試結(jié)果一樣,依舊是在等待:

而第三個頁面馬上返回了用戶信息,結(jié)果如下:

過一分鐘后前兩個頁面也會收到返回信息:zhagnsan,跟上次測試一樣。雖然這里通過調(diào)大數(shù)據(jù)庫連接池的配置參數(shù),使問題得到了初步的解決。這里可以進(jìn)一步分析,通過前面執(zhí)行show processlist;查看數(shù)據(jù)庫連接信息的時候,我們發(fā)現(xiàn)兩條sql長時間占用連接,導(dǎo)致連接不能有效釋放,使得連接池被占滿,導(dǎo)致第三個請求才一直獲取不到連接而報錯,所以這里的這兩條sql也是需要優(yōu)化的,具體分析show processlist的結(jié)果,初步看后面的sql語句。

再去代碼中確認(rèn),看下獲取用戶名請求對應(yīng)的db曾操作,查看mapper.xml在select語句執(zhí)行有個sleep(60)的操作,所以這里很明顯會慢,導(dǎo)致延遲60s后才會執(zhí)行查詢操作。所以把這個地方去掉就可以了。這里只是個demo,為了展示連接池如何優(yōu)化來做的,到具體的業(yè)務(wù)場景下,這里的慢sql分析還需要借助explain分析工具來進(jìn)行分析。再去代碼中確認(rèn),看下獲取用戶名請求對應(yīng)的db曾操作,查看mapper.xml。

確實(shí)在查詢的時候,有一個sleep的操作,去掉這個sleep(60)后,mapper.xml文件如下:
<?xml versinotallow="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.camps.connpooldemo.mapper.UserMapper">
    <resultMap id="userMap"type="com.camps.connpooldemo.model.User">
        <result column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="age" property="age"/>
        <result column="sleep_time" property="sleepTime"/>
        <result column="create_time" property="createTime"/>
    </resultMap>
    <sql id="Vo_Column_List">
        `id`,
        `name`,
        `age`,
        `sleep_time`,
        `create_time`
    </sql>
    <select id="getUser" resultType="com.camps.connpooldemo.model.User" resultMap="userMap">
        select * from t_user where id = #{id} limit 1;
    </select>
    <select id="getName" resultType="com.camps.connpooldemo.model.User" resultMap="userMap">
        select name from t_user where id = #{id} limit 1;
    </select>
</mapper>重啟服務(wù)再次進(jìn)行測試,前兩個頁面發(fā)起請求后可以立刻獲得返回數(shù)據(jù)zhangsan,第三個頁發(fā)起請求也可以馬上獲得用戶的全量信息。
6. 小結(jié)
數(shù)據(jù)庫連接池是現(xiàn)在后端開發(fā)中必然會使用的一項(xiàng)池化技術(shù),主要是通過對數(shù)據(jù)庫的連接進(jìn)行管理和復(fù)用,以此來減少頻繁的連接創(chuàng)建,從而減少不必要的性能消耗。但是隨著并發(fā)量的提高,連接池在使用上也很容易出現(xiàn)一些問題,最常見的就是連接耗盡,導(dǎo)致請求獲取不到連接而報錯。所以掌握好數(shù)據(jù)庫連接池問題的排查是非常有必要的,同時我們還應(yīng)當(dāng)做好數(shù)據(jù)庫的一些性能監(jiān)控,這樣對于連接的使用情況就能比較清晰的觀測到,這樣在要出現(xiàn)問題的時候就會發(fā)出告警,從而可以進(jìn)行及時干預(yù),避免出現(xiàn)線上問題。















 
 
 







 
 
 
 