2.2 连接池

Reactor Netty HTTP 客户端连接池深度配置:ConnectionProvider 创建与参数调优、固定池与弹性池的选择、连接复用与空闲驱逐策略、最大连接数与等待队列上限、LIFO/FIFO 复用策略与多服务隔离。附可运行示例。

默认池与自定义池

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 集成。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南