关于FeignClient的connectTimeout参数的分析

最近工作上面的项目使用了Spring Cloud,RPC的客户端是FeignClient,经常遇到超时问题,于是请教了同事,同事告诉我使用如下配置即可防止超时时间太短而导致报错:

feign:
  client:
    config:
      default:
        connectTimeout: 连接超时时间
        readTimeout: 读取超时时间

ok,超时问题解决,一切正常,本文结束,再见。。。。。。。。。啊不对,大家发现什么地方不正常了吗?

这个connectTimeout,好奇怪啊!

首先,readTimeout很好理解,就是对socket设置nonblock选项,然后在read的时候判断这个操作究竟花了多少时间,如果超过给定的时间就抛出异常或者返回已经读取到的数据;

但是这个connectTimeout???connect函数原型:

 

可以看到,参数只有socket、address、address_len,和timeout没有半毛钱关系,那这个timeout究竟是怎么来的?

 

不如通过FeignClient源代码分析一下这个参数究竟有什么用。我们首先通过参数找到它的引用位置:

得益于IDEA的强大功能,它帮我们定位到参数是在这里使用的:

 

可以看出这是一个配置集合,我们在它附近找到connectTimeout参数:

可以推断出,当框架启动后,会将application.yml中的配置集合加载到FeignClientConfiguration对象当中,其中这个配置对象当中的connectionTimeout变量就是文件中指定的超时时间。

那么接下来的动作其实很好猜,这个参数肯定会在发送HTTP请求的时候设置到connection对象当中。现在我们要做的就是看这个参数具体是怎么传递的,并且传递到什么地方了。

我们先观察这个变量,发现它是一个私有变量,并且属于FeignClientConfiguration,所以我们推断外部对象读取这个变量时肯定是通过getter来进行访问的,我们通过交叉引用,找到getConnectTimeout()的调用者,总共有两条:

其中第一条是用来初始化builder的,我们并不感兴趣,第二条这个Request和发送请求有点关系,所以我们从这里开始着手。

可以看出Request对象中也有一个变量,叫做connectionTimeoutMillis,存放也是刚才那个变量的值(一个变量要存放这么多地方吗,搁这儿套娃呢):

 

我们找到这个变量的使用位置(有多个,在这里我只列出了我们感兴趣的位置):

 

这个convertAndSend函数也大有来头,这里开个坑,下次分析下这一块Feign RPC服务发现、调用的流程(感觉是个巨坑,内容有点多)。

总之和我们的猜想一样,这个connection对象会使用connectionTimeout参数。根据经验,这个东西多半是Java里面自带的私货,为了方便开发人员,在普通的socket上面封装了这种功能。

因为普通的socket编程其实是既没有readTimeout,更没有connectionTimeout,很多服务端所谓的readTimeout其实都是一个epoll模型管理socket事件,然后通过一个后台线程检查所有客户端socket上一次读取到数据的时间,如果超过某个阈值,就会主动关闭这个连接,把客户端踢下线,有时候甚至没有后台线程,就是这个epoll线程本身在处理事件之前,检查上一次读取到数据的时间。

 

我们继续我们的分析流程, 首先我们注意到这是一个HttpUrlConnection,所以我们找到这个类,并且根据常识,我们判断它连接远端主机时调用的是connect函数,所以我们直接定位到这个函数好了:

 

我们继续跟进plainConnect(),发现它进行了检查,最后调用的是plainConnect0(), 所以我们继续跟进plainConnect0()好了:

这里的代码比较多,就不贴图了,简单说明下它的流程:

1. 检查缓存中是否有url对应的连接,如果有就直接使用

2. 检查proxy设定,如果设定了使用proxy,则使用proxy产生连接,否则直连目标。

其中连接是在此产生的:

 

我们一直跟进getNewHttpClient(位于sun.net.www.protocol.http.HttpURLConnection),可以发现它创建了一个HttpClient对象:

 

在这里,它:

1. 设置了connectTimeout时间

2. 打开了连接(吐槽下openServer这个名字,让人感觉像是服务端的感觉啊,真是取名鬼才)

我们继续跟进会发现它调用了doConnect()函数,其中最重要的是拿到socket对象后进行connect,它是带connectionTimeout的:

 

这个connect已经是Java的socket封装了(引用的java.net.Socket)到这里我们的问题已经转变为:

java.net.Socket的connectionTimeout是怎么实现的?说实话,要不是这次看代码,以前我一直不知道java的Socket竟然有connectionTimeout这种参数。

我们继续从java.net.Socket开始,看下这个connectionTimeout是如何使用的。

我们找到函数:

public void connect(SocketAddress endpoint, int timeout) throws IOException

这个函数最关键的地方:

 

它调用了impl的connect,这个impl是一个SocketImpl类型的变量,这难道就是传说中的委派模式?

具体我们应该看哪个实现呢?第三个是一个socks socket,肯定不是我们要找的。

