Apache HttpClients连接超时为何是配置值的2倍?
问题描述
我将Apache HttpClients的ConnectTimeout设置为1000毫秒,禁用了自动重试与重定向,请求一个不存在的URL(http://dns.google/a.mp4),预期1秒后超时,但实际耗时约2秒才抛出NoRouteToHostException。无论修改ConnectTimeout的值,实际超时时间始终是配置值的2倍,使用版本为4.5.13,这是什么原因?
示例代码
public static void main(String[] args) throws Exception { RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(1_000) .setSocketTimeout(5_000) .setConnectionRequestTimeout(60_000) .build(); CloseableHttpClient httpClient = HttpClients.custom() .disableAutomaticRetries() .disableRedirectHandling() .setDefaultRequestConfig(requestConfig) .build(); HttpHead httpHead = new HttpHead("http://dns.google/a.mp4"); httpHead.setHeader("Accept-Encoding", "identity"); final long startTime = System.currentTimeMillis(); try (CloseableHttpResponse httpResponse = httpClient.execute(httpHead)) { final long elapsedTime = System.currentTimeMillis() - startTime; System.out.println("Success after " + elapsedTime + " ms"); } catch (Exception e) { final long elapsedTime = System.currentTimeMillis() - startTime; System.err.println("Error after " + elapsedTime + " ms"); e.printStackTrace(); } }
报错信息
Error after 2064 ms java.net.NoRouteToHostException: No route to host at java.base/sun.nio.ch.Net.connect0(Native Method) at java.base/sun.nio.ch.Net.connect(Net.java:579) at java.base/sun.nio.ch.Net.connect(Net.java:568) at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:593) at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327) at java.base/java.net.Socket.connect(Socket.java:633) at org.apache.http.conn.socket.PlainConnectionSocketFactory.connectSocket(PlainConnectionSocketFactory.java:75) at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376) at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) 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)
原因分析
核心问题在于DNS解析返回了多个IP地址(比如dns.google通常会同时返回IPv4和IPv6地址),而Apache HttpClient的setConnectTimeout是针对单个IP地址的连接超时时间,并非整体的连接阶段超时。
HttpClient默认会按照DNS返回的IP顺序,依次尝试连接每个地址,每个IP都等待你设置的超时时间。当第一个IP连接失败后,会立刻尝试第二个IP,再等待一次配置的超时时间,两次超时叠加后就出现了“2倍配置值”的总耗时,最后才抛出NoRouteToHostException。
需要注意的是,你禁用的自动重试是针对请求失败后的重试逻辑,和连接阶段的多IP尝试无关。
解决办法
1. 强制使用单一协议(例如IPv4)
自定义连接套接字工厂,限制HttpClient只使用IPv4地址,这样只会尝试一个IP,超时时间就是你配置的数值:
// 创建仅支持IPv4的连接套接字工厂 PlainConnectionSocketFactory ipv4SocketFactory = new PlainConnectionSocketFactory() { @Override public Socket createSocket(final HttpContext context) throws IOException { return new Socket(AddressFamily.INET); } }; Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", ipv4SocketFactory) .build(); PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); CloseableHttpClient httpClient = HttpClients.custom() .disableAutomaticRetries() .disableRedirectHandling() .setConnectionManager(connManager) .setDefaultRequestConfig(requestConfig) .build();
2. 自定义DNS解析策略
实现自己的DnsResolver,只返回一个IP地址,让HttpClient仅尝试一次连接:
DnsResolver singleIpResolver = host -> { InetAddress[] addresses = InetAddress.getAllByName(host); // 仅返回第一个解析到的IP return new InetAddress[]{addresses[0]}; }; CloseableHttpClient httpClient = HttpClients.custom() .disableAutomaticRetries() .disableRedirectHandling() .setDefaultRequestConfig(requestConfig) .setDnsResolver(singleIpResolver) .build();
3. 调整JVM的IPv6优先级
在JVM启动参数中添加-Djava.net.preferIPv4Stack=true,让JVM优先使用IPv4协议,这样DNS解析会优先返回IPv4地址,若网络环境不支持IPv6则只会尝试一个IP。
内容的提问来源于stack exchange,提问作者Dasmowenator

