MySQL使用ReplicationConnection導致連接失效解決
引言
MySQL數據庫讀寫分離,是提高服務質量的常用手段之一,而對於技術方案,有很多成熟開源框架或方案,例如:sharding-jdbc、spring中的AbstractRoutingDatasource、MySQL-Router等,而mysql-jdbc中的ReplicationConnection亦可支持。
本文暫不對讀寫分離的技術選型做過多的分析,隻是探索在使用druid作為數據源、結合ReplicationConnection做讀寫分離時,連接失效的原因,並找到一個簡單有效的解決方案。
問題背景
由於歷史原因,某幾個服務出現連接失效異常,關鍵報錯如下:
從日志不難看出,這是由於該連接長時間未和MySQL服務端交互,服務端已將連接關閉,典型的連接失效場景。
涉及的主要配置
jdbc配置
jdbc:mysql:replication://master_host:port,slave_host:port/database_name
druid配置
testWhileIdle=true(即,開啟瞭空閑連接檢查);
timeBetweenEvictionRunsMillis=6000L(即,對於獲取連接的場景,如果某連接空閑時間超過1分鐘,將會進行檢查,如果連接無效,將拋棄後重新獲取)。
附:DruidDataSource.getConnectionDirect中
處理邏輯如下:
if (testWhileIdle) { final DruidConnectionHolder holder = poolableConnection.holder; long currentTimeMillis = System.currentTimeMillis(); long lastActiveTimeMillis = holder.lastActiveTimeMillis; long lastExecTimeMillis = holder.lastExecTimeMillis; long lastKeepTimeMillis = holder.lastKeepTimeMillis; if (checkExecuteTime && lastExecTimeMillis != lastActiveTimeMillis) { lastActiveTimeMillis = lastExecTimeMillis; } if (lastKeepTimeMillis > lastActiveTimeMillis) { lastActiveTimeMillis = lastKeepTimeMillis; } long idleMillis = currentTimeMillis - lastActiveTimeMillis; long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis; if (timeBetweenEvictionRunsMillis <= 0) { timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS; } if (idleMillis >= timeBetweenEvictionRunsMillis || idleMillis < 0 // unexcepted branch ) { boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn); if (!validate) { if (LOG.isDebugEnabled()) { LOG.debug("skip not validate connection."); } discardConnection(poolableConnection.holder); continue; } } }
mysql超時參數配置wait_timeout=3600(3600秒,即:如果某連接超過一個小時和服務端沒有交互,該連接將會被服務端kill)。 顯而易見,基於如上配置,按照常規理解,不應該出現“The last packet successfully received from server was xxx,xxx,xxx milliseconds ago”的問題。(當然,當時也排除瞭人工介入kill掉數據庫連接的可能)。
當“理所應當”的經驗解釋不瞭問題所在,往往需要跳出可能浮於表面經驗束縛,來一次追根究底。那麼,該問題的真正原因是什麼呢?
本質原因
當使用druid管理數據源,結合mysql-jdbc中原生的ReplicationConnection做讀寫分離時,ReplicationConnection代理對象中實際存在master和slaves兩套連接,druid在做連接檢測時候,隻能檢測到其中的master連接,如果某個slave連接長時間未使用,會導致連接失效問題。
原因分析
mysql-jdbc中,數據庫驅動對連接的處理過程
結合com.mysql.jdbc.Driver源碼,不難看出mysql-jdbc中獲取連接的主體流程如下:
對於以“jdbc:mysql:replication://”開頭配置的jdbc-url,通過mysql-jdbc獲取到的連接,其實是一個ReplicationConnection的代理對象,默認情況下,“jdbc:mysql:replication://”後的第一個host和port對應master連接,其後的host和port對應slaves連接,而對於存在多個slave配置的場景,默認使用隨機策略進行負載均衡。
ReplicationConnection代理對象,使用JDK動態代理生成的,其中InvocationHandler的具體實現,是ReplicationConnectionProxy,關鍵代碼如下:
public static ReplicationConnection createProxyInstance(List<String> masterHostList, Properties masterProperties, List<String> slaveHostList, Properties slaveProperties) throws SQLException { ReplicationConnectionProxy connProxy = new ReplicationConnectionProxy(masterHostList, masterProperties, slaveHostList, slaveProperties); return (ReplicationConnection) java.lang.reflect.Proxy.newProxyInstance(ReplicationConnection.class.getClassLoader(), INTERFACES_TO_PROXY, connProxy); }
ReplicationConnectionProxy的重要組成
關於數據庫連接代理,ReplicationConnectionProxy中的主要組成如下圖:
ReplicationConnectionProxy存在masterConnection和slavesConnection兩個實際連接對象,currentConnetion(當前連接)可以切換成mastetConnection或者slavesConnection,切換方式可以通過設置readOnly實現。
業務邏輯中,實現讀寫分離的核心也在於此,簡單來說:使用ReplicationConnection做讀寫分離時,隻要做一個“設置connection的readOnly屬性的”aop即可。
基於ReplicationConnectionProxy,業務邏輯中獲取到的Connection代理對象,數據庫訪問時的主要邏輯是什麼樣的呢?
ReplicationConnection代理對象處理過程
對於業務邏輯而言,獲取到的Connection實例,是ReplicationConnection代理對象,該代理對象通過ReplicationConnectionProxy和ReplicationMySQLConnection相互協同完成對數據庫訪問的處理,其中ReplicationConnectionProxy在實現 InvocationHandler的同時,還充當對連接管理的角色,核心邏輯如下圖:
對於prepareStatement等常規邏輯,ConnectionMySQConnection獲取到當前連接進行處理(普通的讀寫分離的處理的重點正是在此);此時,重點提及pingInternal方法,其處理方式也是獲取當前連接,然後執行pingInternal邏輯。
對於ping()這個特殊邏輯,圖中描述相對簡單,但主體含義不變,即:對master連接和sleves連接都要進行ping()的處理。
圖中,pingInternal流程和druid的MySQ連接檢查有關,而ping的特殊處理,也正是解決問題的關鍵。
druid數據源對MySQ連接的檢查
druid中對MySQL連接檢查的默認實現類是MySqlValidConnectionChecker,其中核心邏輯如下:
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); } }
對應服務中使用的mysql-jdbc(5.1.45版),在未設置“druid.mysql.usePingMethod”系統屬性的情況下,默認usePingMethod為true,如下:
public MySqlValidConnectionChecker(){ try { clazz = Utils.loadClass("com.mysql.jdbc.MySQLConnection"); if (clazz == null) { clazz = Utils.loadClass("com.mysql.cj.jdbc.ConnectionImpl"); } if (clazz != null) { ping = clazz.getMethod("pingInternal", boolean.class, int.class); } if (ping != null) { usePingMethod = true; } } catch (Exception e) { LOG.warn("Cannot resolve com.mysql.jdbc.Connection.ping method. Will use 'SELECT 1' instead.", e); } configFromProperties(System.getProperties()); } @Override public void configFromProperties(Properties properties) { String property = properties.getProperty("druid.mysql.usePingMethod"); if ("true".equals(property)) { setUsePingMethod(true); } else if ("false".equals(property)) { setUsePingMethod(false); } }
同時,可以看出MySqlValidConnectionChecker中的ping方法使用的是MySQLConnection中的pingInternal方法,而該方法,結合上面對ReplicationConnection的分析,當調用pingInternal時,隻是對當前連接進行檢驗。執行檢驗連接的時機是通過DrduiDatasource獲取連接時,此時未設置readOnly屬性,檢查的連接,其實隻是ReplicationConnectionProxy中的master連接。
此外,如果通過“druid.mysql.usePingMethod”屬性設置usePingMeghod為false,其實也會導致連接失效的問題,因為:當通過valideQuery(例如“select 1”)進行連接校驗時,會走到ReplicationConnection中的普通查詢邏輯,此時對應的連接依然是master連接。
題外一問:ping方法為什麼使用“pingInternal”,而不是常規的ping?
原因:pingInternal預留瞭超時時間等控制參數。
解決方式
調整依賴版本
服務中使用的mysql-jdbc版本為5.1.45,druid版本為1.1.20。經過對其他高版本依賴的瞭解,依然存在該問題。
修改讀寫分離實現
修改的工作量主要在於數據源配置和aop調整,但需要一定的整體回歸驗證成本,鑒於涉及該問題的服務重要性一般,暫不做大調整。
拓展mysql-jdbc驅動
基於原有ReplicationConnection的功能,拓展pingInternal調整為普通的ping,集成原有Driver拓展新的Driver。方案可行,但修改成本不算小。
基於druid,拓展MySQL連接檢查
為簡單高效解決問題,選擇拓展MySqlValidConnectionChecker,並在druid數據源中加上對應配置即可。拓展如下:
public class MySqlReplicationCompatibleValidConnectionChecker extends MySqlValidConnectionChecker { private static final Log LOG = LogFactory.getLog(MySqlValidConnectionChecker.class); /** * */ private static final long serialVersionUID = 1L; @Override public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception { if (conn.isClosed()) { return false; } if (conn instanceof DruidPooledConnection) { conn = ((DruidPooledConnection) conn).getConnection(); } if (conn instanceof ConnectionProxy) { conn = ((ConnectionProxy) conn).getRawObject(); } if (conn instanceof ReplicationConnection) { try { ((ReplicationConnection) conn).ping(); LOG.info("validate connection success: connection=" + conn.toString()); return true; } catch (SQLException e) { LOG.error("validate connection error: connection=" + conn.toString(), e); throw e; } } return super.isValidConnection(conn, validateQuery, validationQueryTimeout); } }
ReplicatoinConnection.ping()的實現邏輯中,會對所有master和slaves連接進行ping操作,最終每個ping操作都會調用到LoadBalancedConnectionProxy.doPing進行處理,而此處,可在數據庫配置url中設置loadBalancePingTimeout屬性設置超時時間。
以上就是MySQL使用ReplicationConnection導致連接失效解決的詳細內容,更多關於MySQL Replication連接失效的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Spring Boot集成Druid出現異常報錯的原因及解決
- 低版本Druid連接池+MySQL驅動8.0導致線程阻塞、性能受限
- JDBC連接的六步實例代碼(與mysql連接)
- 解決JDBC連接Mysql長時間無動作連接失效的問題
- jdbc中自帶MySQL 連接池實踐示例