那我们用的要么是个HttpConnectSocketImpl,要么是个从AbstractPlainSocketImpl。在这里我们更倾向于它是一个AbstractPlainSocketImpl,因为我们这个Socket对象是从一个HttpUrlConnection调用过来的,本身符合分层设计的思想,即:应用层->协议层->传输层;但是如果从HttpUrlConnection调用Socket对象,然后又调用回一个HttpConnectSocketImpl,从传输层又回到了协议层,显然不符合逻辑。综上所述,调用的应该是AbstractPlainSocketImpl中的connect。

我们从AbstractPlainSocketImpl继续分析:

调用链路是:AbstractPlainSocketImpl.connect -> connectToAddress -> doConnect -> socketConnect,如下图:

 

AbstractPlainSocketImpl.connect调用connectToAddress:

connectToAddress调用doConnect:

doConnect调用socketConnect:

 

看这个socketConnect的声明:

这是一个native函数,说明socket的连接超时功能不是在java实现的,是在jre的native库中实现的,只能去看jvm的源代码了。

我们找到jvm里面实现这一部分native函数的代码:

http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/tip/src/solaris/native/java/net/PlainSocketImpl.c 

第255行是函数体的开始,但是我们关心的部分实际上在第356行:

    } else {
        /*
         * A timeout was specified. We put the socket into non-blocking
         * mode, connect, and then wait for the connection to be
         * established, fail, or timeout.
         */
        SET_NONBLOCKING(fd);//###1

        /* no need to use NET_Connect as non-blocking */
        connect_rv = connect(fd, (struct sockaddr *)&him, len);

        /* connection not established immediately */
        if (connect_rv != 0) {
            int optlen;
            jlong prevTime = JVM_CurrentTimeMillis(env, 0);

            ....../*
             * Wait for the connection to be established or a
             * timeout occurs. poll/select needs to handle EINTR in
             * case lwp sig handler redirects any process signals to
             * this thread.
             */
            while (1) {
                jlong newTime;
#ifndef USE_SELECT
                {
                    struct pollfd pfd;
                    pfd.fd = fd;
                    pfd.events = POLLOUT;

                    errno = 0;
                    connect_rv = NET_Poll(&pfd, 1, timeout);//###2
                }
#else
                {
                    fd_set wr, ex;
                    struct timeval t;

                    t.tv_sec = timeout / 1000;
                    t.tv_usec = (timeout % 1000) * 1000;

                    FD_ZERO(&wr);
                    FD_SET(fd, &wr);
                    FD_ZERO(&ex);
                    FD_SET(fd, &ex);

                    errno = 0;
                    connect_rv = NET_Select(fd+1, 0, &wr, &ex, &t);//###2
                }
#endif

                if (connect_rv >= 0) {
                    break;
                }
                if (errno != EINTR) {
                    break;
                }

                /*
                 * The poll was interrupted so adjust timeout and
                 * restart
                 */
                newTime = JVM_CurrentTimeMillis(env, 0);
                timeout -= (newTime - prevTime);//###3
                if (timeout <= 0) {
                    connect_rv = 0;
                    break;
                }
                prevTime = newTime;

            } /* while */

            if (connect_rv == 0) {
                JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
                            "connect timed out");//###4

                /*
                 * Timeout out but connection may still be established.
                 * At the high level it should be closed immediately but
                 * just in case we make the socket blocking again and
                 * shutdown input & output.
                 */
                SET_BLOCKING(fd);
                JVM_SocketShutdown(fd, 2);
                return;
            }

代码中重要的部分已经用###标出,并且标上了红色:

###1:这一步将socket设置为non-blocking的。ps:这里算是涨姿势了,以前只知道设置non-blocking可以让read变成异步的,然后扔给epoll托管,没想到对connect函数也管用。

###2:这个就是不断的poll,看是否已经连接上,为什么这里有两个轮询的poll函数?请注意#ifndef,在jvm编译的时候应该可以通过宏来决定使用哪个poll函数,也就是说真正编译好的代码中,只会有一个poll函数了

###3:每次轮询完毕后,从timeout中扣掉这次轮询用的时间,如果timeout小于等于0,说明timeout用完了,此时应该退出轮询循环。

###4:这个就是大家熟悉的报错了,如果timeout时间过去了,还没能连接上,这个函数会在jvm内引发一个SocketTimeoutException,让用户的java代码得到通知并进行处理。

从这里我们可以大致猜测这个connect的工作流程和connectTimeout的使用情况:

  1. 我们设置了connectTimeout
  2. 这个参数被FeignClient读取到配置
  3. RPC调用时,feign产生一个HttpUrlConnection,使用这个参数
  4. HttpUrlConnection会产生一个socket连接,用于发送/读取数据
  5. Socket连接调用native函数socketConnect产生连接(其实java的Socket就是对socket的一个封装,推测在windows应该就是对WSOCK的一个封装)
  6. native函数对socket设置为异步模式,然后调用connect,最后根据connectionTimeout进行轮询,如果超时则抛出异常,否则成功
posted on 2022-08-07 00:22  specialist  阅读(3992)  评论(1编辑  收藏  举报