詭異的druid鏈接池鏈接斷開故障經驗總結

来源:https://www.cnblogs.com/wangiqngpei557/archive/2019/01/19/10291606.html
-Advertisement-
Play Games

最近在陸續做機房升級相關工作,配合DBA對產線資料庫鏈接方式做個調整,將原來直接鏈接讀庫的地址切換到統一的讀負載均衡的代理 haproxy 上,方便機櫃和伺服器的搬遷。 切換之後線上時不時的會發生 discard connection 錯誤,導致程式報 500 錯誤,但不是每次都必現的。 開發框... ...



  • 背景
  • 癥狀
  • 排查
  • 修複

背景

最近在陸續做機房升級相關工作,配合DBA對產線資料庫鏈接方式做個調整,將原來直接鏈接讀庫的地址切換到統一的讀負載均衡的代理 haproxy 上,方便機櫃和伺服器的搬遷。
切換之後線上時不時的會發生 discard connection 錯誤,導致程式報 500 錯誤,但不是每次都必現的。

開發框架: spring boot+mybatis+druid+shardingJDBC
網路架構:
appserver->mysql(master) 寫
appserver->haproxy->mysql(slave)/n 讀

第一反應肯定是因為這次的讀庫地址的變動引起的問題,覺得問題應該是 druid 鏈接池中的 connection 保活策略沒起作用,只要做下配置修改應該就可以了。結果這個問題讓我們排查了好幾天,我們竟然踩到了千年難遇的深坑。

這個問題排查的很坎坷,一次次的吐血,最終我們定位到問題並且優雅的修複了,我們一起來體驗下這個一次一次讓你絕望一次一次打臉的過程。

癥狀

先說故障癥狀,經常出現如下錯誤:

discard connection
com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

The last packet successfully received from the server was 72,557 milliseconds ago. The last packet sent successfully to the server was 0 milliseconds ago.

根據錯誤日誌初步判斷肯定是與 db 之間的鏈接已經斷開,嘗試使用了一個已經斷開的鏈接才會引起這個錯誤發生。但是根據我們對 druid 瞭解,druid 有鏈接檢查功能,按理不會拿到一個無效鏈接才對,帶著這個線索我們上路了。

排查

為了準確的知道 db 的鏈接的存活時間,瞭解到 haproxy 對轉發的 db tcp 鏈接空閑時間在 1m 之內,超過 1m 不活動就會被關掉。也就說我們與 db 之間的原來的長鏈接在 1m 之內會被斷開。我們先不管這個時間設置的是否符合所有的大併發場景,至少在 druid 的鏈接池裡會有有效鏈接檢查,應該不會拿到無效鏈接才對,我們做了配置調整。

我們看下 druid 跟鏈接時間相關的配置:

datasource.druid.validationQuery=SELECT 1
datasource.druid.validationQueryTimeout=2000
datasource.druid.testWhileIdle=true
datasource.druid.minEvictableIdleTimeMillis=100000
datasource.druid.timeBetweenEvictionRunsMillis=20000

配置的每項的意思這裡就不解釋了。
我們啟用了 testWhileIdle 配置,讓每次拿取鏈接的時候發起檢查。根據 timeBetweenEvictionRunsMillis 的配置只有大於這個時間 druid 才會發起檢查,所以可能的場景是拿到一個即將過期的鏈接,根據這個線索我們調整這個時間為 20000ms,也就是超過 20s 會檢查當前拿取的鏈接確定是否有效,檢查的方式應該是使用 validationQuery 配置的 sql 語句才對,但是發現我們並找不到任何有關於 SELECT 1 的痕跡。

為什麼你死活找不到 SELECT 1

首先要搞清楚 validationQuery 為什麼沒起作用,帶著這個疑問開始 debug druid 源碼。

