mysql 数据库链接中断报错分析

一、背景

Mysql 的DBA给Mysql定义一套规则,mysql 服务器端的默认的超时时间wait_timeout为8小时,但DBA把wait_timeout改为600秒,我估计这规则本意是减少数据库的长时间链接的情况,只要链接空闲超过600秒,服务器端会自动断开链接。所有产生的影响必须由客户端程序来保障。

1.1、版本说明:

mysql:5.7.17
druid:1.1.5
mysql-connector-java:5.1.44

1.2、druid重要属性配置

 <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> 
     <!-- 基本属性 url、user、password -->
     <property name="url" value="${jdbc_url}" />
     <property name="username" value="${jdbc_user}" />
     <property name="password" value="${jdbc_password}" />
       
     <!-- 配置初始化大小、最小、最大 -->
     <property name="initialSize" value="5" />
     <property name="minIdle" value="10" /> 
     <property name="maxActive" value="20" />
  
     <!-- 配置获取连接等待超时的时间 -->
     <property name="maxWait" value="60000" />
  
     <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
     <property name="timeBetweenEvictionRunsMillis" value="50000" />
  
     <!-- 配置一个连接在池中最小、最大生存的时间,单位是毫秒 -->
     <property name="minEvictableIdleTimeMillis" value="60000" />
     <property name="maxEvictableIdleTimeMillis" value="500000" />
   
     <property name="validationQuery" value="select 1" />
     <property name="testWhileIdle" value="true" />
     <property name="testOnBorrow" value="false" />
     <property name="testOnReturn" value="false" />
  
     <property name="keepAlive" value="true" />
     <property name="phyMaxUseCount" value="1000" />
  
     <!-- 配置监控统计拦截的filters -->
     <property name="filters" value="stat" /> 
 </bean>

详细配置请见官网文档:DruidDataSource配置属性列表

二、现象

系统上线一段时间后,在监控时而报错如下:

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.

根据错误日志初步判断肯定是 mysql服务端把链接已经断开,但客户端不知道并,依然尝试使用了一个已经断开的链接才会引起这个错误发生。但是根据我们对 druid 了解,druid 有链接检查功能,按理不会拿到一个无效链接才对。

三、分析

3.1、整体分析

image.png

图片表示了druid在获取线程池的大致的逻辑过程:druid在初始化时会创建两个守护线程,分别承担线程的创建(CreateConnectionThread)和销毁任务(DestoryConnectionThread),
当用户线程出现等待获取线程的操作时(且线程池中的线程数不大于最大活动线程数),创建线程会自动创建新的连接并放到线程池中,所以当用户线程需要新的连接时,只需要直接从线程池获取即可。
当用户线程从线程池中获取到连接会根据用户的配置决定是否线程进行有效性验证,如果验证线程有效则返回线程,如果无效则将该连接关闭,(DestoryConnectionThread自动回收已关闭的连接)

3.2、线程创建及销毁任务

程序启动在创建数据连接时,会自动创建两个任务(job),也就是CreateConnectionThread和DestoryConnectionThread

  1. CreateConnectionThread比较简单,也是个守候线程,代码如下:
public class CreateConnectionThread extends Thread {

        public CreateConnectionThread(String name){
            super(name);
            this.setDaemon(true);
        }

        public void run() {
            initedLatch.countDown();

            long lastDiscardCount = 0;
            int errorCount = 0;
            for (;;) {
                // addLast
                try {
                    lock.lockInterruptibly();
                } catch (InterruptedException e2) {
                    break;
                }

                long discardCount = DruidDataSource.this.discardCount;
                boolean discardChanged = discardCount - lastDiscardCount > 0;
                lastDiscardCount = discardCount;

                try {
                    boolean emptyWait = true;

                    if (createError != null
                            && poolingCount == 0
                            && !discardChanged) {
                        emptyWait = false;
                    }

                    if (emptyWait
                            && asyncInit && createCount < initialSize) {
                        emptyWait = false;
                    }

                    if (emptyWait) {
                        // 必须存在线程等待,才创建连接
                        if (poolingCount >= notEmptyWaitThreadCount //
                                && (!(keepAlive && activeCount + poolingCount < minIdle))
                                && !isFailContinuous()
                        ) {
                            empty.await();
                        }

                        // 防止创建超过maxActive数量的连接
                        if (activeCount + poolingCount >= maxActive) {
                            empty.await();
                            continue;
                        }
                    }

                } catch (InterruptedException e) {
                    lastCreateError = e;
                    lastErrorTimeMillis = System.currentTimeMillis();

                    if (!closing) {
                        LOG.error("create connection Thread Interrupted, url: " + jdbcUrl, e);
                    }
                    break;
                } finally {
                    lock.unlock();
                }

                PhysicalConnectionInfo connection = null;

                try {
                    connection = createPhysicalConnection();
                } catch (SQLException e) {
                    LOG.error("create connection SQLException, url: " + jdbcUrl + ", errorCode " + e.getErrorCode()
                              + ", state " + e.getSQLState(), e);

                    errorCount++;
                    if (errorCount > connectionErrorRetryAttempts && timeBetweenConnectErrorMillis > 0) {
                        // fail over retry attempts
                        setFailContinuous(true);
                        if (failFast) {
                            lock.lock();
                            try {
                                notEmpty.signalAll();
                            } finally {
                                lock.unlock();
                            }
                        }

                        if (breakAfterAcquireFailure) {
                            break;
                        }

                        try {
                            Thread.sleep(timeBetweenConnectErrorMillis);
                        } catch (InterruptedException interruptEx) {
                            break;
                        }
                    }
                } catch (RuntimeException e) {
                    LOG.error("create connection RuntimeException", e);
                    setFailContinuous(true);
                    continue;
                } catch (Error e) {
                    LOG.error("create connection Error", e);
                    setFailContinuous(true);
                    break;
                }

                if (connection == null) {
                    continue;
                }

                boolean result = put(connection);
                if (!result) {
                    JdbcUtils.close(connection.getPhysicalConnection());
                    LOG.info("put physical connection to pool failed.");
                }

                errorCount = 0; // reset errorCount
            }
        }
    }

