You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vert.x Circuit Breaker使用时内存占用高、线程阻塞问题

问题背景

在使用Vert.x Circuit Breaker组件时遇到内存占用过高问题。测试场景为调用httpbin.org接口,所有请求均返回成功响应。单请求调用时运行正常,但在压测过程中JVM老年代内存使用率持续飙升。

复现示例

核心Verticle实现代码如下:

public class CleanServer extends AbstractVerticle {
Logger logger = Logger.getLogger(CleanServer.class.getName());

  @Override
  public void start(Promise<Void> startPromise) throws Exception {
      Router router = Router.router(vertx);
      CircuitBreakerCache cbc = new CircuitBreakerCache(vertx);
      router.route(HttpMethod.GET, "/get").handler(context -> {
          List<String> domains = context.queryParam("user");
          String domain = domains.get(0);
          CircuitBreaker cb = cbc.getCircuitBreaker(domain + context.request().path());
          HttpServerResponse serverResponse =
                  context.response().setChunked(true);
          cb.executeWithFallback(promise -> {
              WebClientOptions options = new WebClientOptions().setTryUseCompression(true).setTcpNoDelay(true).setTcpCork(true).setReceiveBufferSize(128).setConnectTimeout(400);
              WebClient client = WebClient.create(vertx, options);
              client.get(80, "httpbin.org", "/status/200")
                      .timeout(2000)
                      .send(ar -> {
                          if (ar.succeeded()) {
                              HttpResponse<Buffer> response = ar.result();
                              int statusCode = response.statusCode();
                              if (statusCode != 200) {
                                  promise.fail(response.statusMessage());
                              } else {
                                  serverResponse.end("Hello!!");
                                  promise.complete();
                              }
                          } else {
                              promise.fail(ar.cause().getMessage());
                          }
                      });
          }, v -> {
              // Executed when the circuit is opened
              logger.log(Level.INFO, domain + " Failed " + cb.state().toString() + " Error: Circuit open");
              serverResponse.setStatusCode(200).setStatusMessage("Circuit Open").end("Circuit Open");
              return context;
          });
      });

      // Create the HTTP server
      vertx.createHttpServer(new HttpServerOptions().setMaxInitialLineLength(10000))
              // Handle every request using the router
              .requestHandler(router)
              // Start listening
              .listen(8080)
              // Print the port
              .onSuccess(server ->
                      System.out.println(
                              "HTTP server started on port " + server.actualPort()
                      )
              );


  }
}

熔断器配置参数:

CircuitBreakerOptions() 
.setMaxFailures(50) 
.setTimeout(5000) 
.setFallbackOnFailure(true) 
.setResetTimeout(10000)));

复现步骤

  • 测试调用接口:http://localhost:8080/get?user=abc
  • 以50QPS的并发压力持续请求上述接口30分钟后,Java堆内存被占满。

补充信息

  • 使用的Vert.x版本:4.2.6

使用的JVM参数:

-XX:+UseG1GC -Xms4g -Xmx4g -XX:InitiatingHeapOccupancyPercent=70 -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=20 -XX:ConcGCThreads=5

压测过程中的JVM内存使用情况截图如下:
JVM使用趋势图

报错信息:

WARNING: Thread Thread[vert.x-eventloop-thread-3,5,main] has been blocked for 3050 ms, time limit is 2000 ms

我推测代码中存在线程阻塞点,但代码逻辑参照官方文档编写,整体较为简单,无法定位具体的问题位置。


根因定位

