Java 杂谈

监控应用获取连接超时问题

2019-07-14  本文已影响33人  唐僧吃肉_
引言

上周一去公司,被领导叫过去询问,业务监控平台 "批处理"任务, 是不是更改执行时间了,从原先的每小时整点执行变为现在的每小时16&45min 执行。
第一反应很懵,45min执行是此前做"测试"(稍后分析这个"测试")时 更改的,但每小时 16min执行,确实没有改过,难道是其他同事改过?(业务监控已经交接给其他同事了),
但应该不可能.虽然领导说是问问,不用改,但还是一连忐忑了几天. 趁着 T+1 联调,事情不太多,回头重新查看这个问题.

问题背景:

从备份库切换回生产库(100节点),然后问题就来了...

问题描述:

1.每次整点执行时,比如早上9:00,线程都会hang住一段时间,然后在9:15:46,前台调度页面显示执行完成,后台却在9:15:00 抛出"连接超时"异常。
因为当时在做迁移,没有更多时间来分析,所以将 执行时间从整点执行 更新 到 每小时45min,后续的效果来看,并未出现预料之外的情况.

原因分析:

如下为简化后的大致流程图:


image.png

将从几个方面来分析:
调度器、Druid数据源、JDBC超时、Oracle服务端等几个方面来分析。

其实解决这个问题最简单,直接有效的方法 ,根据现有的问题特征(这个问题在备份库是不存在的)
从生产库(100节点) 查询 :

一:调度器

调度器的原因,是可以排除的,前台调度页面,显示执行开始时间 是在 09:45,而 后台记录显示,线程在被hang住15分钟后,抛出异常,重新获取连接后,将整个流程执行完毕!
15分钟,是不是很巧呢?

二:Druid数据源

问题特征:
1.线程在指定的时间启动,在hang住15分钟后,抛出"connection time out";
2.监控平台 与 "100节点"并不在一个网段中,对于内网,考虑会有防火墙的存在。
3.Druid连接池,对于连接的超时时间设置在60sec(100节点对连接的超时控制无法知晓,不过可以反推出来);

问题分析:

2.1 线程 在 被hang住的这段时间里,到底在做什么?
用jstat查看jvm内存使用情况,发现各区内存占用率较低,查看GC信息也没有异常。
再接茬查看线程栈,发现线程 hang 在socket底层的TCP套接字读取上:如图:


微信图片_20190714220852.jpg

通过跟踪 druid 代码,当前线程正在执行 query-"select 1 from dual",这是一条验证sql,本身不会有太大的数据量,但 线程一直 hang 住,等待服务端的响应(等待服务端写入数据,服务端写入数据完成会发送结束标志), 最可能的一种情况,服务端将该连接关闭,导致客户端无响应,一直处于等待状态。

一段时间后(15min),抛出"connection time out" (在没找到准确原因之前,将执行时间提前15min,也就是每个小时45min执行);

二:原因分析:

业务线程一直处于runable状态,但是一直hang住却没有返回值, 而方法链调用的源头是 Druid连接池的isValidConnection()方法。 问题似乎与 Druid的配置有关.
Druid在获取连接后,在验证连接的有效性时,被hang住,难道是连接超时了 ?

根据线程 是在调用 socketread 与服务端通信时 hang 住,且 调用源头来自 druid的 isValidConnection发起。
可以推断,是否连接因为超时,被服务端关闭,导致客户端发起socketread无响应,长时间 hang住 呢 ?
当前 druid 中配置的连接有效时长为60 sec ;

2.2 DruidAbstractDataSource源码分析:
public boolean isValidConnection(Connection conn, String query, int validationQueryTimeout) throws Exception {
        if (query == null || query.length() == 0) {
            return true;
        }
        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);
        }
    }

查看调用链,在执行如上代码时 hang 住 , isValidConnection 的调用参数中,包含一个validationQueryTimeout,是否因为没有设置validationQueryTimeout或者设置的时间过长导致的呢?
1》在访问核心100节点的数据源中,配置validationQueryTimeout时长为:15s ,即 客户端等待服务端响应,超过15sec 服务端无响应后抛出异常,中断 被 hang 住的状态。
然而并没有效果~(自己都笑了);

Druid连接池里的连接(100节点)是超时的,那为什么没有被kill掉,配置的有效时长60sec.

Druid执行空闲连接的校验源码:

public class DestroyTask implements Runnable {

