Java 里面的 connect timeout 的实现
在如今的微服务应用中, 有很多的服务之间相互调用. 在服务调用的时候, 我们都会设置 connect timeout
和 read timeout
, 那么这个 connect timeout
的逻辑是什么样子的? JDK 里面到底是怎么实现的?
下面我们以一个简单的URL连接的例子, 来看这个 connect timeout
是如何工作的.
连接例子
下面是我们用来演示的代码, 它的意图就是不断的循环去获得某个网页. 不断循环的目的是为了给我们的手工操作留有足够的时间. 这段代码有网络操作, 所以会调用系统调用 connect
来拿到响应(response), 对应 bpftrace 里面的 tracepoint:syscalls:sys_enter_connect
.(感谢 chatGPT 给我们演示代码)
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class URLTest {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 1000; i++) {
try {
URL url = new URL("http://www.tianxiaohui.com");
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
con.setConnectTimeout(5000); // 连接超时时间 5000ms
con.setReadTimeout(10000); // 读取超时时间 10000ms
System.out.println(i + "Response code: " + con.getResponseCode());
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String line;
StringBuilder response = new StringBuilder();
while ((line = in.readLine()) != null) {
response.append(line);
}
in.close();
//System.out.println(response.toString());
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
}
Thread.sleep(2000);
}
}
}
获取调用栈
我们可以通过拦截系统调用 connect
的时候, 去获取调用栈, 然后依次查看调用栈里面的方法, 来确认哪里使用了 timeout
, 啥如何使用的.
通过 bpftrace 获取的 connect 调用栈
$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_connect /pid==288407/ { @[ustack(20)] = count(); }'
Attaching 1 probe...
^C
@[
__connect+75
Java_java_net_PlainSocketImpl_socketConnect+687
Ljava/net/PlainSocketImpl;::socketConnect+213
Ljava/net/AbstractPlainSocketImpl;::doConnect+332
Ljava/net/AbstractPlainSocketImpl;::connect+444
Ljava/net/Socket;::connect+596
Lsun/net/NetworkClient;::doConnect+1208
Lsun/net/www/http/HttpClient;::openServer+72
Lsun/net/www/http/HttpClient;::openServer+332
Lsun/net/www/http/HttpClient;::<init>+924
Lsun/net/www/http/HttpClient;::New+1284
Lsun/net/www/protocol/http/HttpURLConnection;::plainConnect0+3156
Lsun/net/www/protocol/http/HttpURLConnection;::plainConnect+260
Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream0+2000
Lsun/net/www/protocol/http/HttpURLConnection;::getInputStream+196
Ljava/net/HttpURLConnection;::getResponseCode+96
Interpreter+4352
call_stub+138
JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+883
jni_invoke_static(JNIEnv_*, JavaValue*, _jobject*, JNICallType, _jmethodID*, JNI_ArgumentPusher*, Thread*) [clone .constprop.1]+682
]: 1
通过 strace 获取的调用栈
$ sudo strace --stack-trace -f -e trace=connect -p 288407
[pid 288408] connect(5, {sa_family=AF_INET6, sin6_port=htons(80), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::ffff:23.222.245.123", &sin6_addr), sin6_scope_id=0}, 28) = 0
> /usr/lib/x86_64-linux-gnu/libc.so.6(__connect+0x4b) [0x12771b]
> /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so(NET_Connect+0xaa) [0xfd0a]
> /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so(Java_java_net_PlainSocketImpl_socketConnect+0x2af) [0xd30f]
> unexpected_backtracing_error [0x7fd780780995]
哪里使用了 timeout
根据 bpftrace 给出的调用栈, 我们依次排查每个方法, 发现从 Ljava/net/Socket;::connect
开始, 就获取了当前设置的 connect timeout 值, 然后依次向下传递, 直到 JDK 里面的 C 代码 Java_java_net_PlainSocketImpl_socketConnect
. 真正使用 timeout
的地方, 就在这个方法里面. 代码如下 (JDK11) https://github.com/openjdk/jdk11/blob/37115c8ea4aff13a8148ee2b8832b20888a5d880/src/java.base/unix/native/libnet/PlainSocketImpl.c#L344-L372
while (1) {
jlong newNanoTime;
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLOUT;
errno = 0;
connect_rv = NET_Poll(&pfd, 1, nanoTimeout / NET_NSEC_PER_MSEC);
if (connect_rv >= 0) {
break;
}
if (errno != EINTR) {
break;
}
/*
* The poll was interrupted so adjust timeout and
* restart
*/
newNanoTime = JVM_NanoTime(env, 0);
nanoTimeout -= (newNanoTime - prevNanoTime);
if (nanoTimeout < NET_NSEC_PER_MSEC) {
connect_rv = 0;
break;
}
prevNanoTime = newNanoTime;
} /* while */
这是一个循环, 只有满足某些条件, 才会跳出. 涉及到 timeout
的地方, 有2处, 一处是 NET_Poll
, 它的定义在: https://github.com/openjdk/jdk11/blob/37115c8ea4aff13a8148ee2b8832b20888a5d880/src/java.base/linux/native/libnet/linux_close.c#L407, 可以看到, 它其实是调用了 系统调用 poll
, 这个 poll
可以传入一个timeout, 当timeout的时候, 返回0. 另外一处是下面的每次 nanoTimeout -= (newNanoTime - prevNanoTime)
, 相当于把已经过去的时间剪掉, 看 timeout 还剩余多少. 但是不论那种方式timeout, connect_rv
的值都会是0.
所以, 我们可以看到接着就有了下面的代码:
if (connect_rv == 0) {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
"connect timed out");
...省略 ...
}
所以, 真正的 timeout 的处理, 要么是在 poll
的系统调用, 要么是在一个循环里面每次循环递减, 直到减没.
在结尾时候, 我们可以思考这吗一个问题: 当不设置timeout, 真的就永远等下去吗?