高并发请求场景时,建议使用HTTP连接池

背景:

当经常遇到调用 被调用方的连接异常, 访问异常等, 也不要简单归结为被调用方的性能差。也要主动分一下自己的调用是否合适,是否可以优化。我们以前的项目曾经发生过将上传来的数百个文件上传请求并发地打到后台的服务系统 等中,结果出现不少超时,甚至连接被强杀的情况。后来优化成连接复用,问题就减少很多。

那么如何优化呢?

  •  RestTemplate 默认不开启连接池,需要替换底层实现:
  @Bean
  public RestTemplate restTemplate() {
      // 连接池配置
      PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
      cm.setMaxTotal(200);           // 连接池总大小
      cm.setDefaultMaxPerRoute(50);  // 对同一个 host 的最大连接数

      CloseableHttpClient httpClient = HttpClients.custom()
              .setConnectionManager(cm)
              .setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
              .build();

      return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));
  }
  • 依赖:
<dependency>
      <groupId>org.apache.httpcomponents.client5</groupId>
      <artifactId>httpclient5</artifactId>
  </dependency>

一句话总结

┌───────────────┬───────────────┬───────────────────────┐
│ │ 无连接复用 │ 有连接复用(池) │
├───────────────┼───────────────┼───────────────────────┤
│ 100个并发请求 │ 建立100条连接 │ 最多建立N条(如20条) │
├───────────────┼───────────────┼───────────────────────┤
│ 握手开销 │ 每次都要 │ 一次握手,多次复用 │
├───────────────┼───────────────┼───────────────────────┤
│ 对端压力 │ 瞬间峰值 │ 平稳可控 │
└───────────────┴───────────────┴───────────────────────┘

连接复用 = 用少量长连接,排队处理大量请求,而不是来一个请求开一条连接。

 

深入:连接复用与连接池

一、TCP 连接的真实代价

每次建立一条新的 HTTPS 连接,实际发生的事:

客户端 服务端 (S3)
|-------- SYN ----------->| ← TCP握手第1步
|<-------- SYN-ACK --------| ← TCP握手第2步
|-------- ACK ----------->| ← TCP握手第3步
|-------- ClientHello ---->| ← TLS握手开始
|<-------- ServerHello ----|
|<-------- Certificate ----|
|-------- KeyExchange ---->|
|<-------- Finished -------| ← TLS握手结束
|-------- HTTP Request --->| ← 真正的业务数据
|<-------- HTTP Response --|
|-------- FIN ----------->| ← 连接关闭

一次请求,真正传数据之前已经来回了 5~7 次。数百并发时,这些握手本身就能把网络和对端打满。

二、连接池内部工作原理

┌─────────────────────────────────┐
│ Connection Pool │
请求1 ──borrow──▶│ [连接A: IDLE] → 变为 LEASED │──▶ S3
请求2 ──borrow──▶│ [连接B: IDLE] → 变为 LEASED │──▶ S3
请求3 ──等待───▶│ [连接C: LEASED] │
│ maxTotal=2, 请求3 进入等待队列 │
└─────────────────────────────────┘
请求1完成 ──return──▶ 连接A 回到 IDLE,请求3 拿到连接A

连接有三种状态:
- IDLE:空闲,可被借用
- LEASED:已借出,正在使用
- EXPIRED:过期,等待回收


三、关键参数详解

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();

// ① 池的总容量上限
cm.setMaxTotal(200);

// ② 对同一个 host:port 的并发连接上限(最重要!)
// 如果只调用一个 S3 endpoint,这个值决定实际并发能力
cm.setDefaultMaxPerRoute(50);

// ③ 单独为某个 host 设置上限(比 DefaultMaxPerRoute 优先级高)
HttpHost s3Host = new HttpHost("s3.amazonaws.com", 443);
cm.setMaxPerRoute(new HttpRoute(s3Host), 100);

RequestConfig requestConfig = RequestConfig.custom()
// ④ 从连接池借连接的等待超时(池满时排队多久放弃)
.setConnectionRequestTimeout(Timeout.ofSeconds(3))

// ⑤ 建立 TCP 连接的超时
.setConnectTimeout(Timeout.ofSeconds(5))

// ⑥ 等待服务端响应的超时(读数据超时)
.setResponseTimeout(Timeout.ofSeconds(30))
.build();

 

三个超时的区别很重要:

时间线:
[等连接池] → [TCP握手] → [发请求] → [等响应] → [读数据]
↑④ ↑⑤ ↑⑥
ConnectionRequest Connect Response/Read