说明:

  • 死循环 for (;;)
  • 必须存在线程等待,才创建连接。
  • 创超时不能超过maxActive数量的连接。
  1. DestoryConnectionThread
    创建了一个死循环的任务,每过timeBetweenEvictionRunsMillis执行一次。
     public class DestroyConnectionThread extends Thread {
         public DestroyConnectionThread(String name){
             super(name);
             this.setDaemon(true);
         }
         public void run() {
             initedLatch.countDown();
             for (;;) {
                 // 从前面开始删除
                 try {
                     if (closed) {
                         break;
                     }
                     if (timeBetweenEvictionRunsMillis > 0) {
                         Thread.sleep(timeBetweenEvictionRunsMillis);
                     } else {
                         Thread.sleep(1000); //
                     }
                     if (Thread.interrupted()) {
                         break;
                     }
                     destroyTask.run();
                 } catch (InterruptedException e) {
                     break;
                 }
             }
         }
     }
    
    

其实真执行回收的以下的方法。

public void shrink(boolean checkTime, boolean keepAlive) {
        try {
            lock.lockInterruptibly();
        } catch (InterruptedException e) {
            return;
        }
        boolean needFill = false;
        int evictCount = 0;
        int keepAliveCount = 0;
        try {
            if (!inited) {
                return;
            }
            final int checkCount = poolingCount - minIdle;
            final long currentTimeMillis = System.currentTimeMillis();
            for (int i = 0; i < poolingCount; ++i) {
                DruidConnectionHolder connection = connections[i];
                if (checkTime) {
                    if (phyTimeoutMillis > 0) {
                        long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
                        if (phyConnectTimeMillis > phyTimeoutMillis) {
                            evictConnections[evictCount++] = connection;
                            continue;
                        }
                    }
                    long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
                    if (idleMillis < minEvictableIdleTimeMillis
                            && idleMillis < keepAliveBetweenTimeMillis
                    ) {
                        break;
                    }
                    if (idleMillis >= minEvictableIdleTimeMillis) {
                        if (checkTime && i < checkCount) {
                            evictConnections[evictCount++] = connection;
                            continue;
                        } else if (idleMillis > maxEvictableIdleTimeMillis) {
                            evictConnections[evictCount++] = connection;
                            continue;
                        }
                    }
                    if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) {
                        keepAliveConnections[keepAliveCount++] = connection;
                    }
                } else {
                    if (i < checkCount) {
                        evictConnections[evictCount++] = connection;
                    } else {
                        break;
                    }
                }
            }
            int removeCount = evictCount + keepAliveCount;
            if (removeCount > 0) {
                System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
                Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
                poolingCount -= removeCount;
            }
            keepAliveCheckCount += keepAliveCount;

            if (keepAlive && poolingCount + activeCount < minIdle) {
                needFill = true;
            }
        } finally {
            lock.unlock();
        }

        if (evictCount > 0) {
            for (int i = 0; i < evictCount; ++i) {
                DruidConnectionHolder item = evictConnections[i];
                Connection connection = item.getConnection();
                JdbcUtils.close(connection);
                destroyCountUpdater.incrementAndGet(this);
            }
            Arrays.fill(evictConnections, null);
        }
        if (keepAliveCount > 0) {
            // keep order
            for (int i = keepAliveCount - 1; i >= 0; --i) {
                DruidConnectionHolder holer = keepAliveConnections[i];
                Connection connection = holer.getConnection();
                holer.incrementKeepAliveCheckCount();
                boolean validate = false;
                try {
                    this.validateConnection(connection);
                    validate = true;
                } catch (Throwable error) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("keepAliveErr", error);
                    }
                    // skip
                }
                boolean discard = !validate;
                if (validate) {
                    holer.lastKeepTimeMillis = System.currentTimeMillis();
                    boolean putOk = put(holer, 0L);
                    if (!putOk) {
                        discard = true;
                    }
                }
                if (discard) {
                    try {
                        connection.close();
                    } catch (Exception e) {
                        // skip
                    }

                    lock.lock();
                    try {
                        discardCount++;

                        if (activeCount + poolingCount <= minIdle) {
                            emptySignal();
                        }
                    } finally {
                        lock.unlock();
                    }
                }
            }
           this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);
            Arrays.fill(keepAliveConnections, null);
        }
        if (needFill) {
            lock.lock();
            try {
                int fillCount = minIdle - (activeCount + poolingCount + createTaskCount);
                for (int i = 0; i < fillCount; ++i) {
                    emptySignal();
                }
            } finally {
                lock.unlock();
            }
        }
    }

