Rumah  >  Artikel  >  pangkalan data  >  Bagaimana untuk menyelesaikan masalah kegagalan sambungan yang disebabkan oleh MySQL menggunakan ReplicationConnection

Bagaimana untuk menyelesaikan masalah kegagalan sambungan yang disebabkan oleh MySQL menggunakan ReplicationConnection

PHPz
PHPzke hadapan
2023-05-26 13:10:321430semak imbas

Pengenalan

Pemisahan baca-tulis pangkalan data MySQL ialah salah satu kaedah biasa untuk meningkatkan kualiti perkhidmatan Bagi penyelesaian teknikal, terdapat banyak rangka kerja atau penyelesaian sumber terbuka yang matang, seperti: sharding-jdbc, AbstractRoutingDatasource. pada musim bunga, MySQL-Router, dsb., dan ReplicationConnection dalam mysql-jdbc juga boleh menyokongnya.

Artikel ini tidak akan melakukan terlalu banyak analisis mengenai pemilihan teknikal pemisahan baca-tulis Ia hanya meneroka sebab kegagalan sambungan apabila menggunakan druid sebagai sumber data dan menggabungkannya dengan ReplicationConnection untuk pemisahan baca-tulis. dan mencari penyelesaian yang mudah dan berkesan.

Latar belakang masalah

Disebabkan sebab sejarah, perkhidmatan tertentu mengalami pengecualian kegagalan sambungan. Laporan ralat utama adalah seperti berikut:

Bagaimana untuk menyelesaikan masalah kegagalan sambungan yang disebabkan oleh MySQL menggunakan ReplicationConnection

Ia. boleh disimpulkan daripada log Ini kerana sambungan tidak berinteraksi dengan pelayan MySQL untuk masa yang lama, menyebabkan pelayan menutup sambungan, yang merupakan kegagalan sambungan biasa.

Konfigurasi utama terlibat

konfigurasi jdbc

jdbc:mysql:replication://master_host:port,slave_host:port/database_name

konfigurasi druid

testWhileIdle=true (iaitu pemeriksaan sambungan melahu didayakan); senario mendapatkan sambungan, jika sambungan melahu selama lebih daripada 1 minit, ia akan diperiksa Jika sambungan tidak sah, ia akan dibuang dan diperoleh semula).

Lampiran: Logik pemprosesan

dalam DruidDataSource.getConnectionDirect adalah seperti berikut:
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;
        }
    }
}

konfigurasi parameter tamat masa mysql masa tunggu 360_0. 3600 saat, iaitu : Jika sambungan tidak berinteraksi dengan pelayan selama lebih daripada sejam, sambungan akan dimatikan oleh pelayan). Jelas sekali, berdasarkan konfigurasi di atas, mengikut pemahaman konvensional, masalah "Paket terakhir yang berjaya diterima daripada pelayan ialah xxx,xxx,xxx milisaat yang lalu" tidak sepatutnya berlaku. (Sudah tentu, kemungkinan campur tangan manual untuk mematikan sambungan pangkalan data juga diketepikan pada masa itu).

Apabila pengalaman "sepatutnya" tidak dapat menjelaskan masalah, selalunya perlu untuk melompat keluar dari belenggu pengalaman cetek dan sampai ke bahagian bawahnya. Jadi, apakah punca sebenar masalah ini? Sebab penting

Apabila druid digunakan untuk mengurus sumber data dan digabungkan dengan ReplicationConnection asli dalam mysql-jdbc untuk pemisahan baca-tulis, sebenarnya terdapat dua set sambungan, tuan dan hamba, dalam objek proksi ReplicationConnection ialah Apabila melakukan pengesanan sambungan, hanya sambungan induk boleh dikesan Jika sambungan hamba tidak digunakan untuk masa yang lama, ia akan menyebabkan kegagalan sambungan.

Analisis sebab

Dalam mysql-jdbc, proses pemprosesan sambungan pemacu pangkalan data

Digabungkan dengan kod sumber com.mysql.jdbc.Driver, ia tidak sukar untuk dilihat bahawa apa yang diperolehi dalam mysql-jdbc Proses utama sambungan adalah seperti berikut:

Untuk jdbc-url yang dikonfigurasikan bermula dengan "jdbc:mysql:replication:// ", sambungan yang diperoleh melalui mysql-jdbc, sebenarnya Ia adalah objek proksi ReplicationConnection. Secara lalai, hos dan port pertama selepas "jdbc:mysql:replication://" sepadan dengan sambungan induk, dan hos berikutnya dan port sepadan dengan sambungan hamba Untuk berbilang konfigurasi hamba, senario, dasar rawak digunakan untuk pengimbangan beban secara lalai. Bagaimana untuk menyelesaikan masalah kegagalan sambungan yang disebabkan oleh MySQL menggunakan ReplicationConnection

Objek proksi ReplicationConnection dijana menggunakan proksi dinamik JDK Pelaksanaan khusus InvocationHandler ialah ReplicationConnectionProxy Kod utama adalah seperti berikut:

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);
 }

Komponen penting bagi pangkalan data ReplicationConnectionProxy

Abdi. Ejen sambungan, komponen utama dalam ReplicationConnectionProxy adalah seperti berikut:

ReplicationConnectionProxy mempunyai dua objek sambungan sebenar, masterConnection dan slavesConnection (sambungan semasa) boleh ditukar kepada masterConnection atau slavesConnection , kaedah menukar Ini boleh dicapai dengan menetapkan readOnly. Bagaimana untuk menyelesaikan masalah kegagalan sambungan yang disebabkan oleh MySQL menggunakan ReplicationConnection

Dalam logik perniagaan, teras untuk merealisasikan pemisahan baca-tulis juga ada di sini secara ringkas: apabila menggunakan ReplicationConnection untuk pemisahan baca-tulis, anda hanya perlu melakukan aop yang "menetapkan atribut readOnly bagi. sambungannya".

Berdasarkan ReplicationConnectionProxy, objek proksi Sambungan yang diperolehi dalam logik perniagaan, apakah logik utama semasa capaian pangkalan data?

Proses pemprosesan objek proksi ReplicationConnection

Untuk logik perniagaan, instance Connection yang diperolehi ialah objek proksi ReplicationConnection, yang melengkapkan pemprosesan capaian pangkalan data melalui ReplicationConnectionProxy dan ReplicationMySQLConnection Antaranya, ReplicationConnectionProxy bukan sahaja melaksanakan InvocationHandler, tetapi juga memainkan peranan pengurusan sambungan Logik teras adalah seperti berikut:

对于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 &#39;SELECT 1&#39; 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属性设置超时时间。

Atas ialah kandungan terperinci Bagaimana untuk menyelesaikan masalah kegagalan sambungan yang disebabkan oleh MySQL menggunakan ReplicationConnection. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:yisu.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam