接口访问超时引发的思考

前言

最近很多云平台用户找过来问,为什么在平台发布的应用,总是偶发接口访问超时,而且超时时间都还蛮固定的。随着问的用户越来越多,发现这是一个共性问题,发生偶发接口访问超时的用户的应用,有如下的特点。

  1. 超时时间固定。通常接口会在60秒左右返回;
  2. 应用访问量不高。这些应用通常是内部系统应用,访问量很低;
  3. 超时的接口涉及数据库操作。所有偶发超时的接口,均有操作数据库的行为;
  4. 使用了数据库连接池且为TomcatJdbc

结合上述的共性问题,问题的关键点就指向了数据库连接池或者用户的SQL ,但是用户提供了执行的SQL 自证了不是那种写得很烂的SQL ,所以重点排查工作就在数据库连接池上了,这一排查,就牵扯出了我对于TomcatJdbc数据库连接池配置的思考。

正文

一. 问题分析

要分析这个问题,首先需要说明一下公司内部私有云的集群网络结构。在搭建K8s集群时,集群内部节点间共同构成一个虚拟网络,示意图如下。

每个集群有一个SLB 作为四层负载均衡设备,作为集群内部服务的统一入口。当集群内部的服务要访问集群外时,这里就涉及出站访问,那么此时需要进行NAT源地址端口转换,如下所示。

当为虚拟网络开启NAT 时,会为NAT 分配一个随机的物理IP 地址,此时所有经过NAT 的客户端出站请求,其源地址均会映射为这个物理IP 以及一个随机物理端口,同时NAT会记录这个映射关系,如下所示。

又由于要防止物理端口耗尽,所以集群网络还有一个监测机制,就是如果在规定时间内,某个TCP 连接没有出站的流量,那么就会将其在NAT 的源地址映射规则移除,此时造成的结果就是这个TCP 连接实际是断开了,但是TCP连接的两端都是感知不到的。

上面简要的介绍了一下公司内部私有云的集群网络的一个结构特点,正是由于上面说到的:如果在规定时间内,某个TCP 连接没有出站的流量,那么就会将其在NAT的源地址映射规则移除,导致了开头应用接口偶发访问慢的问题,具体原因且听我慢慢说道。

出现问题的应用,有两个特点需要关注,其一是使用了TomcatJdbc数据库连接池,其二是应用访问量不高。

首先说一下数据库连接池,其作为池化技术,本质就是提前和数据库服务端建立连接然后把连接保存起来,当应用程序想要和数据库进行交互时,可以直接从连接池里获取连接,而不需要重新建连,达到了连接复用的作用。

其次再分析一下应用访问量不高,正是由于应用访问量不高,连接池里的连接会空闲下来,而连接空闲,表现在TCP 连接层面就是没有请求的报文出去,也就是没有出集群的出站流量,那么说到这里,帅的人其实已经发现问题的所在了,因为在公司内部私有云的集群网络下,如果在规定时间内,某个TCP 连接没有出站的流量,那么这个TCP 连接就会被断开,并且这个断开是两端都无法感知到的,也就是数据库连接池认为这个连接还可用,数据库服务端也认为这个连接还可用,那么当后续请求到来时,应用程序就会从数据库连接池中拿到这个底层TCP连接已经断开的数据库连接

分析到这里,问题基本已经清晰了,接口访问超时,是因为访问接口时会触发数据库操作,从而会从TomcatJdbc 数据库连接池中拿到一个底层TCP 连接已经断开的数据库连接,到这里其实还没完全解释清楚为什么会慢,就算使用了一个不可用的数据库连接,按理也不应该阻塞60秒左右,要弄明白这个,我又去深入研究了一下TomcatJdbc的源码,总算是发现了问题所在。

完整的TomcatJdbc 源码分析,会在后续的文章中给出,这里直接就给出导致慢的原因及对应部分的源码简析。慢的时间就是消耗在TomcatJdbc 校验数据库连接上,当我们从TomcatJdbc 数据库连接池获取出一个连接时,如果配置了testOnBorrowtrue ,那么此时就会校验连接,而所谓校验连接,就是使用这个连接,给数据库发送一条简单的查询语句,这个语句通常为SELECT 1,简化版源码如下所示。

java 复制代码
public boolean validate(int validateAction, String sql) {
    
    ......

    String query = sql;

    ......

    boolean transactionCommitted = false;
    Statement stmt = null;
    try {
        // 通过物理连接创建出Statement
        stmt = connection.createStatement();

        // 获取配置的校验连接超时时间
        // 默认的校验连接超时时间为-1
        int validationQueryTimeout = poolProperties.getValidationQueryTimeout();
        if (validationQueryTimeout > 0) {
            stmt.setQueryTimeout(validationQueryTimeout);
        }

        // 执行校验SQL
        stmt.execute(query);
        stmt.close();
        this.lastValidated = now;
        transactionCommitted = silentlyCommitTransactionIfNeeded();
        return true;
    } catch (Exception ex) {
        
        ......

    } finally {
        if (!transactionCommitted) {
            silentlyRollbackTransactionIfNeeded();
        }
    }
    return false;
}

在校验连接的时候,会通过物理连接创建出Statement ,然后如果配置了validationQueryTimeout 参数,则会给Statement 设置查询超时时间,但是如果没有配validationQueryTimeout 参数,则就不会给Statement 设置查询超时时间,这个时候问题就来了,如果不给Statement 设置查询超时时间,那么这个超时时间是多久呢。要搞懂这个问题,我们需要回顾一下数据库中的三种超时参数:Transaction Timeout (事务超时),Statement Timeout (语句查询超时)和Socket Timeout(套接字超时)。

超时参数 说明
Transaction Timeout 用于限制某个事务执行时间的上限。通常Transaction Timeout = N * Statement Timeout + 业务处理耗时 ,其中N 表示事务中要执行的SQL
Statement Timeout 用于限制某条SQL语句的最大执行时间
Socket Timeout 数据库连接的Socket 套接字读写数据的阻塞超时时间,通常Socket Timeout 需要大于Statement Timeout 的值,否则Statement Timeout就没有意义

在本文的场景中,重点关注Statement TimeoutSocket Timeout 。首先是Statement Timeout ,上面的源码分析中已经知道,当没有配置TomcatJdbc 数据库连接池的validationQueryTimeout 参数时,就不会设置Statement Timeout ,此时按照JDBC 的官方注释可以知道,这个时候Statement Timeout 是不超时的,那么这会儿超时肯定就是Socket Timeout 触发的超时,这是因为Socket 是无法感知底层TCP 连接的状态的,也就是就算TCP 连接断开了,那么Socket 也会阻塞在读写操作上,直到超时。那么到这里,已经基本接近答案了,接口访问超时,就是因为在对连接执行校验时阻塞在了Socket 读写数据上直到超时,那么还有最后一个问题,为什么超时时间都是固定为60秒呢,这个原因就是公司内部给出的数据库连接串的最佳实践中,把socketTimeout配置为了60000,即60秒,就像下面这个连接串一样。

yaml 复制代码
jdbc:mysql://xxx.xxx.xxx:3306/test?characterEncoding=UTF-8&failOverReadOnly=false&secondsBeforeRetryMaster=0&queriesBeforeRetryMaster=0&serverTimezone=Asia/Shanghai&useUnicode=true&useSSL=false&connectTimeout=10000&socketTimeout=60000

二. 问题解决

那么上述的问题如何解决,其实核心就在于TomcatJdbc数据库连接池的配置。

首先按照问题分析里的思路,首先应该想到要配置validationQueryTimeout 参数,并且可以将validationQueryTimeout 参数设置为一个较小的值,这样就能够快速的完成连接校验。其实这是一个正确但不完全正确的做法,因为当validationQueryTimeout 参数生效的时候,说明已经是从数据库连接池里拿出了一个底层TCP 连接已经断开的数据库连接,这时候去校验这个连接,无论validationQueryTimeout参数配置为了一个多么小的值,这里的校验耗时都是阻塞住了业务线程,这很明显不是一个优雅的做法。

