前几天一个机房网络抖动,引发了很多对外请求的超时问题,在发生问题排查日志的时候,发现了这么一个现象,httpclient我们的请求超时时间并没有按照我们的设置报超时异常

我们的大概配置如下:

RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(1000)
.setSocketTimeout(2000)
.setConnectionRequestTimeout(1000)
.build();

但实际却发现很多请求超时时间都达到了10几秒甚至有的二十几秒,大大超过了我们的预期时间,决定通过跟踪源码一探究竟:

原来http读取网络数据的时候是其实是使用的BufferedReader类,而我们知道java的io类其实都是对基本输入流的装饰,其底层其实是利用的SocketInputStream来读取数据,一路代码跟踪,我们跟踪到了这个方法

int read(byte b[], int off, int length, int timeout) throws IOException {
int n = 0; // EOF already encountered
if (eof) {
return -1;
} // connection reset
if (impl.isConnectionReset()) {
throw new SocketException("Connection reset");
} // bounds check
if (length <= 0 || off < 0 || off + length > b.length) {
if (length == 0) {
return 0;
}
throw new ArrayIndexOutOfBoundsException();
} boolean gotReset = false; Object traceContext = IoTrace.socketReadBegin();
// acquire file descriptor and do the read
FileDescriptor fd = impl.acquireFD();
try {
n = socketRead0(fd, b, off, length, timeout);
if (n > 0) {
return n;
}
} catch (ConnectionResetException rstExc) {
gotReset = true;
} finally {
impl.releaseFD();
IoTrace.socketReadEnd(traceContext, impl.address, impl.port,
timeout, n > 0 ? n : 0);
} /*
* We receive a "connection reset" but there may be bytes still
* buffered on the socket
*/
if (gotReset) {
traceContext = IoTrace.socketReadBegin();
impl.setConnectionResetPending();
impl.acquireFD();
try {
n = socketRead0(fd, b, off, length, timeout);
if (n > 0) {
return n;
}
} catch (ConnectionResetException rstExc) {
} finally {
impl.releaseFD();
IoTrace.socketReadEnd(traceContext, impl.address, impl.port,
timeout, n > 0 ? n : 0);
}
} /*
* If we get here we are at EOF, the socket has been closed,
* or the connection has been reset.
*/
if (impl.isClosedOrPending()) {
throw new SocketException("Socket closed");
}
if (impl.isConnectionResetPending()) {
impl.setConnectionReset();
}
if (impl.isConnectionReset()) {
throw new SocketException("Connection reset");
}
eof = true;
return -1;
}

这个方法的核心其实就是 socketRead0(fd, b, off, length, timeout)这个方法的调用,而这个方法是这样的:

private native int socketRead0(FileDescriptor fd,
byte b[], int off, int len,
int timeout)
throws IOException;

这个是native方法,通过下载openjdk1.8源码,我们在openjdk\jdk\src\solaris\native\java\net的目录下找到了相关实现,在SocketInputStream.c文件里,代码如下:

Java_java_net_SocketInputStream_socketRead0(JNIEnv *env, jobject this,
jobject fdObj, jbyteArray data,
jint off, jint len, jint timeout)
{
char BUF[MAX_BUFFER_LEN];
char *bufP;
jint fd, nread; if (IS_NULL(fdObj)) {
/* shouldn't this be a NullPointerException? -br */
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
"Socket closed");
return -;
} else {
fd = (*env)->GetIntField(env, fdObj, IO_fd_fdID);
/* Bug 4086704 - If the Socket associated with this file descriptor
* was closed (sysCloseFD), then the file descriptor is set to -1.
*/
if (fd == -) {
JNU_ThrowByName(env, "java/net/SocketException", "Socket closed");
return -;
}
} /*
* If the read is greater than our stack allocated buffer then
* we allocate from the heap (up to a limit)
*/
if (len > MAX_BUFFER_LEN) {
if (len > MAX_HEAP_BUFFER_LEN) {
len = MAX_HEAP_BUFFER_LEN;
}
bufP = (char *)malloc((size_t)len);
if (bufP == NULL) {
bufP = BUF;
len = MAX_BUFFER_LEN;
}
} else {
bufP = BUF;
} if (timeout) {
nread = NET_Timeout(fd, timeout);
if (nread <= ) {
if (nread == ) {
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 if (errno == ENOMEM) {
JNU_ThrowOutOfMemoryError(env, "NET_Timeout native heap allocation failed");
} 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 -;
}
} nread = NET_Read(fd, bufP, len); if (nread <= ) {
if (nread < ) { switch (errno) {
case ECONNRESET:
case EPIPE:
JNU_ThrowByName(env, "sun/net/ConnectionResetException",
"Connection reset");
break; case EBADF:
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
"Socket closed");
break; case EINTR:
JNU_ThrowByName(env, JNU_JAVAIOPKG "InterruptedIOException",
"Operation interrupted");
break; default:
NET_ThrowByNameWithLastError(env,
JNU_JAVANETPKG "SocketException", "Read failed");
}
}
} else {
(*env)->SetByteArrayRegion(env, data, off, nread, (jbyte *)bufP);
} if (bufP != BUF) {
free(bufP);
}
return nread;
}

