[实战]http client 无限等待返回结果

一、业务问题

公司内部有一个爬虫系统,定时抓取部分网站数据存入我们数据库,进行后续约业务逻辑处理。业务反馈有时有些网址的数据没有抓取.

二、相关背景

软件系统: 

采用httpclient 4.5.3 + webmagic实现,定时作务触发后,从数据库分页取出需要处理的数据,新建一个线程,通过httpclient获取数据进行后续处理。

爬虫在访问目标网站时,使用了免费代理,代理稳定型较差。

该项目个人认为是一个网络IO密集型项目。

 

硬件系统: 

服务是在Docker虚拟机上,分了2核8G内存,公司标配。

二、排查问题

首先我通过top -H 查看机器负载相关信息,发现cpu使用量比较少,但负载很高,基本上到10了(没有截图,大家靠想象吧)。由于该项目我认为是一个网络io密集型项目,且同时使用了大量的不稳定的代理,初步推测是因为大量代理不可用,httpclient访问目标网站时间比较长,同时负载较高,导致任务没有执行完成,于是找运维沟通负载较高原因,运维反馈,在Docker虚机中使用top查询的信息其实是物理机器的负载(个人对Docker不熟悉), 对于物理机来讲,这样的负载正常 ,此条线索断.

之后查询日志发现一次定时任务触发后,需要抓取3000条数据,但httpclient请求远程地址有返回结果的只有2800多条(成功和失败都算,超时在失败中计算),有200多条一直没有返回。

httpclient访问url处代码类似下面

 public static  void test() {
        String url = "https://www.baidu.com/";
        String ip = "180.126.138.27";
        int port = 8118;


        CloseableHttpClient httpclient = HttpClients.custom().setRetryHandler(getHttpRequestRetryHandler()).build();
        HttpGet httpGet = new HttpGet(url);
        Map<String,String> headerMap = new HashMap<>();
        //直接在这个位置设置userAgent
        headerMap.put("User-Agent", "\"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1\"");
        if (headerMap != null){
            for (String key : headerMap.keySet()){
                httpGet.setHeader(key,headerMap.get(key));
            }
        }

        httpGet.setConfig(getRequestConfig(ip, port));
        CloseableHttpResponse response = null;
        try {
            response = httpclient.execute(httpGet);
            HttpEntity entity = response.getEntity();
            System.out.println(EntityUtils.toString(entity, "UTF-8").length());
        } catch(Exception e) {
            e.printStackTrace();
        }finally {
            if (response != null) {
                try {
                    response.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static RequestConfig getRequestConfig(String ip, int port){
        RequestConfig.Builder builder = RequestConfig.custom()
                .setSocketTimeout(5000)
                .setConnectTimeout(5000)
                .setMaxRedirects(20)
                .setConnectionRequestTimeout(5000);
        if (ip != null && !"".equals(ip)){
            HttpHost httpHost = new HttpHost(ip,port);
            builder.setProxy(httpHost);
        }
        return builder.build();
    }

    private static HttpRequestRetryHandler getHttpRequestRetryHandler(){
        HttpRequestRetryHandler httpRequestRetryHandler = (exception, executionCount, context) -> {
            if (executionCount > 1) {
                return false;
            }
            if (exception instanceof NoHttpResponseException) {
                return true;
            }
            if (exception instanceof SSLHandshakeException) {
                return false;
            }
            if (exception instanceof InterruptedIOException) {
                return false;
            }
            if (exception instanceof UnknownHostException) {
                return false;
            }
            if (exception instanceof ConnectTimeoutException) {
                return false;
            }
            if (exception instanceof SSLException) {
                return false;
            }

            return false;
        };
        return httpRequestRetryHandler;
    }

 

于是使用jstack打印出线程信息(这个线程信息其实是我用上面的测试代码进行模仿访问出现相同情况时,打出来的,线上机器没权限)

 

   java.lang.Thread.State: RUNNABLE
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
	at java.net.SocketInputStream.read(SocketInputStream.java:171)
	at java.net.SocketInputStream.read(SocketInputStream.java:141)
	at org.apache.http.impl.conn.LoggingInputStream.read(LoggingInputStream.java:84)
	at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:137)
	at org.apache.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:153)
	at org.apache.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:282)
	at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:138)
	at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56)
	at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259)
	at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163)
	at org.apache.http.impl.conn.CPoolProxy.receiveResponseHeader(CPoolProxy.java:165)
	at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:273)
	at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:125)
	at org.apache.http.impl.execchain.MainClientExec.createTunnelToTarget(MainClientExec.java:473)
	at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:398)
	at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:237)
	at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:185)
	at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
	at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:111)
	at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108)
	at com.ziroom.minsu.spider.Test.cc(Test.java:68)
	at com.ziroom.minsu.spider.Test.main(Test.java:44)

   Locked ownable synchronizers:
	- None

