关于HTTPClient使用代理poxy请求导致socketRead0线程长时间挂起解决

问题描述:

项目场景:实际项目创建一个固定数量的线程池,用来消费用户数据,但偶尔会发现有些线程消失了,也就是线程不进行工作,实际工作的线程数量一直减少,直至彻底不消费用户数据。

Executor executor = Executors.newFixedThreadPool(100);

原因分析:

首先排除代码的逻辑错误,确保没有出现死锁等情况。在确保没有死锁的情况下,通过Thread Dump得到日志进一步分析。

"pool-6-thread-29" #118 prio=5 os_prio=0 tid=0x00007ff9dc006800 nid=0x6ee0 runnable [0x00007ff9c59e6000]
   java.lang.Thread.State: RUNNABLE
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.read(SocketInputStream.java:150)
	at java.net.SocketInputStream.read(SocketInputStream.java:121)
	at sun.security.ssl.InputRecord.readFully(InputRecord.java:465)
	at sun.security.ssl.InputRecord.readV3Record(InputRecord.java:593)
	at sun.security.ssl.InputRecord.read(InputRecord.java:532)
	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:954)
	- locked <0x00000006d298e328> (a java.lang.Object)
	at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1343)
	- locked <0x00000006d298e338> (a java.lang.Object)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1371)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1355)
	at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:396)
	at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.upgrade(DefaultHttpClientConnectionOperator.java:193)
	at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.upgrade(PoolingHttpClientConnectionManager.java:389)
	at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:416)
	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:56)
	at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:89)
	at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
	at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53)
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:724)
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:681)
	at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:597)
	....

通过任务对应得到“消失的线程”,得到线程的name,拿到线程对应具体状态RUNNABLE。
在java的文档里对RUNNABLE的描述是这样的:

A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.
处于 runnable 状态下的线程正在 Java 虚拟机中执行,但它可能正在等待来自于操作系统的其它资源,比如处理器。

根据堆栈信息,可以看到具体出问题的是使用RestTemplate请求的时候出现问题,有可能是一直等待网络资源。
出现问题的RestTemplate由于业务原因,没有使用默认的SimpleClientHttpRequestFactory,而是使用了HttpClient作为RequestFactory,这里拓展一下,RestTemplate可以设置不同的RequestFactory,通过源码查看,内置已经支持如下:
在这里插入图片描述通过源码直接Debugger到java.net.SocketInputStream,会发现加了代理的请求,请求的SocketTimeout为0,这是最直接的原因,导致了线程一直在登录服务端的返回,彻底阻塞,影响后续任务。
在这里插入图片描述但项目实际设置了ConnectTimeout和SocketTimeout,而且在非代理请求情况,这里显示的是设置的SocketTimeout值,但加了代理后,就出现了0,推测是加了代理,HTTPClient内部的一些问题。

 RequestConfig config = RequestConfig.custom()
 .setConnectTimeout(httpConfig.getConnectTimeout())
 .setSocketTimeout(httpConfig.getReadTimeout())
 .setProxy(httpConfig.getProxy())
 .setRedirectsEnabled(httpConfig.isRedirect()).build();

源码分析:

https请求过程中,加代理和不加代理的主要区别在于创建路由过程不一样。 创建路由方法位于:org.apache.http.impl.execchain.MainClientExec#establishRoute

在这里插入图片描述

圈红框部分的代码,就是代理为了创建代理路由需要额外运行的代码,重点在:

 case HttpRouteDirector.CONNECT_PROXY:
                this.connManager.connect(
                        managedConn,
                        route,
                        timeout > 0 ? timeout : 0,
                        context);
                final HttpHost proxy  = route.getProxyHost();
                tracker.connectProxy(proxy, false);
                break;

其中在创建SocketConfig的过程中,代码如下:
org.apache.http.impl.conn.PoolingHttpClientConnectionManager#resolveSocketConfig


    private SocketConfig resolveSocketConfig(final HttpHost host) {
        SocketConfig socketConfig = this.configData.getSocketConfig(host);
        if (socketConfig == null) {
            socketConfig = this.configData.getDefaultSocketConfig();
        }
        if (socketConfig == null) {
            socketConfig = SocketConfig.DEFAULT;
        }
        return socketConfig;
    }

该方法默认中,socketConfig 、this.configData.getDefaultSocketConfig();都为空,所以代理路由Route使用了默认的SocketConfig.DEFAULT。
查看org.apache.http.config.SocketConfig的默认对象:
在这里插入图片描述此处默认的SocketTimeout为0,没有超时,一直阻塞。
到此为止,定位到了具体的错误根源。

根据资料查找
1、httpclient 4.3.6之前的一个bug,会导致连接一直阻塞,导致不会超时,但检查项目httpclient版本高于4.3.6。
2、官方已经提示jdk8的某些版本存在该bug,但新版本已经修复,本次出现问题的jdk版本为1.8.0_25和1.8.0_191,处于未修复的版本,需要自己项目代码进行修复。
[JDK-8075484] SocketInputStream.socketRead0 can hang even with soTimeout set - Java Bug System
https://bugs.openjdk.java.net/browse/JDK-8075484#


解决方案:


    private SocketConfig resolveSocketConfig(final HttpHost host) {
        SocketConfig socketConfig = this.configData.getSocketConfig(host);
        if (socketConfig == null) {
            socketConfig = this.configData.getDefaultSocketConfig();
        }
        if (socketConfig == null) {
            socketConfig = SocketConfig.DEFAULT;
        }
        return socketConfig;
    }

从上面的 this.configData.getDefaultSocketConfig();入手,设置DefaultSocketConfig();


        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(httpConfig.getConnectTimeout())
                .setSocketTimeout(httpConfig.getReadTimeout())
                .setProxy(httpConfig.getProxy())
                .setRedirectsEnabled(httpConfig.isRedirect()).build();
        SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
                getSSLContext(), new String[]{"TLSv1"}, null, 
                NoopHostnameVerifier.INSTANCE);

        CloseableHttpClient httpClient = HttpClients.custom()
                .setDefaultRequestConfig(config).setSSLSocketFactory(sslSocketFactory)
                .setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36")
                //这里修改默认配置里的的SocketTimeout为40秒
                .setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(40000).build()).build();

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient));

重点就是加了一行代码:.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(40000).build())

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值