四、连接池的三大陷阱

陷阱1:连接泄漏

// 危险:手动管理连接时忘记关闭
CloseableHttpResponse response = client.execute(request);
String body = EntityUtils.toString(response.getEntity());
// 如果这里抛异常,response 没有 close → 连接永远不归还池

// 正确:用 try-with-resources
try (CloseableHttpResponse response = client.execute(request)) {
return EntityUtils.toString(response.getEntity());
}
// RestTemplate 内部已帮你处理,不用担心

 

陷阱2:僵尸连接(Stale Connection)

连接在池中空闲太久,对端已经关闭了,但客户端不知道,借出去发请求时才发现连接已死 → 报错。

CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
// 后台线程定期清理过期和空闲太久的连接
.evictExpiredConnections()
.evictIdleConnections(TimeValue.ofSeconds(30))
.build();

 

陷阱3:defaultMaxPerRoute 太小

maxTotal = 200,但 defaultMaxPerRoute = 2(默认值!)

结果:对 S3 只有 2 条并发连接,其余 198 个请求全部排队
→ connectionRequestTimeout 超时异常大量出现
→ 误以为是 S3 慢,其实是自己的连接池配死了

Apache HttpClient 5 的默认值 defaultMaxPerRoute = 5,生产环境必须按实际调大。

 

五、连接复用的生命周期管理

// Keep-Alive 策略:决定一条连接可以空闲多久
ConnectionKeepAliveStrategy keepAliveStrategy = (response, context) -> {
// 优先读服务端返回的 Keep-Alive: timeout=60
HeaderElementIterator it = new BasicHeaderElementIterator(
response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
if ("timeout".equalsIgnoreCase(he.getName()) && he.getValue() != null) {
return TimeValue.ofSeconds(Long.parseLong(he.getValue()));
}
}
// 服务端没说,默认保持 30 秒
return TimeValue.ofSeconds(30);
};

 

原则:客户端的 Keep-Alive 时长必须小于服务端的,否则服务端已关闭,客户端还以为连接活着 → 僵尸连接。


六、完整的生产级配置

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
// 1. 连接池
PoolingHttpClientConnectionManager cm =
new PoolingHttpClientConnectionManager();
cm.setMaxTotal(200);
cm.setDefaultMaxPerRoute(50);

// 2. 请求超时
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofSeconds(3))
.setConnectTimeout(Timeout.ofSeconds(5))
.setResponseTimeout(Timeout.ofSeconds(30))
.build();

// 3. HttpClient
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.setDefaultRequestConfig(requestConfig)
.evictExpiredConnections()
.evictIdleConnections(TimeValue.ofSeconds(30))
.build();

return new RestTemplate(
new HttpComponentsClientHttpRequestFactory(httpClient));
}
}

 


七、如何判断连接池配置是否合理

看这几个指标:

┌──────────────────────────────────────────────────┬─────────────────────────────────────────┐
│ 现象 │ 可能原因 │
├──────────────────────────────────────────────────┼─────────────────────────────────────────┤
│ ConnectionRequestTimeout 频繁出现 │ defaultMaxPerRoute 太小,请求排队太久 │
├──────────────────────────────────────────────────┼─────────────────────────────────────────┤
│ 连接正常但偶发 SocketException: Connection reset │ 僵尸连接,需要开启 evictIdleConnections │
├──────────────────────────────────────────────────┼─────────────────────────────────────────┤
│ 响应慢但连接数不高 │ responseTimeout 问题,被调方确实慢 │
├──────────────────────────────────────────────────┼─────────────────────────────────────────┤
│ 上游没异常,下游超时 │ 线程池打满或 GC pause,跟连接池无关 │
└──────────────────────────────────────────────────┴─────────────────────────────────────────┘

监控连接池状态:

// 定时打印连接池状态,方便排查
PoolStats stats = cm.getTotalStats();
log.info("连接池状态 - 可用:{} 已借出:{} 等待:{} 最大:{}",
stats.getAvailable(), stats.getLeased(),
stats.getPending(), stats.getMax());

 

总结

连接复用解决的核心问题:
握手开销 → Keep-Alive 复用连接,握手只做一次
并发连接数 → 连接池限制上限,保护对端
资源浪费 → 空闲连接回收,避免占用对端文件描述符

配置三要素:
maxTotal → 总容量天花板
defaultMaxPerRoute → 对单个 host 的并发上限(最关键)
evictIdle → 清理僵尸连接

posted @ 2026-06-15 16:25  苹果芒  阅读(12)  评论(0)    收藏  举报