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, 真的就永远等下去吗?

标签: none

添加新评论