那么更为优雅的做法是配置testWhileIdle ,理解这个参数前,我们简要介绍一下TomcatJdbc 数据库连接池的一个工作机制,首先TomcatJdbc 数据库连接池有两个队列用于存放可用的连接(idle )和借出的连接(busy ),其实这很好理解,可用的连接就放在idle 队列中,业务线程获取连接时,就从idle 队列中获取,每个被获取的连接,会从idle 队列中转移到busy 队列中,与此同时,TomcatJdbc数据库连接池还有一个定时任务,这个定时任务触发执行时,会做如下三件事情。

  1. 连接泄漏检测;
  2. 可用连接检测保活;
  3. idle队列大小调整。

重点关注连接泄漏检测可用连接检测保活 。连接泄漏检测主要是针对busy 队列,主要用于判断借出的连接有没有在规定时间内被归还,如果没有,则强制回收连接;空闲连接检测保活主要是针对idle队列,主要用于检测可用连接的状态,在检测时如果不可用,则重连一次数据库,如果可用则相当于当前连接与数据库做了一次保活。

那么到这里,testWhileIdle 这个参数的作用就很明确了,当配置testWhileIdletrue 时,就会每隔timeBetweenEvictionRunsMillis 的时间对所有可用且校验间隔达到了validationInterval 的连接进行校验和保活,至此,优雅程度已经达到百分之九十五了,还差百分五,这百分之五就在于timeBetweenEvictionRunsMillisvalidationInterval 这两个配置项,假如我们知道连接空闲达到T 时间时底层TCP 连接会被断开,那么在配置testWhileIdletrue 的前提下,还需要满足T > timeBetweenEvictionRunsMillis > validationInterval ,这样才能避免空闲连接的底层TCP连接被断开。

其实吧,上面一通操作做完,也只是百分之九十九的优雅,最后百分之一,在于如果配置了testWhileIdletrue ,并且也满足T > timeBetweenEvictionRunsMillis > validationInterval ,那么这个时候,就不要配置如下两个参数为true了。

  1. testOnBorrow
  2. testOnReturn

上述参数均会导致在业务线程中花费时间去进行连接校验,虽然正常的连接校验起来很快,但是毕竟业务线程跑得能快一点是一点不是,好在上述参数默认情况下都是false,所以也不用显式的去进行配置。

后记

公司内部私有云的集群网络的特点,导致了TomcatJdbc 数据库连接池里的数据库连接在空闲一定时间后其底层TCP 连接就会断开从而不可用,然后TomcatJdbc 数据库连接池在校验连接时就会阻塞住直到触发Socket Timeout ,这种阻塞在很多时候都是致命的,所以需要结合TomcatJdbc 数据库连接池的配置来对可用的空闲连接进行保活操作,这就需要配置testWhileIdletrue来异步的进行连接保活。

不单是TomcatJdbc 数据库连接池,DruidHikariCP数据库连接池其实也有相同的问题,并且解决的核心思路也是配置连接保活。

其实不单是数据库连接池,只要底层是TCP连接的连接池化技术,都会存在这个问题,所以在使用各种连接池时,保活配置,一定要配上。

相关推荐
程序员南飞36 分钟前
ps aux | grep smart_webrtc这条指令代表什么意思
java·linux·ubuntu·webrtc
弥琉撒到我39 分钟前
微服务swagger解析部署使用全流程
java·微服务·架构·swagger
一颗花生米。2 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
问道飞鱼2 小时前
Java基础-单例模式的实现
java·开发语言·单例模式
ok!ko5 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2401_857622666 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589366 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰6 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
vvvae12347 小时前
分布式数据库
数据库
哎呦没7 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端