大量线程一直在等待socketRead0访问返回。

大家都知道 http底层协议是基于tcp的,有两个超时时间,一个是连接超时时间,另一个是连接成功后,多长时间数据没有返回断开连接。

http client最底层在访问服务端时,一定是通过socket来实现的,在java socket中有两个相关参数

 

sock.setSoTimeout();  // 连接成功后,多长时间数据没有返回断开连接
sock.connect(remoteAddress, connectTimeout); // connectTimeout, 连接超时时间

 

但上面代码中明显是将所有时间设置了。

 

        RequestConfig.Builder builder = RequestConfig.custom()
                .setSocketTimeout(5000) //就是socket中的soTimeOut,翻看源码注释上面写了
                .setConnectTimeout(5000) //就是socket中的connectTimeout
                .setMaxRedirects(20)
                .setConnectionRequestTimeout(5000); //从连接池中获取连接的超时时间

于是翻阅httpclient源码,发现虽然在RequestConfig设置了SocketTimeout,但在最底层设置SoTimeout时并未使用,socket中如果不设置该值则会导致socket连接成功后如果不返回数据,则该socket会一直等待。

DefaultHttpClientConnectionOperator类connect方法相关代码
Socket sock = sf.createSocket(context);
sock.setSoTimeout(socketConfig.getSoTimeout()); //跟代码你就会发现这个值为空
sock.setReuseAddress(socketConfig.isSoReuseAddress());
sock.setTcpNoDelay(socketConfig.isTcpNoDelay());

设置SoTimeout后,问题解决。设置方法

SocketConfig socketConfig = SocketConfig.custom().setSoTimeout(3000).build();
CloseableHttpClient httpclient = HttpClients.custom().setDefaultSocketConfig(socketConfig).setRetryHandler(getHttpRequestRetryHandler()).build();

问题解决.

三、SocketConfig属性说明

private final int soTimeout; 
private final boolean soReuseAddress; //
private final int soLinger;
private final boolean soKeepAlive;
private final boolean tcpNoDelay;
private final int sndBufSize;
private final int rcvBufSize;
private final int backlogSize;

 

So作为前缀。其实这个So就是Socket Option的缩写

SoTimeout:

设置socket调用InputStream读数据的超时时间,以毫秒为单位,如果超过这个时候,会抛出java.net.SocketTimeoutException,

默认值-1, socket中如果不设置该值则会导致socket连接成功后如果不返回数据,则该socket会一直等待.

 

soReuseAddress:

在TCP断开链接的时候我们需要四次握手来断开,而且当两端都关闭了read/write通道以后我们还是要等待一个TIME_WAIT时间
SO_REUSEADDR的作用:诉OS如果一个端口处于TIME_WAIT状态, 那么我们就不用等待直接进入使用模式, 不需要继续等待这个时间结束

 

为什么要等待一个TIME_WAIT时间?

看看TCP/IP协议组我们就知道,这样做是为了让在网络中残余的TCP包消失, 也就是说, 如果我们没有等到这个时间就让OS把这个端口释放给其他的进程使用,别的进程很有可能就会收到上一个会话的残余TCP包,这样就会出现一系列的不可预知的错误


SO_REUSEADDR可以用在以下四种情况下。
(摘自《Unix网络编程》卷一,即UNPv1)
1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启
动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。
2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但
每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可
以测试这种情况。
3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个soc
ket绑定的ip地址不同。这和2很相似,区别请看UNPv1。
4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的
多播,不用于TCP

 

