bigywater.tech
Spring · 59

WebClient:连接池、超时与重试

WebClient 的连接池默认多大?请求排队等连接会怎样?默认有没有超时?retry 要不要加 filter?POST 能不能重试?空响应体怎么处理?这篇用一个会计数的本地服务端,在 Spring Boot 4.0.8 的 WebClient 上逐个测量。

500 / 20
WebClient 默认共享连接池的 maxConnections / 自建 ConnectionProvider 不指定时的 maxConnections
3 次
超时后重试的 POST,服务端实际执行写入的次数(客户端只发了 1 个逻辑请求)
4 次
没有 filter 的 Retry.backoff(3) 对 404 的调用次数(加了 5xx filter 只有 1 次)

连接池

实测 · 本机执行输出
  HttpResources.get()(WebClient 默认用的共享资源)的 maxConnections() = 500
  ConnectionProvider.builder("t").build().maxConnections() = 20
  默认连接池:
  700 个并发请求(服务端各睡 1500 ms,每 2 ms 发起一个):成功 700,失败 {},总用时 3852 ms
      服务端收到 700 个请求、484 个连接
  自己建连接池 maxConnections=5(等待连接的超时用默认值):
  20 个并发请求(服务端各睡 300 ms,每 0 ms 发起一个):成功 20,失败 {},总用时 1387 ms
      服务端收到 20 个请求、5 个连接
  自己建连接池 maxConnections=5,pendingAcquireTimeout=200ms:
  20 个并发请求(服务端各睡 300 ms,每 0 ms 发起一个):成功 5,失败 {WebClientRequestException ← PoolAcquireTimeoutException=15},总用时 443 ms
      服务端收到 5 个请求、5 个连接

默认共享池 500 个连接;自建不写就是 20;等连接可以设超时。

超时

实测 · 本机执行输出
  不配置任何超时:
  WebClient 读到 slow-done,用时 2149 ms
  Mono.timeout(500ms) 异常 ReactiveException ← TimeoutException,用时 512 ms
  HttpClient.responseTimeout(500ms) 异常 WebClientRequestException ← ReadTimeoutException,用时 520 ms
  spring.http.clients.read-timeout=500ms:
  WebClient 异常 WebClientRequestException ← ReadTimeoutException,用时 627 ms
  spring.http.clients.connect-timeout=300ms:
  连 10.255.255.1:81:异常 WebClientRequestException ← ConnectTimeoutException,用时 400 ms

默认没有响应超时,有三种设置方式。

重试

实测 · 本机执行输出
  Retry.backoff(3, 100ms) 对 /flaky(前 2 次返回 503)→ 'flaky-ok',用时 553 ms
  对 404 也 retry(没有 filter):异常 RetryExhaustedException ← NotFound,message=Retries exhausted: 3/3,用时 384 ms
  retry 加 filter(只重试 5xx),对 404:异常 NotFound,用时 3 ms
  onRetryExhaustedThrow 还原原异常:ServiceUnavailable
  Retry.fixedDelay(3, 200ms) → 'flaky-ok',用时 422 ms
      服务端统计各路径被调用次数:{'/flaky': 6, '/flaky?a': 3, '/s/404': 5, '/s/503': 3, '/flaky?b': 3}

加 filter 只重试该重试的错误;耗尽后的异常可以还原。

非幂等

实测 · 本机执行输出
  POST /post-slow(服务端先写入再睡 1 秒),timeout(300ms).retry(2):最终 ReactiveException ← TimeoutException,用时 961 ms
      服务端 /post-slow 被调用(并执行写入)3 次

客户端超时不代表服务端没执行,重试 POST 要幂等。

状态码

实测 · 本机执行输出
  retrieve() /s/200 → 'body-of-200'
  retrieve() /s/404 → 抛 NotFound,body='body-of-404'
  retrieve() /s/500 → 抛 InternalServerError,body='body-of-500'
  exchangeToMono /s/404 → 不抛,'404 body-of-404'
  onStatus 自定义 → IllegalStateException
  204 → bodyToMono 为空,defaultIfEmpty 得到 (空)

retrieve() 抛异常,exchangeToMono 自己处理,空响应体用 defaultIfEmpty。

分步演示

连接池、超时与重试
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 Spring Boot 4.0.8 的 SpringApplication(spring-boot-starter-webclient + starter-reactor-netty,非 Web 模式,Reactor Netty 1.3.7);被调用的服务是本机的 Python http.server(HTTP/1.1,keep-alive),统计连接数与每个路径的调用次数;700 个并发请求按每 2 ms 一个发起,避免一次性冲垮测试服务端(第一次试跑 600 个同时发起时有 58 个连接失败,是测试服务端的问题);默认池的连接数 484 小于 500 是因为请求陆续完成后复用了连接
    • 没有验证:空闲连接的回收(maxIdleTime / maxLifeTime)与服务端关闭连接时的重试;HTTP/2;SSL;exchangeToMono 不消费响应体时的连接泄漏;响应体过大时的 maxInMemorySize 限制(DataBufferLimitException);ExchangeFilterFunction 的使用;Reactor Netty 的 metrics
    京ICP备2026067502号-1京公网安备11010502063487号