if (isTestWhileIdle()) {
                    final long currentTimeMillis = System.currentTimeMillis();
                    final long lastActiveTimeMillis = poolableConnection.getConnectionHolder().getLastActiveTimeMillis();
                    final long idleMillis = currentTimeMillis - lastActiveTimeMillis;
                    long timeBetweenEvictionRunsMillis = this.getTimeBetweenEvictionRunsMillis();
                    if (timeBetweenEvictionRunsMillis <= 0) {
                        timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
                    }

                    if (idleMillis >= timeBetweenEvictionRunsMillis) {
                        boolean validate = testConnectionInternal(poolableConnection.getConnection());
                        if (!validate) {
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("skip not validate connection.");
                            }

                            discardConnection(realConnection);
                            continue;
                        }
                    }
                }
            }

閑置時間肯定會有大於 timeBetweenEvictionRunsMillis 時間的,會發起 testConnectionInternal 方法檢查。我們繼續跟進去看,

protected boolean testConnectionInternal(DruidConnectionHolder holder, Connection conn) {
   boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);
   

內部會使用 validConnectionChecker 檢查對象發起檢查。

public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
        if (conn.isClosed()) {
            return false;
        }

        if (usePingMethod) {
            if (conn instanceof DruidPooledConnection) {
                conn = ((DruidPooledConnection) conn).getConnection();
            }

            if (conn instanceof ConnectionProxy) {
                conn = ((ConnectionProxy) conn).getRawObject();
            }

            if (clazz.isAssignableFrom(conn.getClass())) {
                if (validationQueryTimeout < 0) {
                    validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT;
                }

                try {
                    ping.invoke(conn, true, validationQueryTimeout * 1000);
                } catch (InvocationTargetException e) {
                    Throwable cause = e.getCause();
                    if (cause instanceof SQLException) {
                        throw (SQLException) cause;
                    }
                    throw e;
                }
                return true;
            }
        }

        String query = validateQuery;
        if (validateQuery == null || validateQuery.isEmpty()) {
            query = DEFAULT_VALIDATION_QUERY;
        }

        Statement stmt = null;
        ResultSet rs = null;
        try {
            stmt = conn.createStatement();
            if (validationQueryTimeout > 0) {
                stmt.setQueryTimeout(validationQueryTimeout);
            }
            rs = stmt.executeQuery(query);
            return true;
        } finally {
            JdbcUtils.close(rs);
            JdbcUtils.close(stmt);
        }

    }

debug 這裡才發現,druid 預設採用的是 mysql.ping 來做鏈接有效性檢查。

druid 預設採用msyql.ping 協議檢查

那是不是用 msyql.ping 協議並不會讓 mysql 重新滑動 session 閑置時間,帶著這個問題打開 information_schema.processlist 進程列表查看會不會刷新會話時間,通過 debug發現是會刷新時間的,說明沒有問題,這條線索算是斷了。

haproxy tiemout主動close上下游鏈接

調整方向,開始懷疑是不是 haproxy 的一些策略導致鏈接失效,開始初步懷疑 haproxy 的輪訓轉發後端鏈接是不是有相關會話保持方式,是不是我們配置有誤導致 haproxy 的鏈接和 mysql 鏈接篡位了。

當然這個猜想有點誇張,但是沒辦法,技術人員就要有懷疑一切的態度。

為了還原產線的網路路線,我在本地搭了一個 haproxy,瞭解下他的工作原理和配置,圖方便我就用了yum順手裝了一個,版本是 HA-Proxy version 1.5.18 不知道是我本地環境問題還是這個版本的 bug,我們配置的 mode tcp 活動檢查一直不生效。

listen service 127.0.0.1:60020
  mode tcp
  balance roundrobin
  option tcplog
  server server1 192.168.36.66:3306 check inter 2000 rise 2 fall 3
  server server2 192.168.36.66:3306 check inter 2000 rise 2 fall 3

由於 haproxy 活動檢查一直不通過,所以無法轉發我的鏈接,搞了半天我只能手動裝了一個低版本的 haproxy HA-Proxy version 1.4.14
完整的配置:

defaults
        mode tcp               
        retries 3              
        option redispatch      
        option abortonclose    
        maxconn 32000          
        timeout connect 2s 
        timeout client 5m 
        timeout server 5m 


