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内存使用情况截图如下:
报错信息:
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个:
- WebClient重复创建导致的严重资源泄漏
WebClient是Vert.x提供的重量级HTTP客户端,内部维护了独立连接池、堆外内存缓冲区、NIO事件监听句柄,官方明确要求作为单例全局复用。你在每个请求的处理逻辑中都新建WebClient实例,且使用后从未调用close()释放资源。按50QPS压测30分钟计算,会累计创建9万个未回收的WebClient实例,其关联的连接、内存、回调对象全部进入老年代且无法被GC回收,是内存持续飙升的核心原因。
同时高QPS下并发创建WebClient需要初始化NIO通道、分配内存,会长时间占用事件循环线程,直接触发你看到的事件循环阻塞警告。 - HTTP响应连接泄漏
现有代码仅在请求成功、熔断器打开两个分支调用了serverResponse.end()释放连接。如果WebClient请求超时、连接失败但熔断器尚未达到打开阈值时,响应连接会一直挂起不释放,累计的挂起连接会进一步占用内存。 - 熔断器实例潜在泄漏风险
你使用的CircuitBreakerCache如果仅按domain+path作为key缓存熔断器实例,没有配置最大容量、LRU淘汰或过期清理逻辑,当请求参数变化生成大量不同key时,会无限创建CircuitBreaker实例占用内存。
修复方案
- 将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())); } }
- 优化CircuitBreakerCache实现:添加最大容量限制、LRU淘汰策略,对长时间未使用的熔断器实例自动调用
close()后清理,避免无限创建实例。 - 可以给CircuitBreaker配置开启指标采集,方便后续监控熔断状态和内存占用。
按以上方案修改后,压测时内存会稳定在正常区间,老年代不会持续飙升,事件循环阻塞警告也会消失。
内容的提问来源于stack exchange,提问作者Jyoti Patel
相关产品推荐
相关产品推荐

