一次Commons-HttpClient的BindException排查
线上有个老应用,在流量增长的时候,HttpClient抛出了BindException。部分的StackTrace信息如下:
java.net.BindException: Address already in use (Bind failed) at
java.net.PlainSocketImpl.socketBind(Native Method) ~[?:1.8.0_162] at
java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387) ~[?:1.8.0_162] at
java.net.Socket.bind(Socket.java:644) ~[?:1.8.0_162] at
sun.reflect.GeneratedMethodAccessor289.invoke(Unknown Source) ~[?:?] at
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_162] at
java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_162] at
org.apache.commons.httpclient.protocol.ReflectionSocketFactory.createSocket(ReflectionSocketFactory.java:139) ~[commons-httpclient-3.1.jar:?] at
org.apache.commons.httpclient.protocol.DefaultProtocolSocketFactory.createSocket(DefaultProtocolSocketFactory.java:125) ~[commons-httpclient-3.1.jar:?] at
org.apache.commons.httpclient.HttpConnection.open(HttpConnection.java:707) ~[commons-httpclient-3.1.jar:?] at
org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$HttpConnectionAdapter.open(MultiThreadedHttpConnectionManager.java:1361) ~[commons-httpclient-3.1.jar:?] at
org.apache.commons.httpclient.HttpMethodDirector.executeWithRetry(HttpMethodDirector.java:387) ~[commons-httpclient-3.1.jar:?] at
org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:171) ~[commons-httpclient-3.1.jar:?] at
org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397) ~[commons-httpclient-3.1.jar:?] at
org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:323) ~[commons-httpclient-3.1.jar:?]`
Ephemeral Port Exhausted
先Google,很多人说是操作系统的临时端口号耗尽了。倒也说得通,线上服务没有连接池,流量一大,HttpClient每创建一个连接就会占用一个临时端口号。
但我还是有疑问。
说疑问之前先简单介绍下临时端口号(Ephemeral Port)。
一个TCP连接由四元组标识:
{source_ip, source_port, destination_ip, destination_port}
对于HttpClient来说,每次都是作为source创建TCP连接,也就是说destination_ip和destination_port是确定的,只需要调用系统调用connect,操作系统会自动分配source_ip和source_port。
这个分配过程不仅HttpClient的使用者不关心,HttpClient的开发者也不用关心。
不过临时端口号对操作系统来说是有限的资源,有个范围限制,同时创建的连接太多,就不够用了。再创建连接,就会报错。
比如下面这条nginx log,就是因为临时端口号耗尽,Nginx无法创建到upstream的连接了:
2016/03/18 09:08:37 [crit] 1888#1888: *13 connect() to 10.2.2.77:8081 failed (99: Cannot assign requested address) while connecting to upstream, client: 10.2.2.42, server: , request: "GET / HTTP/1.1", upstream: "http://10.2.2.77:8081/", host: "10.2.2.77"
这个时候我的疑问来了。
如果原因是临时端口号耗尽,HttpClient为什么会抛出BindException呢?作为创建TCP连接的source这一方,只需要系统调用connect,没必要系统调用bind啊。
如果原因是临时端口号耗尽,像上面nginx log那种错误提示才是合理的吧?
HttpClient 3.1
猜猜猜,猜不出来,只好去看看HttpClient的代码。
老应用之老,不止年纪大,用的三方库的版本也旧。HttpClient还是commons-httpclient-3.1.jar。
package org.apache.commons.httpclient.protocol;
public final class ReflectionSocketFactory:
public static Socket createSocket(
final String socketfactoryName,
final String host,
final int port,
final InetAddress localAddress,
final int localPort,
int timeout)
throws IOException, UnknownHostException, ConnectTimeoutException
{
if (REFLECTION_FAILED) {
//This is known to have failed before. Do not try it again
return null;
}
// This code uses reflection to essentially do the following:
//
// SocketFactory socketFactory = Class.forName(socketfactoryName).getDefault();
// Socket socket = socketFactory.createSocket();
// SocketAddress localaddr = new InetSocketAddress(localAddress, localPort);
// SocketAddress remoteaddr = new InetSocketAddress(host, port);
// socket.bind(localaddr);
// socket.connect(remoteaddr, timeout);
// return socket;
try {
Class socketfactoryClass = Class.forName(socketfactoryName);
Method method = socketfactoryClass.getMethod("getDefault",
new Class[] {});
Object socketfactory = method.invoke(null,
new Object[] {});
method = socketfactoryClass.getMethod("createSocket",
new Class[] {});
Socket socket = (Socket) method.invoke(socketfactory, new Object[] {});
if (INETSOCKETADDRESS_CONSTRUCTOR == null) {
Class addressClass = Class.forName("java.net.InetSocketAddress");
INETSOCKETADDRESS_CONSTRUCTOR = addressClass.getConstructor(
new Class[] { InetAddress.class, Integer.TYPE });
}
Object remoteaddr = INETSOCKETADDRESS_CONSTRUCTOR.newInstance(
new Object[] { InetAddress.getByName(host), new Integer(port)});
Object localaddr = INETSOCKETADDRESS_CONSTRUCTOR.newInstance(
new Object[] { localAddress, new Integer(localPort)});
if (SOCKETCONNECT_METHOD == null) {
SOCKETCONNECT_METHOD = Socket.class.getMethod("connect",
new Class[] {Class.forName("java.net.SocketAddress"), Integer.TYPE});
}
if (SOCKETBIND_METHOD == null) {
SOCKETBIND_METHOD = Socket.class.getMethod("bind",
new Class[] {Class.forName("java.net.SocketAddress")});
}
SOCKETBIND_METHOD.invoke(socket, new Object[] { localaddr});
SOCKETCONNECT_METHOD.invoke(socket, new Object[] { remoteaddr, new Integer(timeout)});
return socket;
}
catch (InvocationTargetException e) {
Throwable cause = e.getTargetException();
if (SOCKETTIMEOUTEXCEPTION_CLASS == null) {
try {
SOCKETTIMEOUTEXCEPTION_CLASS = Class.forName("java.net.SocketTimeoutException");
} catch (ClassNotFoundException ex) {
// At this point this should never happen. Really.
REFLECTION_FAILED = true;
return null;
}
}
if (SOCKETTIMEOUTEXCEPTION_CLASS.isInstance(cause)) {
throw new ConnectTimeoutException(
"The host did not accept the connection within timeout of "
+ timeout + " ms", cause);
}
if (cause instanceof IOException) {
throw (IOException)cause;
}
return null;
}
catch (Exception e) {
REFLECTION_FAILED = true;
return null;
}
}
重点是这两句:
SOCKETBIND_METHOD.invoke(socket, new Object[] { localaddr});
SOCKETCONNECT_METHOD.invoke(socket, new Object[] { remoteaddr, new Integer(timeout)});
HttpClient在connect之前调用了bind,系统调用bind返回了EADDRINUSE错误:
EADDRINUSE
The given address is already in use.
然后是java.net.PlainSocketImpl.socketBind(Native Method)抛出了BindException。
这样的话,的确,是临时端口号耗尽,导致抛出了BindException,因为HttpClient在connect之前,先调用了bind。
只是,为什么要先bind呢?
Bind before Connect
connect之前先bind,是允许的,但并没有什么好处,反而带来极大的危害。
好吧,其实在特定情况下也可能有一点好处,这里先说危害,后面再说好处。
前面说了,临时端口号是有限的资源,数量是有限制的。并且TCP连接是个四元组:
{source_ip, source_port, destination_ip, destination_port}
如果我们直接调用connect,由操作系统来分配临时端口号:
connect(socket, destination_addr, sizeof destination_addr);
那么操作系统就为不同的destination_ip和destination_port,分别维护临时端口号分配。
假设临时端口号数量为N,那么每一个destination_ip和destination_port的组合,都能创建N个连接。
而如果connect之前先调用bind:
bind(socket, source_addr, sizeof source_addr);
connect(socket, destination_addr, sizeof destination_addr);
那已经bind过还没释放的source_port就不会再允许bind。临时端口号就变成了不同destination之间共用的资源。
假设临时端口号数量为N,那么所有destination_ip和destination_port的组合加起来,一共只能创建N个连接。
反应到HttpClient和java应用上,举例来讲:
如果你的java应用,既要使用HttpClient访问百度,又要使用HttpClient访问Google,还要使用HttpClient访问Bing。你的操作系统临时端口号数量限制为10000。
那么直接connect,百度、Google、Bing都能同时存在10000个连接,且互相之间无影响。
先bind后connect,百度、Google、Bing加起来一共只能创建10000个连接,且互相之间有影响,需要连接百度的流量大了,连接多了超过限制了,需要连接Google和Bing的也会失败。
HttpClient 4.4
看到这里,原因已经清楚了。接下来去找了比较新的HttpCliet版本来看是否有改进。如下是HttpClient 4.4的创建连接相关代码:
package org.apache.http.impl.pool;
public class BasicConnFactory implements ConnFactory<HttpHost, HttpClientConnection>:
@Override
public HttpClientConnection create(final HttpHost host) throws IOException {
final String scheme = host.getSchemeName();
Socket socket = null;
if ("http".equalsIgnoreCase(scheme)) {
socket = this.plainfactory != null ? this.plainfactory.createSocket() :
new Socket();
} if ("https".equalsIgnoreCase(scheme)) {
socket = (this.sslfactory != null ? this.sslfactory :
SSLSocketFactory.getDefault()).createSocket();
}
if (socket == null) {
throw new IOException(scheme + " scheme is not supported");
}
final String hostname = host.getHostName();
int port = host.getPort();
if (port == -1) {
if (host.getSchemeName().equalsIgnoreCase("http")) {
port = 80;
} else if (host.getSchemeName().equalsIgnoreCase("https")) {
port = 443;
}
}
socket.setSoTimeout(this.sconfig.getSoTimeout());
if (this.sconfig.getSndBufSize() > 0) {
socket.setSendBufferSize(this.sconfig.getSndBufSize());
}
if (this.sconfig.getRcvBufSize() > 0) {
socket.setReceiveBufferSize(this.sconfig.getRcvBufSize());
}
socket.setTcpNoDelay(this.sconfig.isTcpNoDelay());
final int linger = this.sconfig.getSoLinger();
if (linger >= 0) {
socket.setSoLinger(true, linger);
}
socket.setKeepAlive(this.sconfig.isSoKeepAlive());
socket.connect(new InetSocketAddress(hostname, port), this.connectTimeout);
return this.connFactory.createConnection(socket);
}
果然,改掉了,没有在connect之前先bind了。直接调用的connect:
socket.connect(new InetSocketAddress(hostname, port), this.connectTimeout);
有条件还是要积极升级各种库的版本啊。
连接池、熔断降级
像这次这个老应用这种,对三方依赖占用的资源没有限制,也没有熔断降级。确实还是太粗放了。
首先连接池必须有,连接复用提升效率,并且可以限制连接数,对客户端对服务端都好。HttpClient本身就支持连接池。
另外对三方依赖要有熔断降级,当一个依赖方出现问题或者相关流量大的时候,该降级降级,该熔断熔断,尽量的将影响控制到最小范围。熔断降级可以用hystrix。
Linux Ephemeral Port Range
就着这次问题排查,总结下临时端口号相关知识。因为每个操作系统不同,这里主要介绍linux。
临时端口号范围:
# sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768 61000
假设我们的业务逻辑处理非常快网络也好,一个连接从建立到关闭在1ms内,那么一个临时端口号被分配到下次可以使用,只需要等待TCP连接的TIME_WAIT状态结束即可。
TIME_WAIT状态的持续时间定义内核代码$KERNEL/include/net/tcp.h中:
#define TCP_TIMEWAIT_LEN (60*HZ)
以上皆为多数linux内核的默认值。
可以看到,默认临时端口号共有61000-32768=28232个。一个端口号被使用后,最少需要60秒才能释放。
也就是说,如果固定了source_ip、destination_ip、destination_port,每分钟最多只能创建28232个连接,平均每秒(61000-32768)/60=470.5个。
几百个,一个非常小的数值。对于流量大的业务,很容易出问题。更何况上面HttpClient先bind再connect。
如果想要改变这种情况,提高能够同时创建的连接数量。有以下几种办法:
- 调大net.ipv4.ip_local_port_range
这个范围可以调大,但最大不能超过65536,最小不能超过1234
比如可以调成这样:
sysctl net.ipv4.ip_local_port_range="1235 65000"
这个操作没什么风险,可以适当调大。
- 允许端口快速复用
也就是允许还处在TIME_WAIT状态的TCP连接占用的本地端口,被其它TCP连接使用。系统默认是不允许的。
可以在系统层面配置net.ipv4.tcp_tw_reuse:
sysctl net.ipv4.tcp_tw_reuse=1
也可以为特定的socket设置SO_REUSEADDR选项。
不过TIME_WAIT状态本身是有意义的,用来保证TCP连接的可靠性。允许复用TIME_WAIT状态的连接占用的端口号,虽然资源利用率提供,但也可能带来难以排查和解决的隐藏问题,需要慎重开启相关配置。
诚如man ip(7)所述:
A TCP local socket address that has been bound is unavailable for some time after closing, unless the SO_REUSEADDR flag has been set. Care should be taken when using this flag as it makes TCP less reliable.
- 使用多个source_ip
这个方案比较tricky,如前所述,固定了source_ip、destination_ip、destination_port,临时端口号数量固定。
如果有多个source_ip,那么可用的临时端口号数量可以成倍增长。
怎么用呢,需要利用系统调用bind的一个特性。如果在bind的时候,指定source_ip,但source_port设置为0,并且为socket设置IP_BIND_ADDRESS_NO_PORT选项。
tcp sockets before binding to a specific source ip with port 0 if you're going to use the socket for connect() rather then listen() this allows the kernel to delay allocating the source port until connect() time at which point it is much cheaper
这样在bind的时候,系统不会分配端口号,而是等到connect时再分配,但又指定了source_ip。
想要用这个方案,就必须先bind再connect了。这就是前文所述,bind before connect有可能的好处。
这个方案不实用,大部分情况下,服务器只有一个可用ip,这个方案都是用不了的。即便能用,用起来也比较麻烦。
Reference
https://idea.popcount.org/2014-04-03-bind-before-connect/
https://www.nginx.com/blog/overcoming-ephemeral-port-exhaustion-nginx-plus/
https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux
https://github.com/torvalds/linux/blob/4ba9920e5e9c0e16b5ed24292d45322907bb9035/net/ipv4/inet_connection_sock.c#L118
一次Commons-HttpClient的BindException排查的更多相关文章
- org.apache.commons.httpclient
org.apache.commons.httpclient /** * post 方法 * @param url * @param params * @return */ public static ...
- java apache commons HttpClient发送get和post请求的学习整理(转)
文章转自:http://blog.csdn.net/ambitiontan/archive/2006/01/06/572171.aspx HttpClient 是我最近想研究的东西,以前想过的一些应用 ...
- org.apache.commons.httpclient工具类
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; import org.apache.commons.httpcl ...
- httpClient使用中报错org.apache.commons.httpclient.HttpMethodBase - Going to buffer response body of large or unknown size.
在使用HttpClient发送请求,使用httpMethod.getResponseBodyAsString();时当返回值过大时会报错: org.apache.commons.httpclient. ...
- org.apache.commons.httpclient.HttpClient的使用(转)
HTTP 协议可能是现在 Internet 上使用得最多.最重要的协议了,越来越多的 Java 应用程序需要直接通过 HTTP 协议来访问网络资源.虽然在 JDK 的 java net包中已经提供了访 ...
- org.apache.commons.httpclient和org.apache.http.client区别(转)
官网说明: http://hc.apache.org/httpclient-3.x/ Commons HttpClient项目现已结束,不再开发.它已被其HttpClient和HttpCore模块中的 ...
- org.apache.commons.httpclient工具类(封装的HttpUtil)
import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java ...
- 通过 Apache Commons HttpClient 发送 HTTPS 请求
1.通过 HTTPS 发送 POST 请求: 2.HTTPS 安全协议采用 TLSv1.2: 3. 使用代理(Proxy)进行 HTTPS 访问: 4.指定 Content-Type 为:applic ...
- httpClient使用总结
前记 最近有个需求,需要根据商品id获取商品详情: 首先想到的是在浏览器里输入url按回车就可以了:或者在linux中使用curl+url来发起一个http请求; 但如果是要在java程序中发出htt ...
随机推荐
- 第11讲-Java泛型和文件操作
1.知识点 1.1.课程回顾 1.2.本章重点 1.2.1.泛型 1.2.2.文件操作 2.具体内容 2.1.Java泛型 2.1.1.为什么需要泛型 我们发现在List中,底层是Object[ ]数 ...
- 【Offer】[55-1] 【二叉树的深度】
题目描述 思路分析 测试用例 Java代码 代码链接 题目描述 输入一棵二叉树的根节点,求该树的深度.从根节点到叶节点依次经过的节点(含根.叶节点)形成树的一条路径,最长路径的长度为树的深度. 牛客网 ...
- 你不得不知道的HashMap面试连环炮
为什么用HashMap? 简述一下Map类继承关系? 解决哈希冲突的方法? 为什么HashMap线程不安全? resize机制? HashMap的工作原理是什么? 有什么方法可以减少碰撞? HashM ...
- Day005_Linux基础之文件权限
test.sh 举例: [oldboy@luffy001 ~]$ ls -l test.sh -rw-r--r-- 1 oldboy ops 0 Nov 14 10:42 test.sh 该文件权 ...
- 关于重写对象equals方法的问题
1.==和equals的区别 a.基本数据类型使用 == 进行值的比较 b.引用类型使用 == 进行比较时,直接比较的是对象的存储地址,即两个引用是否指向了同一个对象. c.equals方法是基类O ...
- Java ArrayList源码分析(有助于理解数据结构)
arraylist源码分析 1.数组介绍 数组是数据结构中很基本的结构,很多编程语言都内置数组,类似于数据结构中的线性表 在java中当创建数组时会在内存中划分出一块连续的内存,然后当有数据进入的时候 ...
- Python网络爬虫实战(二)数据解析
上一篇说完了如何爬取一个网页,以及爬取中可能遇到的几个问题.那么接下来我们就需要对已经爬取下来的网页进行解析,从中提取出我们想要的数据. 根据爬取下来的数据,我们需要写不同的解析方式,最常见的一般都是 ...
- Null is your firend, not a mistake
原文作者: Roman Elizarov 原文地址: Null is your firend, not a mistake 译者:秉心说 Kotlin Island from Wikimedia by ...
- 一次容器化springboot程序OOM问题探险
背景 运维人员反馈一个容器化的java程序每跑一段时间就会出现OOM问题,重启后,间隔大概两天后复现. 问题调查 一查日志 由于是容器化部署的程序,登上主机后使用docker logs Contain ...
- (五)Linux内存管理zone_sizes_init
背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...