线上有个老应用,在流量增长的时候,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排查的更多相关文章

  1. org.apache.commons.httpclient

    org.apache.commons.httpclient /** * post 方法 * @param url * @param params * @return */ public static ...

  2. java apache commons HttpClient发送get和post请求的学习整理(转)

    文章转自:http://blog.csdn.net/ambitiontan/archive/2006/01/06/572171.aspx HttpClient 是我最近想研究的东西,以前想过的一些应用 ...

  3. org.apache.commons.httpclient工具类

    import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; import org.apache.commons.httpcl ...

  4. httpClient使用中报错org.apache.commons.httpclient.HttpMethodBase - Going to buffer response body of large or unknown size.

    在使用HttpClient发送请求,使用httpMethod.getResponseBodyAsString();时当返回值过大时会报错: org.apache.commons.httpclient. ...

  5. org.apache.commons.httpclient.HttpClient的使用(转)

    HTTP 协议可能是现在 Internet 上使用得最多.最重要的协议了,越来越多的 Java 应用程序需要直接通过 HTTP 协议来访问网络资源.虽然在 JDK 的 java net包中已经提供了访 ...

  6. org.apache.commons.httpclient和org.apache.http.client区别(转)

    官网说明: http://hc.apache.org/httpclient-3.x/ Commons HttpClient项目现已结束,不再开发.它已被其HttpClient和HttpCore模块中的 ...

  7. org.apache.commons.httpclient工具类(封装的HttpUtil)

    import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java ...

  8. 通过 Apache Commons HttpClient 发送 HTTPS 请求

    1.通过 HTTPS 发送 POST 请求: 2.HTTPS 安全协议采用 TLSv1.2: 3. 使用代理(Proxy)进行 HTTPS 访问: 4.指定 Content-Type 为:applic ...

  9. httpClient使用总结

    前记 最近有个需求,需要根据商品id获取商品详情: 首先想到的是在浏览器里输入url按回车就可以了:或者在linux中使用curl+url来发起一个http请求; 但如果是要在java程序中发出htt ...

随机推荐

  1. Mysql InnoDB引擎下 事务的隔离级别

    mysql InnoDB 引擎下事物学习 建表user CREATE TABLE `user` ( `uid` bigint(20) unsigned NOT NULL AUTO_INCREMENT, ...

  2. Springboot2.x 自动创建表并且执行初始化数据

    1.使用springboot jdbc初始化数据库 项目结构 schema.sql drop table if exists user; create table user (id bigint(20 ...

  3. 一文看尽Java-多线程概念

    一.前言 主要讲解一下多线程中的一些概念,本文之后就开始针对JUC包的设计开始解读: 二.概念     线程安全     1.存在共享数据(临界资源):2.多个线程同时操作共享数据:只有同时出现这两种 ...

  4. IDEA中工程上传到SVN

    1.先在IDEA上集成SVN 2.查看SVN仓库:连接SVN ①此时应该先去SVN服务器中新建一个SVN服务: 点击下一步 点击下一步 点击create ②查看SVN仓库 先复制SVN的地址 把复制的 ...

  5. GO 语言学习笔记--数组切片篇

    1.对于make 数组切片,长度和容量需要理解清楚: 容量表示底层数组的大小,长度是你可以使用的大小: 容量的用处在哪?在与当你用 appen d扩展长度时,如果新的长度小于容量,不会更换底层数组,否 ...

  6. JVM类加载器以及双亲委派模型

    从java开发人员的角度来看,类加载器可以分为3种: 1.启动类加载器(Bootstrap ClassLoader),负责将存放在<JAVA_HOME>\lib目录中,或者被-Xbootc ...

  7. d010:盈数、亏数和完全数

    题目: 对一个正整数N而言,将它除了本身以外所有的因子加起来的总和为S,如果S>N,则N为盈数,如果S<N,则N为亏数,而如果S=N,则N为完全数(Perfect Number).例如10 ...

  8. shell中sh, exec, source, fork, ./的区别

    shell中sh, exec, source, fork, ./的区别 1,sh sh test.sh ​ sh是通过创建子进程(subshell)去执行脚本,父进程无法使用子进程中的变量,而子进程对 ...

  9. Ubuntu18.04安装测试TensorFlow-GPU

    1 安装Ubuntu18.04.03 lts spt@spt-ts:~$ lsb_release -a No LSB modules are available. Distributor ID: Ub ...

  10. sql server 2014 的安装

    1.双击打开sql_server2014的安装包 2.点击弹出来的对话框的确定按钮 3.等待一会,安装包在准备中 4.弹出SQL server 安装中心,点击全新 SQL Server 独立安装 5. ...