默认池与自定义池
HttpClient.create() 并不是「每个请求新建一个 TCP 连接」。它默认使用 Reactor Netty 的全局共享池 HttpResources.get(),同一目标地址的连接会被复用。默认参数是:maxConnections = max(cores, 8) × 2、pendingAcquireMaxCount = 2 × maxConnections(没有显式设置时按连接上限的两倍推导,8 核机器上是 16 和 32)、pendingAcquireTimeout = 45s。
这套默认值适合「一个进程只访问少量下游」的场景。当你要控制某个下游的连接配额、隔离故障、单独设置监控标签时,才需要自己建池:
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
ConnectionProvider provider = ConnectionProvider.builder("my-pool")
.maxConnections(50)
.pendingAcquireMaxCount(100)
.build();
HttpClient client = HttpClient.create(provider);
ConnectionProvider.builder(String name) 创建一个命名连接池。name 会作为池的标识出现在 ConnectionProvider.name() 和相关的 metrics 标签里,便于在监控中区分多个池;它不负责给 EventLoop 或业务线程命名。
「固定」与「弹性」只是配置策略
Reactor Netty 1.3 的 ConnectionProvider 只有一种池实现,ConnectionProvider.fixed(...)、elastic(...) 这些旧工厂方法已经移除。所谓固定池 / 弹性池,是同一组参数下的两种用法:
- 按上限规划:
maxConnections按业务峰值设定,排队上限也设得较小,池子满了就让调用方快速失败或回压。 - 按需增长:把
maxConnections设得较大,排队上限放宽,让突发流量先排队而不是立刻失败。
按上限规划(推荐用于已知负载)
ConnectionProvider planned = ConnectionProvider.builder("planned")
.maxConnections(50)
.pendingAcquireTimeout(Duration.ofSeconds(10))
.pendingAcquireMaxCount(100)
.build();
| 参数 | 含义 |
|---|---|
maxConnections | 池内最大连接数(硬上限) |
pendingAcquireTimeout | 等待可用连接的超时 |
pendingAcquireMaxCount | 排队等待连接的请求上限 |
当连接数达到 maxConnections,新请求进入等待队列;排队数达到 pendingAcquireMaxCount 后,acquire 会失败,抛出 reactor.netty.internal.shaded.reactor.pool.PoolAcquirePendingLimitException。注意它在 internal.shaded 包里,直接继承 RuntimeException,不是 ReactorNettyException,也不该被业务代码按类型 import 捕获。它表示「池和队列都满了」,不是连接本身出错;通常应回压到调用方,或在 acquire 外层用 onErrorResume 按「pending limit」语义降级,而不是无脑重试。
按需增长(推荐用于突发流量)
ConnectionProvider bursty = ConnectionProvider.builder("bursty")
.maxConnections(500)
.pendingAcquireMaxCount(-1) // 无上限排队
.build();
更大的连接上限和更长的排队能吸收突发流量,但每个连接都要占用文件描述符、内存和对端的连接配额。生产环境必须给出明确上限,并监控连接数、排队数和 acquire 等待时间,不要把 -1 当成永久方案。
空闲驱逐
连接长期空闲可能被对端或中间件关闭。配置空闲检测:
ConnectionProvider provider = ConnectionProvider.builder("evicting")
.maxConnections(100)
.maxIdleTime(Duration.ofMinutes(5)) // 空闲 5 分钟驱逐
.maxLifeTime(Duration.ofMinutes(30)) // 最长存活 30 分钟
.lifo() // 后进先出:复用最新连接
.build();
| 参数 | 行为 |
|---|---|
maxIdleTime | 连接在池中空闲超过此时间后关闭 |
maxLifeTime | 连接总存活时间上限,防内存泄漏 |
lifo() / fifo() | 复用策略:最新连接(LIFO)或最旧连接(FIFO) |
lifo() 在负载稳定时更好——最新连接更可能还活着(对端未超时关闭)。fifo() 在负载波动时更公平——避免某些连接永远不被用到。
自定义健康检查
默认驱逐只基于时间。Reactor Netty 没有内建的「发送心跳探测连接是否活着」机制:HTTP 层没有通用的 ping 语义,TCP 连接在收到 RST 之前看起来都是可用的。不要用「收到 5xx 就 dispose 连接」当健康检查——5xx 是应用层错误,连接本身是健康的,随手丢弃只会破坏连接池命中率。
真正可行的做法是显式调用下游的健康端点,把结果用于路由或熔断(第 11 章),例如:
public Mono<Boolean> probe(String baseUrl) {
return HttpClient.create()
.get()
.uri(baseUrl + "/health")
.responseSingle((res, bytes) -> bytes.asString()
.map(body -> res.status().code() == 200 && body.contains("UP")))
.timeout(Duration.ofSeconds(2))
.onErrorReturn(false);
}
ConnectionProvider 侧能做的是让后台定期执行驱逐扫描:
ConnectionProvider provider = ConnectionProvider.builder("health-checked")
.maxConnections(50)
.evictInBackground(Duration.ofSeconds(30)) // 每 30 秒后台扫描一次
.build();
evictInBackground 只是按固定间隔触发一次驱逐扫描,依据仍是 maxIdleTime、maxLifeTime 和你注册的 evictionPredicate;它本身不会发心跳去探测连接是否可用。真正的活性探测要么依赖协议层(HTTP 请求失败后剔除),要么在 evictionPredicate 里做应用层判断。
按目标地址隔离
如果你同时连接多个服务(如用户服务和订单服务),同一个全局池会导致互相阻塞。为每个目标创建独立池:
ConnectionProvider userPool = ConnectionProvider.builder("user-service")
.maxConnections(20)
.build();
ConnectionProvider orderPool = ConnectionProvider.builder("order-service")
.maxConnections(30)
.build();
HttpClient userClient = HttpClient.create(userPool)
.baseUrl("https://user-api.example.com");
HttpClient orderClient = HttpClient.create(orderPool)
.baseUrl("https://order-api.example.com");
按服务隔离后,订单服务的连接风暴不会挤占用户服务的连接配额。
等待策略:失败快 vs 排队
当池满时,有两种策略:
// 策略 A:立即失败
ConnectionProvider failFast = ConnectionProvider.builder("fail-fast")
.maxConnections(10)
.pendingAcquireMaxCount(0) // 不排队,池满直接抛 PoolAcquirePendingLimitException
.build();
// 策略 B:无限排队(慎用)
ConnectionProvider queue = ConnectionProvider.builder("queue")
.maxConnections(10)
.pendingAcquireMaxCount(-1) // 无上限排队;等不到连接时抛 PoolAcquireTimeoutException
.pendingAcquireTimeout(Duration.ofSeconds(60))
.build();
推荐策略:固定排队上限(如 2×maxConnections),超时 5–10 秒。这样既不会因为瞬间高峰大量失败,也不会因为下游故障导致无限堆积。
关闭连接池
ConnectionProvider 持有线程和连接资源,应用关闭时需要释放:
ConnectionProvider provider = ConnectionProvider.builder("app")
.maxConnections(50)
.build();
HttpClient client = HttpClient.create(provider);
// 使用 ...
// 关闭
provider.dispose();
// 或异步:provider.disposeLater().block();
在 Spring 应用里把 ConnectionProvider 注册为 Bean,并用 @PreDestroy 调用 disposeLater() 即可随容器关闭自动释放;不要在每个业务类里各自建池、各自 dispose。
eventd v2 的客户端配置
eventd v2 内部服务间的 HTTP 调用使用如下连接池策略:
public class ServiceClient {
private final HttpClient client;
public ServiceClient(String baseUrl) {
ConnectionProvider pool = ConnectionProvider.builder("eventd-internal")
.maxConnections(100)
.maxIdleTime(Duration.ofMinutes(2))
.maxLifeTime(Duration.ofMinutes(10))
.pendingAcquireMaxCount(200)
.pendingAcquireTimeout(Duration.ofSeconds(5))
.lifo()
.build();
this.client = HttpClient.create(pool)
.baseUrl(baseUrl)
.headers(h -> h.set("Content-Type", "application/json"));
}
public Mono<String> call(String path) {
return client.get()
.uri(path)
.responseSingle((res, bytes) -> {
if (res.status().code() == 200) return bytes.asString();
return Mono.error(new RuntimeException("HTTP " + res.status().code()));
});
}
public void shutdown() {
// 应用关闭时调用;connectionProvider() 已经返回 ConnectionProvider
client.configuration().connectionProvider()
.disposeLater()
.block();
}
}
小结
| 概念 | 配置 |
|---|---|
| 按上限规划 | 较小的 maxConnections + 有限 pendingAcquireMaxCount |
| 按需增长 | 更大的 maxConnections,排队上限放宽 |
| 空闲驱逐 | maxIdleTime + maxLifeTime |
| 复用策略 | lifo()(负载稳定)或 fifo()(负载波动) |
| 后台驱逐 | evictInBackground(Duration)(不做活性探测) |
| 多服务隔离 | 每个 baseUrl 一个 ConnectionProvider |
| 关闭 | provider.dispose() 或 disposeLater() |
下一节 2.3 客户端重试、超时与弹性 :retryWhen、timeout、降级策略、Resilience4j 集成。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。