一次數據庫鏈接池優化的實踐剖析

問題背景:MySQL 線程數只升不降

一段時間以來,XXX 部門開放平臺 OPENXXX 系統在業務高峯頻繁出現 MySQL 線程數升高的現象。升高自己不是問題,問題是隨着業務高峯過去,QPS 下來後 ,MySQL 線程數卻依然居高不下,這是什麼緣由?java

思考方向上,你們都知道,MySQL 是經過線程池來進行線程管理的,基於過往經驗,上述狀況極可能是線程池的配置策略不合理致使線程建立後沒法及時釋放,而實際上線程的利用率是很低的————這一點經過分析系統線程也能夠看到,waiting 態線程佔據 MySQL 總線程數的一半有餘(見下圖)。mysql

落地實踐上,方向雖然是明確的,但具體是 MySQL 的哪一項策略配置不合理、又該作怎樣的調整,須要作細緻的調研分析才能回答。由此發起 MySQL 線程的優化治理專項。sql

clipboard.png

追根溯源:問題根源的分析定位

對比業務高峯先後的 MySQL 線程,發現飆升的主要是 [MySQL Statement Cancellation Timer] ,由此引出第一階段問題,[MySQL Statement Cancellation Timer] 線程是從哪裏來的?數據庫

1、Timer 線程生命週期

走讀代碼流程,梳理獲得 Timer 線程的生命週期,以下圖所示(Timer 節點以及問題節點已標識)——網絡

clipboard.png

2、生命週期詳細解讀

Timer 線程建立鏈路

dump 現場線程,配合線程 stack 走讀 mysql connector jar 的代碼。session

clipboard.png
一、定位代碼,java.util.TimerThread#run—— TimerThread 是 mysql-connector-java-xxx.jar 中的 Timer 的一個內部類,等待 Timer 隊列中的任務以執行、調度mybatis

clipboard.png
二、順藤摸瓜,能夠追到 [MySQL Statement Cancellation Timer] 線程的生成鏈路app

clipboard.png
com.mysql.jdbc.ConnectionImpl#getCancelTimer異步

clipboard.png

三、查看 getCancelTimer 的上游調用 ,主要是 mysql-connector-java-xxx.jar 中的主管 sql 查詢的 Statementasync

clipboard.png
com.mysql.jdbc.StatementImpl#executeQuery

clipboard.png
小結

走讀 [MySQL Statement Cancellation Timer] 線程的調用鏈邏輯,能夠抽象 3 點核心信息——

  • Timer 線程是 mysql connection 鏈接維度的
  • 應用開啓 mysql 的 queryTimeouts 且 timeoutInMillis != 0 的話,伴隨每個鏈接的建立,都會同步開啓一個 Timer 線程,以進行超時控制
  • Timer 線程即是以前經過 jstack 抓取到的 DB 異常線程 [MySQL Statement Cancellation Timer]

能夠推斷 OPENXXX 應用一定開啓了 queryTimeout。查看 mybatis-config.xml,肯定在每次 DB 查詢的時候,均插上了 queryTimeout——defaultStatementTimeout 設置對全局 sql 生效,包括 insert、select、update

clipboard.png

Timer 線程銷燬鏈路

jdk 規範保證,任何線程都有自身的退出機制。查看 Statement 中 cancelTask 的執行過程,依次追溯。

一、com.mysql.jdbc.StatementImpl.CancelTask#run——調用 Connection 進行 cancel
clipboard.png

二、com.mysql.jdbc.ConnectionImpl#close

clipboard.png

三、com.mysql.jdbc.ConnectionImpl#realClose——關閉 Timer 線程

clipboard.png
小結

至此獲取到 [MySQL Statement Cancellation Timer] 線程的 cancel 鏈路,走讀代碼邏輯,抽象核心信息——鏈接關閉時,會調用 Connection.close 方法 cancel 掉 Timer 線程,即 [MySQL Statement Cancellation Timer] 線程。

3、核心問題總結定位

鏈接建立時,queryTimeout 會使 jdbc driver 新建 cancelTask 並使用 Timer 進行調度,一旦 sql 查詢超時則執行 cancel 動做;鏈接關閉時,調用 Connection 以 cancel 掉 Timer 線程。

問題來到第二個階段:既然鏈接超時關閉的時候,纔會將 Timer 線程 cancel 掉,那麼控制超時的具體是哪些策略呢?

超時策略:MySQL 線程數降低的關鍵

對於選型關係型數據庫的應用而言,數據庫的鏈接關閉策略自上而下由兩層組成:一、JDBC;二、Mysql,經由各層的一系列超時參數進行控制。須要注意的是,網絡文檔對各層各參數的釋義大多不夠精準,甚至相互矛盾。如下參數分析均來自官方文檔,並隨載官方連接以便詳細查閱。

1、JDBC 層(c3p0)

