MySQL使用ReplicationConnection導(dǎo)致連接失效解決
引言
MySQL數(shù)據(jù)庫讀寫分離,是提高服務(wù)質(zhì)量的常用手段之一,而對于技術(shù)方案,有很多成熟開源框架或方案,例如:sharding-jdbc、spring中的AbstractRoutingDatasource、MySQL-Router等,而mysql-jdbc中的ReplicationConnection亦可支持。
本文暫不對讀寫分離的技術(shù)選型做過多的分析,只是探索在使用druid作為數(shù)據(jù)源、結(jié)合ReplicationConnection做讀寫分離時,連接失效的原因,并找到一個簡單有效的解決方案。
問題背景
由于歷史原因,某幾個服務(wù)出現(xiàn)連接失效異常,關(guān)鍵報錯如下:
從日志不難看出,這是由于該連接長時間未和MySQL服務(wù)端交互,服務(wù)端已將連接關(guān)閉,典型的連接失效場景。
涉及的主要配置
jdbc配置
jdbc:mysql:replication://master_host:port,slave_host:port/database_name
druid配置
testWhileIdle=true(即,開啟了空閑連接檢查);
timeBetweenEvictionRunsMillis=6000L(即,對于獲取連接的場景,如果某連接空閑時間超過1分鐘,將會進(jìn)行檢查,如果連接無效,將拋棄后重新獲取)。
附: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超時參數(shù)配置
wait_timeout=3600(3600秒,即:如果某連接超過一個小時和服務(wù)端沒有交互,該連接將會被服務(wù)端kill)。 顯而易見,基于如上配置,按照常規(guī)理解,不應(yīng)該出現(xiàn)“The last packet successfully received from server was xxx,xxx,xxx milliseconds ago”的問題。(當(dāng)然,當(dāng)時也排除了人工介入kill掉數(shù)據(jù)庫連接的可能)。
當(dāng)“理所應(yīng)當(dāng)”的經(jīng)驗(yàn)解釋不了問題所在,往往需要跳出可能浮于表面經(jīng)驗(yàn)束縛,來一次追根究底。那么,該問題的真正原因是什么呢?
本質(zhì)原因
當(dāng)使用druid管理數(shù)據(jù)源,結(jié)合mysql-jdbc中原生的ReplicationConnection做讀寫分離時,ReplicationConnection代理對象中實(shí)際存在master和slaves兩套連接,druid在做連接檢測時候,只能檢測到其中的master連接,如果某個slave連接長時間未使用,會導(dǎo)致連接失效問題。
原因分析
mysql-jdbc中,數(shù)據(jù)庫驅(qū)動對連接的處理過程
結(jié)合com.mysql.jdbc.Driver源碼,不難看出mysql-jdbc中獲取連接的主體流程如下:
對于以“jdbc:mysql:replication://”開頭配置的jdbc-url,通過mysql-jdbc獲取到的連接,其實(shí)是一個ReplicationConnection的代理對象,默認(rèn)情況下,“jdbc:mysql:replication://”后的第一個host和port對應(yīng)master連接,其后的host和port對應(yīng)slaves連接,而對于存在多個slave配置的場景,默認(rèn)使用隨機(jī)策略進(jìn)行負(fù)載均衡。
ReplicationConnection代理對象,使用JDK動態(tài)代理生成的,其中InvocationHandler的具體實(shí)現(xiàn),是ReplicationConnectionProxy,關(guān)鍵代碼如下:
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的重要組成
關(guān)于數(shù)據(jù)庫連接代理,ReplicationConnectionProxy中的主要組成如下圖:
ReplicationConnectionProxy存在masterConnection和slavesConnection兩個實(shí)際連接對象,currentConnetion(當(dāng)前連接)可以切換成mastetConnection或者slavesConnection,切換方式可以通過設(shè)置readOnly實(shí)現(xiàn)。
業(yè)務(wù)邏輯中,實(shí)現(xiàn)讀寫分離的核心也在于此,簡單來說:使用ReplicationConnection做讀寫分離時,只要做一個“設(shè)置connection的readOnly屬性的”aop即可。
基于ReplicationConnectionProxy,業(yè)務(wù)邏輯中獲取到的Connection代理對象,數(shù)據(jù)庫訪問時的主要邏輯是什么樣的呢?
ReplicationConnection代理對象處理過程
對于業(yè)務(wù)邏輯而言,獲取到的Connection實(shí)例,是ReplicationConnection代理對象,該代理對象通過ReplicationConnectionProxy和ReplicationMySQLConnection相互協(xié)同完成對數(shù)據(jù)庫訪問的處理,其中ReplicationConnectionProxy在實(shí)現(xiàn) InvocationHandler的同時,還充當(dāng)對連接管理的角色,核心邏輯如下圖:
對于prepareStatement等常規(guī)邏輯,ConnectionMySQConnection獲取到當(dāng)前連接進(jìn)行處理(普通的讀寫分離的處理的重點(diǎn)正是在此);此時,重點(diǎn)提及pingInternal方法,其處理方式也是獲取當(dāng)前連接,然后執(zhí)行pingInternal邏輯。
對于ping()這個特殊邏輯,圖中描述相對簡單,但主體含義不變,即:對master連接和sleves連接都要進(jìn)行ping()的處理。
圖中,pingInternal流程和druid的MySQ連接檢查有關(guān),而ping的特殊處理,也正是解決問題的關(guān)鍵。
druid數(shù)據(jù)源對MySQ連接的檢查
druid中對MySQL連接檢查的默認(rèn)實(shí)現(xiàn)類是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); } }
對應(yīng)服務(wù)中使用的mysql-jdbc(5.1.45版),在未設(shè)置“druid.mysql.usePingMethod”系統(tǒng)屬性的情況下,默認(rèn)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方法,而該方法,結(jié)合上面對ReplicationConnection的分析,當(dāng)調(diào)用pingInternal時,只是對當(dāng)前連接進(jìn)行檢驗(yàn)。執(zhí)行檢驗(yàn)連接的時機(jī)是通過DrduiDatasource獲取連接時,此時未設(shè)置readOnly屬性,檢查的連接,其實(shí)只是ReplicationConnectionProxy中的master連接。
此外,如果通過“druid.mysql.usePingMethod”屬性設(shè)置usePingMeghod為false,其實(shí)也會導(dǎo)致連接失效的問題,因?yàn)椋寒?dāng)通過valideQuery(例如“select 1”)進(jìn)行連接校驗(yàn)時,會走到ReplicationConnection中的普通查詢邏輯,此時對應(yīng)的連接依然是master連接。
題外一問:ping方法為什么使用“pingInternal”,而不是常規(guī)的ping?
原因:pingInternal預(yù)留了超時時間等控制參數(shù)。
解決方式
調(diào)整依賴版本
服務(wù)中使用的mysql-jdbc版本為5.1.45,druid版本為1.1.20。經(jīng)過對其他高版本依賴的了解,依然存在該問題。
修改讀寫分離實(shí)現(xiàn)
修改的工作量主要在于數(shù)據(jù)源配置和aop調(diào)整,但需要一定的整體回歸驗(yàn)證成本,鑒于涉及該問題的服務(wù)重要性一般,暫不做大調(diào)整。
拓展mysql-jdbc驅(qū)動
基于原有ReplicationConnection的功能,拓展pingInternal調(diào)整為普通的ping,集成原有Driver拓展新的Driver。方案可行,但修改成本不算小。
基于druid,拓展MySQL連接檢查
為簡單高效解決問題,選擇拓展MySqlValidConnectionChecker,并在druid數(shù)據(jù)源中加上對應(yīng)配置即可。拓展如下:
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()的實(shí)現(xiàn)邏輯中,會對所有master和slaves連接進(jìn)行ping操作,最終每個ping操作都會調(diào)用到LoadBalancedConnectionProxy.doPing進(jìn)行處理,而此處,可在數(shù)據(jù)庫配置url中設(shè)置loadBalancePingTimeout屬性設(shè)置超時時間。
以上就是MySQL使用ReplicationConnection導(dǎo)致連接失效解決的詳細(xì)內(nèi)容,更多關(guān)于MySQL Replication連接失效的資料請關(guān)注本站其它相關(guān)文章!
版權(quán)聲明:本站文章來源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請保持原文完整并注明來源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學(xué)習(xí)參考,不代表本站立場,如有內(nèi)容涉嫌侵權(quán),請聯(lián)系alex-e#qq.com處理。