Feign配置参数connectTimeout解析

本文由 简悦 SimpRead 转码, 原文地址 www.cnblogs.com

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

[](javascript:void(0); "复制代码")

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

[](javascript:void(0); "复制代码")

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 行:

[](javascript:void(0); "复制代码")

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

[](javascript:void(0); "复制代码")

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

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 @ 2023-04-03 15:49  托马斯布莱克  阅读(922)  评论(0编辑  收藏  举报