soLinger:

在Java Socket中,当我们调用Socket的close方法时,默认的行为是当底层网卡所有数据都发送完毕后,关闭连接
通过setSoLinger方法,我们可以修改close方法的行为
1,setSoLinger(true, 0)
当网卡收到关闭连接请求后,无论数据是否发送完毕,立即发送RST包关闭连接
2,setSoLinger(true, delay_time)
当网卡收到关闭连接请求后,等待delay_time
如果在delay_time过程中数据发送完毕,正常四次挥手关闭连接
如果在delay_time过程中数据没有发送完毕,发送RST包关闭连接

 

说明:

RST包用于强制关闭TCP链接。

TCP连接关闭的正常方法是四次握手。但四次握手不是关闭TCP连接的唯一方法. 有时,如果主机(发起关闭的一方)需要尽快关闭连接(或连接超时,端口或主机不可达),RST (Reset)包将被发送. 注意,由于RST包不是TCP连接中的必须部分, 可以只发送RST包(即不带ACK标记). 但在正常的TCP连接中RST包可以带ACK确认标记

 

tcpNoDelay:

数据包较小时不延迟发送.(true:不延迟,false延迟默认值)

在默认情况下,客户端向服务器发送数据时,会根据数据包的大小决定是否立即发送。当数据包中的数据很少时,如只有1个字节,而数据包的头却有几十个字节(IP头+TCP头)时,系统会在发送之前先将较小的包合并到软大的包后,一起将数据发送出去。在发送下一个数据包时,系统会等待服务器对前一个数据包的响应,当收到服务器的响应后,再发送下一个数据包,这就是所谓的Nagle算法;在默认情况下,Nagle算法是开启的。


这种算法虽然可以有效地改善网络传输的效率,但对于网络速度比较慢,而且对实现性的要求比较高的情况下(如游戏、Telnet等),使用这种方式传输数据会使得客户端有明显的停顿现象。因此,最好的解决方案就是需要Nagle算法时就使用它,不需要时就关闭它。而使用setTcpToDelay正好可以满足这个需求。当使用setTcpNoDelay(true)将Nagle算法关闭后,客户端每发送一次数据,无论数据包的大小都会将这些数据发送出去。

sndBufSize:
在默认情况下,输出流的发送缓冲区是8096个字节(8K)。这个值是Java所建议的输出缓冲区的大小。如果这个默认值不能满足要求,可以用setSendBufferSize方法来重新设置缓冲区的大小。但最好不要将输出缓冲区设得太小,否则会导致传输数据过于频繁,从而降低网络传输的效率。
如果底层的Socket实现不支持SO_SENDBUF选项,这两个方法将会抛出SocketException例外。必须将size设为正整数,否则setSendBufferedSize方法将抛出IllegalArgumentException例外

rcvBufSize:
在默认情况下,输入流的接收缓冲区是8096个字节(8K)。这个值是Java所建议的输入缓冲区的大小。如果这个默认值不能满足要求,可以用setReceiveBufferSize方法来重新设置缓冲区的大小。但最好不要将输入缓冲区设得太小,否则会导致传输数据过于频繁,从而降低网络传输的效率。
如果底层的Socket实现不支持SO_RCVBUF选项,这两个方法将会抛出SocketException例外。必须将size设为正整数,否则setReceiveBufferSize方法将抛出IllegalArgumentException例外

 

 

backlogSize:

 

 

没有找到过多关于这个的说明

 

四、说明

我个人虽然工作很久,但刚刚开始写博客,写得不好还请大家见谅。定的目标是每周一篇博客,本来打算这周写纸上谈定系统,但我具体还没有思考清楚,于是先把我这两天遇到的问题写成一篇实战,让大家看看。自已写了这几篇博客后,发现真的很不容易,耗费很大精力,而且写出来还不是自己想要的。还请大家多多指点

五、参考:

http://elf8848.iteye.com/blog/1739598

https://www.cnblogs.com/kex1n/p/7437290.html

其实还参考了很多,但没有记下来

 

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值