通过代码我们可以知道,数据的读取是通过NET_Timeout (fd, timeout)来实现的,我们继续跟踪代码,在linux_close.c文件中,发现了NET_Timeout的实现:

int NET_Timeout(int s, long timeout) {
long prevtime = , newtime;
struct timeval t;
fdEntry_t *fdEntry = getFdEntry(s); /*
* Check that fd hasn't been closed.
*/
if (fdEntry == NULL) {
errno = EBADF;
return -;
} /*
* Pick up current time as may need to adjust timeout
*/
if (timeout > ) {
gettimeofday(&t, NULL);
prevtime = t.tv_sec * + t.tv_usec / ;
} for(;;) {
struct pollfd pfd;
int rv;
threadEntry_t self; /*
* Poll the fd. If interrupted by our wakeup signal
* errno will be set to EBADF.
*/
pfd.fd = s;
pfd.events = POLLIN | POLLERR; startOp(fdEntry, &self);
rv = poll(&pfd, , timeout);
endOp(fdEntry, &self); /*
* If interrupted then adjust timeout. If timeout
* has expired return 0 (indicating timeout expired).
*/
if (rv < && errno == EINTR) {
if (timeout > ) {
gettimeofday(&t, NULL);
newtime = t.tv_sec * + t.tv_usec / ;
timeout -= newtime - prevtime;
if (timeout <= ) {
return ;
}
prevtime = newtime;
}
} else {
return rv;
} }
}

代码中的关键点在 poll(&pfd, 1, timeout);poll是linux中的字符设备驱动中的一个函数,作用是把当前的文件指针挂到设备内部定义的等待

这样就很好理解了,其实这个时间是我两次读取数据之间的最长阻塞时间,如果我在网络抖动的情况下,我每次2秒之内返回一部分数据,这样我就一直不会超时了,为了验证我们的理解写了test,代码如下,一个controller,用来接受http请求:

@org.springframework.stereotype.Controller
@RequestMapping("/hello")
public class Controller {
@RequestMapping("/test")
public void tets(HttpServletRequest request ,HttpServletResponse response) throws IOException, InterruptedException {
System.out.println("I'm coming");
PrintWriter writer = response.getWriter();
while (true){
writer.print("ha ha ha");
writer.flush();
Thread.sleep(2000);
System.out.println("I'm ha ha ha");
}
}
}

这个代码就是每隔2s发送一条数据,循环发送,模拟网络不好的时候,收到的数据断断续续,再来一个test用来发送请求:

 @Test
public void tetsHttpClientHttp() throws IOException {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(1000)
.setSocketTimeout(3000)
.setConnectionRequestTimeout(1000)
.build(); CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).build();
// 创建Get请求
HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/hello/test");
CloseableHttpResponse response =httpClient.execute(httpGet);
HttpEntity responseEntity = response.getEntity();
if (responseEntity != null) {
System.out.println("响应内容为:" + EntityUtils.toString(responseEntity));
}
}

服务端结果如下:

客户端结果如下:

程序并没有如期抛出异常,和我们预想的一样,而当我们修改socketTimeout为1000时,经验证可以抛出java.net.SocketTimeoutException: Read timed out 异常

为此,为了更准确控制时间,我们需要自己实现超时机制:

ExecutorService executor = Executors.newFixedThreadPool(1);
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(1000)
.setSocketTimeout(3000)
.setConnectionRequestTimeout(1000)
.build(); CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).build();
// 创建Get请求
HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/hello/test");
CloseableHttpResponse response =httpClient.execute(httpGet);
HttpEntity responseEntity = response.getEntity();
return EntityUtils.toString(responseEntity);
}
};
Future<String> future = executor.submit(callable);
System.out.print(future.get(5,TimeUnit.SECONDS));

这样就可以避免这种情况,在请求线程超时时抛出 java.util.concurrent.TimeoutException避免长时间占住业务线程影响我们的服务,当然这只是个例子,现实我们可能还要考虑线程数,拒绝策略等情况。

