背景
在远程调用的世界里,Timeout 的情况非常常见,几乎每段时间就会听到几个同事关于 Timeout 各种情况的讨论,偶尔的会出现不同开发语言间的同事的讨论,例如 read timeout, 语言的隔阂使得大家讨论的都不知道是否是同一回事。
对于 Java,各种远程调用,http,hessian,dubbo 什么的,抛个 timeout 异常也是常见的事情,timeout 是什么,一般追追源码,追到最后发现是个 native 方法,看着 javadoc, 了解得不甚透彻。 所以本文尽量从 Java 到操作系统层面尝试说明常见的各种 Timeout。
主要内容
现象
对于 Java 开发来说,最常见的异常莫过于 SocketTimeoutException,从异常日志,一般会有两种情况
connect timed out
read timed out
Caused by: java.net.SocketTimeoutException: connect timed out
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:345)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:170)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
复制代码
原理 connect timed out
"connect timed out"从字面上看就是连接的超时时间,那么超时时间是怎么控制的?
java.net.Socket
从 Sokcet 的 connect 方法可以看出,timeout 参数会一致往下传递,最后到了 PlainSocketImpl.socketConnect 的 native 方法, java native 方法是否真的很神秘?也不神秘,让我们一起看下 JVM 底层的实现,以下是 jdk8-openjdk 的源码
PlainSocketImpl.c
以下只截取部分重要的源码, 从源码上看,没设置超时时间时,jvm 采用 connect 的传统阻塞式方式,反之,则采用 select/poll 非阻塞式的方式, 由于 poll/select 都是得采用轮询的方式,在客户端没有设置超时的时候,采用轮询会带来不必要的开销,所以没设置超时时采用 connect 的阻塞方式是合理的
JNIEXPORT void JNICALL
Java_java_net_PlainSocketImpl_socketConnect(JNIEnv *env, jobject this,
jobject iaObj, jint port,
jint timeout)
{
if (timeout < 0 ) {
connect_rv = NET_Connect(fd, (struct sockaddr *)&him, len);
...
}
else {
#ifndef USE_SELECT
{
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLOUT;
errno = 0;
connect_rv = NET_Poll(&pfd, 1, timeout);
}
#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);
}
#endif
}
if (connect_rv == 0) {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
"connect timed out");
/*
* 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;
}
/* has connection been established */
optlen = sizeof(connect_rv);
if (JVM_GetSockOpt(fd, SOL_SOCKET, SO_ERROR, (void*)&connect_rv,
&optlen) <0) {
connect_rv = errno;
}
}
}
复制代码
原理 Read timed out
从下面这个时序图看, read timedout 的原理就是通过系统调用 poll, 传入对应的 socket 文件句柄,在 timeout 时间内没有数据返回
当调用 NET_Timeout 没返回任何数据的时候, 根据情况会抛出 SocketTimeoutException 或者 SokcetException, 这个 SocketTimeoutException 就是我们经常遇到的 read timed out
if (timeout) {
nread = NET_Timeout(fd, timeout);
if (nread <= 0) {
if (nread == 0) {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
"Read timed out");
} else if (nread == JVM_IO_ERR) {
if (errno == EBADF) {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException", "Socket closed");
} else {
NET_ThrowByNameWithLastError(env, JNU_JAVANETPKG "SocketException",
"select/poll failed");
}
} else if (nread == JVM_IO_INTR) {
JNU_ThrowByName(env, JNU_JAVAIOPKG "InterruptedIOException",
"Operation interrupted");
}
if (bufP != BUF) {
free(bufP);
}
return -1;
}
}
复制代码
总结
"connect timed out" 是在指定时间内 TCP 连接未创建成功时 jdk 抛出的异常
"Read timed out"是在调用 socketread 后,指定时间内未收到响应时 jdk 抛出的异常, 假如一个 http 响应 10k, 每次 socket read 4k, 那么就需要发起 3 次 read 的请求,假如 timeout 设置 3 秒,那么就允许每次 read 都等待 3 秒,最差的情况就是大概 9 秒读完数据,当然这得是极端的网络情况, 所以大部分情况下都是客户端发起请求后,在指定时间内收到的服务器的回包响应。
评论