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() 好了:
这里的代码比较多,就不贴图了,简单说明下它的流程:
-
检查缓存中是否有 url 对应的连接,如果有就直接使用
-
检查 proxy 设定,如果设定了使用 proxy,则使用 proxy 产生连接,否则直连目标。
其中连接是在此产生的:
我们一直跟进 getNewHttpClient(位于 sun.net.www.protocol.http.HttpURLConnection),可以发现它创建了一个 HttpClient 对象:
在这里,它:
-
设置了 connectTimeout 时间
-
打开了连接(吐槽下 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 的使用情况:
- 我们设置了 connectTimeout
- 这个参数被 FeignClient 读取到配置
- RPC 调用时,feign 产生一个 HttpUrlConnection,使用这个参数
- HttpUrlConnection 会产生一个 socket 连接,用于发送 / 读取数据
- Socket 连接调用 native 函数 socketConnect 产生连接(其实 java 的 Socket 就是对 socket 的一个封装,推测在 windows 应该就是对 WSOCK 的一个封装)
- native 函数对 socket 设置为异步模式,然后调用 connect,最后根据 connectionTimeout 进行轮询,如果超时则抛出异常,否则成功