背景:
需要去监控某个网站,所以写了一个爬虫程序,被爬取的链接是Https,使得的是网上的代理,按ip使用量计费,该计费模式确实好用!
框架:httpClient 4.5.10
Java: Java 9
implementation 'org.apache.httpcomponents:httpclient:4.5.10'
问题:
然后问题出现了,因为是一个监控程序,所以需要不断的轮询,然后开了10个左右线程轮询,结果跑了半小时后,10个线程全部刮起,thread dump一下发现每个线程都如下:
"http-nio-8081-exec-7@5795" daemon prio=5 tid=0x2a nid=NA runnable java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(SocketInputStream.java:-1) 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 sun.security.ssl.InputRecord.readFully(InputRecord.java:465) at sun.security.ssl.InputRecord.read(InputRecord.java:503) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:975) - locked <0x1c8d> (a java.lang.Object) at sun.security.ssl.SSLSocketImpl.readDataRecord(SSLSocketImpl.java:933) at sun.security.ssl.AppInputStream.read(AppInputStream.java:105) - locked <0x1c8e> (a sun.security.ssl.AppInputStream) 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:280) at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:138) at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56)
WTF?一直卡在 java.net.SocketInputStream.socketRead0 方法上,难道是我没有设置超时逻辑?于是赶紧去查找代码,加上逻辑:
HttpClient httpClient = HttpClientBuilder.create() .setConnectionManager(connManager) .setConnectionTimeToLive(20000L, TimeUnit.MILLISECONDS) .setRetryHandler(getRetryHandler()) .setDefaultCredentialsProvider(getProxyProvider()) .build();
自信的跑了一段时间后,无用还是挂着,于是上网看看,发现很多人都遇到过这种事,但没人说明是为啥?有几种可能:
- httpClient某个版本的bug,在某个版本后修复了,然后检查发现,我的版本是最新的,没问题
- 某博主也是用的代理,情况和我一摸一样,然后搞不出问题,所以就绕过去了,kill thread ,这真是神操作
分析:
现在卡在Runnable上面,而且是“阻塞”,当然不是获取锁的block状态,而是同步等待的意思;这个就比较有意思了,那么此刻线程在干嘛呢?我们分析下SocketInputStream.socketRead0这个方法到底在干嘛!
解决:
因为是用的是框架,对框架也不熟,所以直觉告诉我需要先考虑是框架的问题,所以开始对httpClient的使用开始研究,太恶心了,这个框架每个版本的使用方法都不太一样;最终我发现两种设置超时的方法,一个是掌控应用层的RequestConfig,第二种是掌控Socket的SocketConfig,这个时候我已经猜到了问题原因,这次信心慢慢,于是我加入了以下代码,发现问题神奇的解决了。
BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager(); connManager.setSocketConfig(SocketConfig.custom().setSoTimeout(2000).build()); HttpClient httpClient = HttpClientBuilder.create() .setConnectionManager(connManager) .setConnectionTimeToLive(20000L, TimeUnit.MILLISECONDS) .setRetryHandler(getRetryHandler()) .setDefaultCredentialsProvider(getProxyProvider()) .build();