listen test1
        bind 0.0.0.0:60000
        mode tcp
        balance roundrobin
        server s1 192.168.36.66:3306 weight 1 maxconn 10000 check inter 10s
        server s2 192.168.36.66:3306 weight 1 maxconn 10000 check inter 10s
        server s3 192.168.36.66:3306 weight 1 maxconn 10000 check inter 10s

1.4 的版本順利完成活動檢查。
我使用 haproxy 進行debug,調試下來也都沒有問題,也翻了下 haproxy 如何轉發鏈接的,內部通過會話的方式保持兩個鏈接的關係,如果是 tcp 長鏈接應該不會出現什麼問題。haproxyhttp 模式下有會話保持方式,tcp 應該是直接捆綁的方式,一旦到 timeout 時間會主動 closemysql 的鏈接,而且沒有出現篡位的問題。到這裡線索又斷了。

自定義 ValidConnectionChecker 埋點日誌

沒有辦法,只能試著埋點 druid 的檢查日誌,排查內部上一次的 check和報錯之間的時間差和 connectionId 是不是一致的。

public class MySqlValidConnectionCheckerDebug extends MySqlValidConnectionChecker {

    @Override
    public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) {
        
            Long connId = 0L;
            try {
                Field connField = ConnectionImpl.class.getDeclaredField("connectionId");
                connField.setAccessible(true);
                connId = (Long) connField.get(((ConnectionProxyImpl) conn).getConnectionRaw());
            } catch (Exception e) {
                log.error("valid connection error", e);
            } finally {
                log.info("valid connection ok. conn:" + connId);
            }

            return true;
}

為了拿到 connectionId 只能反射獲取,在本地debug下沒問題,能正常拿到 connectionId,但是發到驗證環境進行驗證的時候報錯了,覺得奇怪,仔細看了下原來開發環境的配置和驗證和生產的不一樣,開發環境沒有走讀寫分離。

驗證和生產都是使用了 mysqlreplication 的機制,所以導致我反射獲取的代碼報錯。

datasource.druid.url=jdbc:mysql:replication

通過debug發現,原來 __druid__的 connectionJDBC4Connection ,變成了 ReplicationConnection ,而且裡面包裝了兩個 connection ,一個 masterconnection ,一個 slaveconnection ,似乎問題有點浮現了。

通過debug發現 druid 的檢查還是會正常走到,當走到 ReplicationConnection 內部的時候 ReplicationConnection 有一個 currentConnection ,這個鏈接是會在 masterConnectionslaveConnection 之間切換,切換的依據是 readOnly 參數。

在檢查的時候由於 druid 並不感知上層的參數,readOnly 也就不會設置。所以走的是 masterConnection ,但是在程式里用的時候通過 springTransactionManagerreadOnly 傳播到了 ShardingJDBCShardingJDBC 在設置到 ReplicationConnection 上,最後導致真正在使用的時候其實使用的是 slaveConnection

找到這個問題之後去 druid github Issues 搜索了下果然有人提過這個問題,在高版本的 druid 中已經修複這個問題了。

修複

修複這個問題有兩個方法,第一個方法,建議升級 druid,裡面已經有 MySqlReplicationValidConnectionChecker 檢查器專門用來解決這個問題。第二個方法就是自己實現 ValidConnectionChecker 檢查器,但是會有在將來出現bug的可能性。

由於時間關係文章只講了主要的排查路線,事實上我們陸續花了一周多時間,再加上周末連續趴上十幾個小時才找到這根本問題。

這個問題之所以難定位的原因主要是牽扯的東西太多,框架層面、網路鏈接層面、mysql伺服器層面,haproxy代理等等,當然其中也繞了很多彎路。。

下麵分享在這個整個排查過程中的一些技術收穫。

相關技術問題

1.mysqlConenction提供了ping方法用來做活動檢查,預設MySqlValidConnectionChecker使用的是pinginternal。