说明:

  • 空闲时间还没有超最小生存时间(minEvictableIdleTimeMillis)时,是不会回收的。
  • 空闲时间超过最小生存时间是并不会全部回收,只会回收前poolingCount - minIdle,minIdle数量暂时不会回收。
  • 当空闲大于最大生存时间时(maxEvictableIdleTimeMillis)时,由客户端口全部回收,druid默认的maxEvictableIdleTimeMillis为7小时。
  • 当服务端的超时时间大于配置的wait_timeout时间时会由服务器全部断开超时链接,不管客端的情况。
  • keepAlive保持链接的逻辑也在这代码中体现。

3.3、客户端超时验证机制

超时验证机制是指客户端拿到链接时,只要当时时间与链接最后活动时间的差大于检测间隔时间(即currentTimeMillis - lastActiveTimeMillis > timeBetweenEvictionRunsMillis)则会发起链接检测,执行testConnectionInternal检测如代码:

public DruidPooledConnection getConnectionDirect(long maxWaitMillis) 
此处省略其它代码
 if (testWhileIdle) {
                    final DruidConnectionHolder holder = poolableConnection.holder;
                    long currentTimeMillis             = System.currentTimeMillis();
                    long lastActiveTimeMillis          = holder.lastActiveTimeMillis;
                    long lastKeepTimeMillis            = holder.lastKeepTimeMillis;
                    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(realConnection);
                             continue;
                        }
                    }
                }
            }

说明:在testOnBorrow=false的情况下,这个参数不要轻易配置,否则浪费性能。

Mysql的检测机制有两种,两种机制理论上能达到相同的效果。在执行的时间都能达到续约的效果(即执行后能重置lastActiveTimeMillis为当前执行的时间)

  1. ping
    druid默认是ping机制,所以默认时配置validationQuery参数是无效的。如果要改变则进程的启动参数中(jvm参数)设置-Ddruid.mysql.usePingMethod=false即可。
  2. 查询SQL(select 1)
    使用查询的机制来检测链接的可用性更的保险。
    代码如下:
 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);
        }
    }

四、结论

1、理论上是不会出来问题的,因为空闲时间只要大于等于timeBetweenEvictionRunsMillis时间会验测出来,则timeBetweenEvictionRunsMillis=60秒,远小于MYSQL的wait_timeout=600秒。除非mysql-connect-java,在默认ping的机制有不稳定性因素。
2、可能网络抖动在执行验证时就已失败,或非链接断开原因。

五、解决方案

1、配置maxEvictableIdleTimeMillis=500000(500秒)

默认情况下maxEvictableIdleTimeMillis=25200000L (即7小时),因为数据库的超时时间从8小时改为600秒,为减少风险理应该由客户端在空闲时主动关闭链接。而非超时后由mysql服务器端把链接关闭。
这个值的合理再应该根据公式:
maxEvictableIdleTimeMillis+timeBetweenEvictionRunsMillis<mysql服务器的wait_timeout
即:500000ms+50000=550000<600000.
由客户端主动释放超时链接。

2、配置keepAlive=true

打开KeepAlive之后的效果:

  1. 初始化连接池时会填充到minIdle数量。
  2. 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。
  3. 当网络断开等原因产生的由ExceptionSorter检测出来的死连接被清除后,自动补充连接到minIdle数量。
    KeepAlive的最大作用是以minIdle数量自动继租,无论服务器端还是客户端都无法释放超时链接(因为不会超时)。
    KeepAlive的使用条件是:建议使用druid 1.1.16或者更高版本。
    详细文档见官网:KeepAlive配置

3、mysql服务端改回wait_timeout= 28800(8小时)

因为默认情况下客户端最大存活时间maxEvictableIdleTimeMillis为7小时,所以在服务器断开链接前,由客户端主动释放超时链接。(类似解决方案一)。

4、建议升级mysql-connect-java 5.1.48版本。

因为Mysql数据库的与驱动包版本的兼容性可能存在问题,升级新的版本解决了很多存在的BUG,具体的解决哪个问题可以在官方文档。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容