鏈接超時參數

  • maxIdleTime:在從池中剔除鏈接以前,容許鏈接閒置多少秒

有效性檢測參數

  1. idleConnecnTestPeriod:定時檢測池中空閒鏈接的週期,用以校驗鏈接的有效性
  2. testConnectionOnCheckin:鏈接提交時,異步校驗其有效性
  3. testConnectionOnCheckout:鏈接回收時,同步校驗其有效性
  4. preferredTestQuery:鏈接的有效性校驗語句。JDBC4 的鏈接包括一個名爲 isvalid()的方法,該方法可做爲快速、可靠的鏈接測試來執行

解讀一下

  1. testConnectionOnCheckin 是一個異步檢測參數而非同步參數,connection 提交指的是 connection 鏈接到 mysql server 而非執行任務
  2. 3個檢測參數是搭配使用的——鏈接提交後,會開啓鏈接狀態的按期檢測機制(testConnectionOnCheckin 爲 true),即每 30 秒(idleConnectionTestPeriod=30)經過 SELECT 1(preferredTestQuery=SELECT 1)語句檢測一次鏈接狀態,從而下降 Communications link failure 的發生機率

關於參數的 Q&A

設置完鏈接超時參數 maxIdleTime 以後,有必要設置有效性檢測參數麼——二者的關係是:鏈接空閒超過 maxIdleTime 後,就會被 mysql server 斷開。但此時鏈接池並無回收這個鏈接,直到鏈接池檢測到該鏈接已被廢棄後,纔會進行回收。在這個時間段內,若是客戶端使用了這個鏈接,就會報錯:Communications link failure。

clipboard.png

2、DB Server 層

wait_timeout:mysql server 關閉鏈接以前,容許鏈接閒置多少秒。默認是 28800,單位秒,即 8 個小時

3、超時策略探究總結

既然 jdbc 層面以及 mysql 層面都有完備的鏈接關閉策略,那麼問題來到第三個階段:OPENXXX 系統自身的配置策略是怎樣的?

系統分析:OPENXXX 的超時關閉策略

依據上文調研的鏈接關閉策略,摸查 OPENXXX 應用,一、JDBC;二、Mysql。

1、JDBC 層(c3p0)

OPENXXX 在 jdbc 層面未配置鏈接關閉策略(無 maxIdleTime),如此一來,只能依賴下層 mysql 的 timeout 機制進行鏈接的關閉。但實際上,mysql server 可否關掉鏈接呢?

2、DB Server 層

一、查詢 mysql server 的 wait_timeout 參數,觀察 DB 設定的鏈接超時配置——[select variable_name,variable_value from information_schema.session_variables where variable_name like 'wait_timeout']

clipboard.png

二、查詢 mysql server 的 Threads_connected 參數,觀察 DB 當前打開的鏈接數——[show status where variable_name = 'Threads_connected']

clipboard.png

三、查詢 DB 平常的 qps

clipboard.png

四、彙總信息:connectionSize~800,qps~800,keepAliveTime~28800s。由此計算線程釋放的機率:(qps keepAliveTime) / connectionSize,即 80028800/800=28800——意味着每一個鏈接在關閉以前,有 28800 次機會拿到任務而不被終止。這種機率下,鏈接是不可能釋放的,鏈接空置率也會很高。

3、系統分析判斷印證

  • 查詢 DB 平常的總鏈接數,能夠看到鏈接沒法主動釋放

clipboard.png

  • 查詢 DB 當前全部鏈接的運行狀況,能夠看到鏈接空置率很高,絕大多數處於 idle 狀態——[show full processlist] 查看,其中 Command 標識鏈接的運行狀態,好比:Sleep,Query,Connect 等

clipboard.png

解決方案:恰當的超時關閉策略

經過配置 jdbc 層的鏈接關閉策略,及時關掉空閒鏈接,從而確保 timer 線程的 cancle。問題來到第四個階段:如何配置 OPENXXX 的鏈接關閉策略?

1、建議方案

實際上,官方已經給出了建議

The most reliable time to test Connections is on check-out. But this is also the most costly choice from a client-performance perspective. Most applications should work quite reliably using a combination of idleConnectionTestPeriod and testConnectionOnCheckin. Both the idle test and the check-in test are performed asynchronously, which can lead to better performance, both perceived and actual.

最可靠的鏈接測試時機是在 connection 回收時進行(testConnectionOnCheckout),但從系統性能的角度來看,這也是最耗費性能的選擇。大多數應用程序應該組合使用 idleConnectionTestPeriod 和 testconConnectionCheckin,一方面能夠保證系統很是可靠地運行,另外一方面空閒測試和提交測試都是異步執行的,這會帶來更好的系統性能。

Set idleConnectionTestPeriod to 30, fire up you application and observe. This is a pretty robust setting, all Connections will tested on check-in and every 30 seconds thereafter while in the pool. Your application should experience broken or stale Connections only very rarely, and the pool should recover from a database shutdown and restart quickly