大多数人可能都不会使用socketTimeout,看了底层才知道一直都做错了的更多相关文章

  1. 在知乎上看到 Web Socket这篇文章讲得确实挺好,从头看到尾都非常形象生动,一口气看完,没有半点模糊,非常不错

    在知乎上看到这篇文章讲得确实挺好,从头看到尾都非常形象生动,一口气看完,没有半点模糊,非常不错,所以推荐给大家,非常值得一读. 作者:Ovear链接:https://www.zhihu.com/que ...

  2. C#本质论第四版-1,抄书才能看下去,不然两三眼就看完了,一摞书都成了摆设。抄下了记忆更深刻

    C#本质论第四版-1,抄书才能看下去,不然两三眼就看完了,一摞书都成了摆设.抄下了记忆更深刻 本书面向的读者 写作本书时,我面临的一个挑战是如何持续吸引高级开发人员眼球的同时,不因使用assembly ...

  3. 很多事情就像看A片,看的人觉得很爽,做的人未必。

    http://m.jingdianju.com/wzgs/shenghuo/201307185135.html 转载自: 从这个角度上来说,我不太赞成过于关注第一份工作的薪水,更没有必要攀比第一份工作 ...

  4. php面试题9(看的时候就应该随手截图做笔记的)

    php面试题9(看的时候就应该随手截图做笔记的) 一.总结 看的时候就应该随手截图做笔记的 二.php面试题9 一.选择题:1.下面哪个表达式不能将两个字符串$s1 和$s2 串联成一个单独的字符串? ...

  5. Google Protocol Buffer 的使用和原理(无论对存储还是数据交换,都是个挺有用的东西,有9张图做说明,十分清楚)

    感觉Google Protocol Buffer无论对存储还是数据交换,都是个挺有用的东西,这里记录下,以后应该用得着.下文转自: http://www.ibm.com/developerworks/ ...

  6. Owin的URL编码怎么搞?以前都是HttpUtility.UrlEncode之类的,现在连system.web都没了,肿么办?

    Owin的URL编码怎么搞?以前都是HttpUtility.UrlEncode之类的,现在连system.web都没了,肿么办? 编码: Uri.EscapeDataString(name) 解码: ...

  7. 那么都数据库表,那么多不同记录。是怎样都存储在一个key-value数据库的?

    那么都数据库表,那么多不同记录.是怎样都存储在一个key-value数据库的? :设置不同的键值而已!不同的表,选出统一的key规范 jedis.sadd("tom:friend:list& ...

  8. 为什么每个请求都要有用户名密码呢,那不是每次都要查询一下了,token,表示这个用户已经验证通过了,在token有效期内,只需要判断token是否有效就可以了

    为什么每个请求都要有用户名密码呢,那不是每次都要查询一下了,token,表示这个用户已经验证通过了,在token有效期内,只需要判断token是否有效就可以了

  9. hdu6055 Regular polygon 脑洞几何 给定n个坐标(x,y)。x,y都是整数,求有多少个正多边形。因为点都是整数点,所以只可能是正四边形。

    /** 题目:hdu6055 Regular polygon 链接:http://acm.hdu.edu.cn/showproblem.php?pid=6055 题意:给定n个坐标(x,y).x,y都 ...

随机推荐

  1. java实现 洛谷 P1425 小鱼的游泳时间

    题目描述 伦敦奥运会要到了,小鱼在拼命练习游泳准备参加游泳比赛,可怜的小鱼并不知道鱼类是不能参加人类的奥运会的. 这一天,小鱼给自己的游泳时间做了精确的计时(本题中的计时都按24小时制计算),它发现自 ...

  2. Java实现第九届蓝桥杯倍数问题

    倍数问题 题目描述 [题目描述] 众所周知,小葱同学擅长计算,尤其擅长计算一个数是否是另外一个数的倍数.但小葱只擅长两个数的情况,当有很多个数之后就会比较苦恼.现在小葱给了你 n 个数,希望你从这 n ...

  3. 提高编译速度! 第一次运行需要注释掉,不然会报错,因为需要编译SO库文件 !

    // 提高编译速度! 第一次运行需要注释掉,不然会报错,因为需要编译SO库文件 ! tasks.whenTaskAdded { task -> if (task.name.contains(&q ...

  4. java类的加载顺序和实例化顺序(Demo程序)

    一.main函数中实例化对象 父类 package com.learn; public class Father { //静态变量 public static int num_1 = 1; //静态代 ...

  5. LinkedList竟然比ArrayList慢了1000多倍?(动图+性能评测)

    数组和链表是程序中常用的两种数据结构,也是面试中常考的面试题之一.然而对于很多人来说,只是模糊的记得二者的区别,可能还记得不一定对,并且每次到了面试的时候,都得把这些的概念拿出来背一遍才行,未免有些麻 ...

  6. skfpdb.db、cc3268.dll、system_V2.dat、JI60JS.dat文件内容、发票数据查询

    cc3268.dll.skfpdb.db.xxxxx_V2.dat,system.dat,JI60JS.dat,log.dat,system_V2.dat,JI60JS_V2.dat,log_V2.d ...

  7. WEB应用的常见安全漏洞

      01. SQL 注入 SQL 注入就是通过给 web 应用接口传入一些特殊字符,达到欺骗服务器执行恶意的 SQL 命令.SQL 注入漏洞属于后端的范畴,但前端也可做体验上的优化.原因:当使用外部不 ...

  8. 2、Redis如何配置成一个windows服务并且设置一键安装卸载与启停

    每天启动redis虽然只是一个命令行的事情,但是还是比较烦,所以…… 参考文档:Windows Service Documentation.docx 默认前提:Redis已安装并配置完成(不知道如何配 ...

  9. Windows安装C的编译环境

    对于java开发者来说安装C的编译环境不是非常熟悉,因此本文对C的安装环境进行介绍以及windows编译Redis和Zookeeper的过程.MinGW主要用于按照gcc.make等环境,cywin用 ...

  10. Spring系列.事务管理原理简析

    Spring的事务管理功能能让我们非常简单地进行事务管理.只需要进行简单的两步配置即可: step1:开启事务管理功能 @Configuration //@EnableTransactionManag ...