ping = clazz.getMethod("pingInternal", boolean.class, int.class);

2.低版本的druid不支持自定義 ValidConnectionChecker 來做個性化的檢查。

3.druid 的test方法使用註意事項,testOnBorrow 在獲取鏈接的時候進行檢查,與testWhileIdle是護持關係。

if (isTestOnBorrow()) {
            } else {
                if (isTestWhileIdle()) {

3.kill mysql processlist 進程會話到鏈接端tcp狀態有延遲,這是tcp的四次斷開延遲。

4.haproxy 1.5.18 版本 mode tcp check不執行,健康檢查設置無效。

5.mysql replication connection master/slave切換邏輯需要註意,會不會跟上下油的鏈接池組合使用出現bug,尤其是分庫不表、讀寫分離、自定義分片。

6.排查mysql伺服器的問題時,打開各種日誌,操作日誌,binlog日誌。

7.springtransactionmanagenent 事務傳播特性會影響下游數據源的選擇,setreadonly、setautocommit。

8.低版本的 druid MySqlValidConnectionChecker 永遠執行不到 ReplicationConnection ping 方法。

作者:王清培(滬江網資深架構師)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [toc] 首發日期:2019 01 19 前言: 有時候,一個後端開發者“不得不”自己去搭建前端界面。如果有的選,當然選一個比較好學的前端“框架”咯(框架很多時候封裝了普通的html元素,使得能更加方便地使用)。 如果你做的項目的界面是一個偏後臺管理的而且要求並不高的界面的時候,你可以考慮easy ...
  • 元素事件:滑鼠按下事件/滑鼠移動事件/滑鼠鬆開事件 元素樣式:讓元素脫離文檔流,採用絕對定位的方式。 一、滑鼠按下事件 當滑鼠在元素上面按下時,保存元素的初始偏移量和滑鼠按下時的坐標,然後在狀態變數裡面標記當前狀態為按下狀態。 二、滑鼠移動事件 當滑鼠拖動元素移動時,我們通過計算滑鼠從起始位到移動位 ...
  • 不知不覺我的第一個小程式已經上線一周了,uv也穩定的上升著。 很多人說我的小程式沒啥用,我默默一笑,心裡說:“它一直敦促我學習,敦促我進步”。我的以一個小程式初衷是經驗分享,目前先把經驗分享到博客園,邊學習邊完善小程式。同時我會持續學習,持續更新,功能定會一天天的完善起來。 歡迎大家掃碼體驗。 閑話 ...
  • 在結構上多一個指向自身的constructor構造函數,這就是原型鏈夠簡單吧. 利用原型鏈結構實現繼承和向鏈表中插入節點,有區別嗎? 沒區別!! ...
  • 先上效果 開發環境要求 需要事先安裝node及npm 前期準備 1.創建文件夾react-echarts-editor2.在項目根目錄(以下稱根目錄)下創建src目錄3.在項目根目錄下創建dist目錄4.在src目錄下創建app.js文件(該文件就來一個react-echarts版的hello wo ...
  • 前言 此內容是個人學習筆記,以便日後翻閱。 非教程,如有錯誤還請指出 Webpack 打包文件 支持JS模塊化 模式: production(0配置預設), development(生產環境) 更詳細的請前往 webpack官網 安裝 npm i webpack webpack cli D 執行 w ...
  • 寫這篇文章之前,關於ubuntu14.04(Trusty)預設安裝的NodeJS版本是0.10.25百思不解(什麼鬼,哪一年的NodeJS) 寫這篇文章之時,NodeJS的LTS版本號都已經10.15.0,當然Ubuntu在2018年也都發行ubuntu18.04(我還沒打算用) 系統我可以用4... ...
  • Cropper.js是一款很好用的圖片裁剪工具,可以對圖片的尺寸、寬高比進行裁剪,滿足諸如裁剪頭像上傳、商品圖片編輯之類的需求。 github: https://github.com/fengyuanchen/cropperjs 網站: https://fengyuanchen.github.io/ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...