    @Override
    public void run() {
        shrink(true, keepAlive);

        //空闲连接的处理
        if (isRemoveAbandoned()) {
            removeAbandoned();
        }
    }
}

此处代码太长,是druid高效的核心,截取一段:

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

            if (checkTime && i < checkCount) {
                evictConnections[evictCount++] = connection;
            } else if (idleMillis > maxEvictableIdleTimeMillis) {
                evictConnections[evictCount++] = connection;
            } else if (keepAlive) {
                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;
} finally {
    lock.unlock();
}

此处需注意的是:

至少到这里,连接超时的原因找到了,引出了另一个问题: 为什么配置 statement的validationQueryTimeout超时,没有达到 超时中断线程的作用 ?

三:JDBC 超时

SUN公司一共定义了4中类型的JDBC,常用的是第四种,该类型的Driver完全由Java代码实现,通过使用socket与数据库进行通信


image.png

通过socket对字节流进行处理,当在网络操作中遇到问题的时候,将会消耗大量的cpu资源,并且失去响应超时。

应用与数据库间的timeout层级
image.png

即:简化后,应用 与 数据库的 timeout层级。
高级别的 timeout 依赖于低级别的 timeout ,只有当低级别的 timeout无误时,高级别的 timeout才能确保在正常。
例如:当 socket timeout 出现问题时, 高级别的 statement timeout 和 transcation timeout都将失效。

Druid是数据库连接的创建和管理,并不干涉timeout的处理。
当连接在Druid中创建,或是Druid发送校验query检查连接有效性的时候,socket timeout将会影响这些过程,但并不直接对应用造成影响。

当在应用中调用Druid的getConnection()方法时,可以设置获取数据库连接的超时时间,但是这和JDBC的timeout毫不相关。

3.1 JDBC的statement timeout处理过程

设置的是一个statement的执行超时时间,即driver等待statement执行完成,接收到数据的超时时间
注意:
statement的timeout不是整个查询的timeout,只是statement执行完成并拉取fetchSize数据返回的超时;
resultSet的next在必要的时候还会触发fetch数据,每次fetch的超时时间是单独算的,默认也是以statement设置的timeout为准

3.1.1 Oracle JDBC Statement的QueryTimeout处理过程
  1. 通过调用Connection的createStatement()方法创建statement
  2. 调用Statement的executeQuery()方法
  3. statement通过自身connection将query发送给Oracle数据库
  4. statement在OracleTimeoutPollingThread(每个classloader一个)上进行注册
  5. 达到超时时间
  6. OracleTimeoutPollingThread调用OracleStatement的cancel()方法
  7. 通过connection向正在执行的query发送cancel消息
3.2 JDBC的 socket Timeout ?

1.JDBC使用socket与数据库连接,数据库并不对应用与数据库间的连接超时进行处理。
2.JDBC的 socket timeout 在数据库被突然停掉或是发生网络错误(由于设备故障等原因)时十分重要。
3.由于TCP/IP的结构原因,socket没有办法探测到网络错误,因此应用也无法主动发现数据库连接断开。
4.如果没有设置socket timeout的话,应用在数据库返回结果前会无期限地等下去,这种连接被称为dead connection。

设置的是jdbc I/O socket read and write operations的超时时间,防止因网络问题或数据库问题,导致driver一直阻塞等待。(建议比statement timeout的时间长)

为了避免 dead connections,socket 必须要有超时配置。
socket timeout可以通过JDBC设置,socket timeout 能够避免应用在发生网络错误时产生无休止等待的情况,缩短服务失效的时间。

使用原则:

四:结论

目前,分析了 监控平台连接池中的连接(100节点)为什么超时 以及 为什么设置 statement超时没有任何效果。
4.1 为什么会在每小时45min执行:
因为连接超时,服务端关闭连接,导致客户端socketRead读取时,服务端无响应,长时间 hang 住。时长15min.所以,当时在没有查清原因之前,为了 在 准点执行,在当时测试此问题时,将执行时间提前15min.

4.2 为什么会在每小时16min执行?

4.3 为什么 每次 hang 住的时间都是15min?

4.5 如何解决?

45&16min问题的解决方案:
在Druid数据源中,指定max的具体时长即可,这需要根据"100节点"的数据来配置.

以上所有问题的产生,只发生在"100节点".

结尾:
其实,分析到这里,什么结论也没有 ,是不是 没有意义呢 ?

只是对解决问题的思路 以及 工作中的一段总结~

上一篇 下一篇

猜你喜欢

热点阅读