將 idleConnectionTestPeriod 設置爲 30,啓動系統並觀察。這是一個很是健壯的設置,全部鏈接都將在提交時進行測試,以後每隔 30 秒在池中進行一次測試。這樣應用程序能夠不多拿到斷開或過期的鏈接,而且能夠在 DB 重啓以後支持鏈接的快速恢復。

2、策略配置

<property name="preferredTestQuery">SELECT 1</property> <!-- 有效性檢測語句 -->
<property name="testConnectionOnCheckin">true</property> <!-- 提交鏈接時校驗鏈接的有效性。Default: false -->=
<property name="idleConnectionTestPeriod">30</property> <!-- 每 30 秒檢查鏈接池中的空閒鏈接。若爲 0 則永不檢測。Default: 0 -->
<property name="maxIdleTime">30</property> <!-- 最大空閒時間,30 秒內未使用鏈接被丟棄。若爲 0 則永不丟棄。Default: 0 -->

測試驗證:超時策略的有效性驗證

test 環境進行測試,驗證配置策略的有效性,三步走:
一、高 qps,mock db 流量,重複發起 query 請求——觀察 cat 堆棧,是否生成大量的 [MySQL Statement Cancellation Timer]
二、低 qps,mock db 流量,間隔發起 query 請求——觀察 cat 堆棧,是否開始縮減 [MySQL Statement Cancellation Timer]
三、無 qps,關閉 db 流量——觀察 cat 堆棧,無 [MySQL Statement Cancellation Timer]

1、測試代碼展現

@Controller
@RequestMapping("/dbtimer")
public class DBTimerController {
    @Resource
    private PushCallbackService callbackService;
    @Autowired
    private MccClient mccClient;
    private static final Logger LOGGER = LoggerFactory.getLogger(DBTimerController.class);
    private static final ExecutorService executorService = Executors.newFixedThreadPool(50);//建立線程池

    @ResponseBody
    @RequestMapping(value = "/dbtest", method = RequestMethod.GET)
    @Ignore("工具接口,無需鑑權")
    public void dbTest() {
        TimerQ timerQ = new TimerQ();
        for (int i = 0; i < 50; i++) {
            executorService.execute(new TimerR(timerQ));
        }
    }

    @ResponseBody
    @RequestMapping(value = "/dbtestdown", method = RequestMethod.GET)
    @Ignore("工具接口,無需鑑權")
    public void dbTestDown() {
        executorService.shutdown();
    }

    class TimerQ {
        public void queryTimer() throws InterruptedException {
            int i = 1;
            while (Boolean.valueOf(mccClient.getValue("mcc_timer_query_switch"))) {
                CallbackLog callbackLog = callbackService.querybyid(i);
                if (callbackLog==null) {
                    continue;
                }
                LOGGER.warn("query timer, callbackInfo:{}", callbackLog.getId());
                ++i;
                if (Boolean.valueOf(mccClient.getValue("mcc_timer_sleep_switch"))) {
                    Thread.sleep(Long.valueOf(mccClient.getValue("mcc_timer_time_switch")));
                }
            }
        }
    }

    class TimerR implements Runnable {
        private TimerQ timerQ;

        public TimerR(TimerQ timerQ) {
            this.timerQ = timerQ;
        }

        @Override
        public void run() {
            try {
                timerQ.queryTimer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
  
}

2、3 重場景驗證

一、初始階段
MySQL Statement Cancellation Timer 線程數爲 0
二、高 qps——生成大量的 [MySQL Statement Cancellation Timer]
MySQL Statement Cancellation Timer 線程數爲 50
三、低 qps——[MySQL Statement Cancellation Timer] 開始縮減
MySQL Statement Cancellation Timer 線程數爲 35
四、無 qps——無 [MySQL Statement Cancellation Timer]
MySQL Statement Cancellation Timer 線程數爲 0

3、上線效果展現

詳見 OPENXXX 系統 DB 異常線程優化案——總結報告。一句話總結:DB 鏈接超時策略的引入,能夠及時有效的關閉鏈接,進而關閉 [MySQL Statement Cancellation Timer],使得 OPENXXX 系統線程表現出了良好的業務彈性,且未損失原有的 sql 性能。

我的總結:抽象提煉優化方法

本次問題的表象是明確的,但掩藏的內核是艱深的。歷經「三方包代碼(原理)— jdbc(c3p0 文檔)— mysql server(manual 文檔) — openXXX(分析) — 測試(驗證)」,我的盡力呈現本次優化實踐從調研到上線的完整過程,亦收穫良多。同時在這裏抽象、提煉一下,主要是我的對於 DB 線程調優的提綱式整理,方便各位同窗進行參考,尋找優化思路——
clipboard.png

相關文章
相關標籤/搜索