这个问题和Vert.x Circuit Breaker组件本身无关,完全是代码中的资源泄漏导致的,核心问题有3个:

  1. WebClient重复创建导致的严重资源泄漏
    WebClient是Vert.x提供的重量级HTTP客户端,内部维护了独立连接池、堆外内存缓冲区、NIO事件监听句柄,官方明确要求作为单例全局复用。你在每个请求的处理逻辑中都新建WebClient实例,且使用后从未调用close()释放资源。按50QPS压测30分钟计算,会累计创建9万个未回收的WebClient实例,其关联的连接、内存、回调对象全部进入老年代且无法被GC回收,是内存持续飙升的核心原因。
    同时高QPS下并发创建WebClient需要初始化NIO通道、分配内存,会长时间占用事件循环线程,直接触发你看到的事件循环阻塞警告。
  2. HTTP响应连接泄漏
    现有代码仅在请求成功、熔断器打开两个分支调用了serverResponse.end()释放连接。如果WebClient请求超时、连接失败但熔断器尚未达到打开阈值时,响应连接会一直挂起不释放,累计的挂起连接会进一步占用内存。
  3. 熔断器实例潜在泄漏风险
    你使用的CircuitBreakerCache如果仅按domain+path作为key缓存熔断器实例,没有配置最大容量、LRU淘汰或过期清理逻辑,当请求参数变化生成大量不同key时,会无限创建CircuitBreaker实例占用内存。

修复方案

  1. 将WebClient改为Verticle启动时一次性初始化的全局单例,所有请求复用同一个实例,同时补全所有分支的响应关闭逻辑,修复后的核心代码如下:
public class CleanServer extends AbstractVerticle {
  private static final Logger logger = Logger.getLogger(CleanServer.class.getName());
  private WebClient webClient;
  private CircuitBreakerCache cbc;

  @Override
  public void start(Promise<Void> startPromise) throws Exception {
    Router router = Router.router(vertx);
    // 启动时全局初始化一次WebClient
    WebClientOptions options = new WebClientOptions()
            .setTryUseCompression(true)
            .setTcpNoDelay(true)
            .setTcpCork(true)
            .setReceiveBufferSize(128)
            .setConnectTimeout(400);
    webClient = WebClient.create(vertx, options);
    cbc = new CircuitBreakerCache(vertx);

    router.route(HttpMethod.GET, "/get").handler(context -> {
      List<String> domains = context.queryParam("user");
      String domain = domains.get(0);
      CircuitBreaker cb = cbc.getCircuitBreaker(domain + context.request().path());
      HttpServerResponse serverResponse = context.response().setChunked(true);

      cb.executeWithFallback(promise -> {
        // 复用全局WebClient发送请求
        webClient.get(80, "httpbin.org", "/status/200")
                .timeout(2000)
                .send(ar -> {
                  if (ar.succeeded()) {
                    HttpResponse<Buffer> response = ar.result();
                    if (response.statusCode() != 200) {
                      // 失败分支先关闭响应再标记失败
                      if (!serverResponse.ended()) {
                        serverResponse.setStatusCode(502).end("Upstream error");
                      }
                      promise.fail(response.statusMessage());
                    } else {
                      serverResponse.end("Hello!!");
                      promise.complete();
                    }
                  } else {
                    // 异常分支同样关闭响应
                    if (!serverResponse.ended()) {
                      serverResponse.setStatusCode(502).end("Request failed");
                    }
                    promise.fail(ar.cause().getMessage());
                  }
                });
      }, v -> {
        logger.log(Level.INFO, domain + " Failed " + cb.state() + " Error: Circuit open");
        if (!serverResponse.ended()) {
          serverResponse.setStatusCode(200).setStatusMessage("Circuit Open").end("Circuit Open");
        }
        return context;
      });
    });

    vertx.createHttpServer(new HttpServerOptions().setMaxInitialLineLength(10000))
            .requestHandler(router)
            .listen(8080)
            .onSuccess(server -> System.out.println("HTTP server started on port " + server.actualPort()));
  }
}
  1. 优化CircuitBreakerCache实现:添加最大容量限制、LRU淘汰策略,对长时间未使用的熔断器实例自动调用close()后清理,避免无限创建实例。
  2. 可以给CircuitBreaker配置开启指标采集,方便后续监控熔断状态和内存占用。

按以上方案修改后,压测时内存会稳定在正常区间,老年代不会持续飙升,事件循环阻塞警告也会消失。


内容的提问来源于stack exchange,提问作者Jyoti Patel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 14:31:15