You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Spring Cloud Gateway MVC 使用SseEmitter时存在SSE连接泄漏问题

Spring Cloud Gateway MVC 使用SseEmitter时存在SSE连接泄漏问题

我最近在使用Spring Cloud Gateway MVC路由到内部API时遇到了一个头疼的问题:当后端API用SseEmitter实现Server-Sent-Events(SSE)长连接时,网关无法正确感知客户端断开的信号,导致后端的SseEmitter长连接和线程一直被占用,形成连接泄漏。而换成响应式的Spring Cloud Gateway就完全正常,这就很让人困惑了。

问题复现场景

我写了一个极简的复现示例,后端API的配置和核心代码如下:

后端API基础配置

server.port: 8781
spring.application.name: api

后端SSE接口实现

import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.UUID;
import java.time.Instant;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SseController {
    private static final Logger log = LoggerFactory.getLogger(SseController.class);

    public record EventDetails(
        @JsonProperty("eventId") UUID id,
        @JsonProperty("timestamp") Instant time,
        @JsonProperty("counter") int counter
    ) { }

    @GetMapping("/stream-sse-mvc")
    public SseEmitter streamSseMvc() {
        // 创建无超时的SseEmitter
        SseEmitter emitter = new SseEmitter(-1L);
        ExecutorService sseMvcExecutor = Executors.newSingleThreadExecutor();

        // 注册生命周期回调,用于监控连接状态
        emitter.onCompletion(() -> {
            log.info("Emitter has been closed");
            sseMvcExecutor.shutdownNow();
        });
        emitter.onTimeout(() -> {
            log.info("Emitter has timed out");
            emitter.complete();
        });
        emitter.onError(t -> {
            log.error("Emitter completed with error", t);
            emitter.completeWithError(t);
        });

        // 启动线程每秒发送一条SSE事件
        sseMvcExecutor.submit(() -> {
            try {
                for (int i = 0; true; i++) {
                    final UUID id = UUID.randomUUID();
                    SseEmitter.SseEventBuilder event = SseEmitter.event()
                            .data(new EventDetails(id, Instant.now(), i + 1))
                            .id(id.toString())
                            .name("sse event - mvc");

                    log.info("Sending event. [counter={}]", i);
                    emitter.send(event);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                log.info("Sending thread interrupted");
                Thread.currentThread().interrupt();
            } catch (Exception e) {
                log.error("Failed to send SSE event", e);
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }
}

这个接口的逻辑很简单:创建一个无超时的SseEmitter,启动单独线程每秒向客户端发送一条事件,循环不会主动终止,全靠客户端断开时触发的回调来清理资源。

现象对比

直接调用后端接口:正常工作

当我直接用HTTP客户端(比如curl、Postman)调用这个SSE接口,然后主动断开连接时,后端的SseEmitter会立刻感知到:

  • 触发onError回调,打印错误日志
  • 触发onCompletion回调,关闭线程池
  • 发送线程会因为emitter.send()抛出异常而终止

对应的日志如下:

2025-11-25T17:19:41.671+01:00 INFO 168228 --- [api] [pool-2-thread-1] c.rbellini.api.controller.SseController : Sending event. [counter=0]
2025-11-25T17:19:42.740+01:00 INFO 168228 --- [api] [pool-2-thread-1] c.rbellini.api.controller.SseController : Sending event. [counter=1]
...
2025-11-25T17:19:52.762+01:00 INFO 168228 --- [api] [pool-2-thread-1] c.rbellini.api.controller.SseController : Sending event. [counter=11]
2025-11-25T17:19:52.765+01:00 ERROR 168228 --- [api] [nio-8781-exec-1] c.rbellini.api.controller.SseController : Emitter completed with error
org.springframework.web.context.request.async.AsyncRequestNotUsableException: Servlet container error notification for disconnected client
...
2025-11-25T17:19:52.766+01:00 INFO 168228 --- [api] [pool-2-thread-1] c.rbellini.api.controller.SseController : Emitter has been closed

通过Gateway MVC调用:连接泄漏

但当我通过Spring Cloud Gateway MVC路由到这个接口,再断开客户端连接时,后端的SseEmitter完全没有任何反应:

  • 既不触发onError也不触发onCompletion回调
  • 发送线程会一直无限循环发送事件,完全不知道客户端已经断开
  • 后端的Tomcat连接和线程会一直被占用,直到手动重启服务

我的疑问与求助

我试过调整Gateway的各种超时配置(比如connect-timeoutresponse-timeout),但似乎都没起作用。现在想请教各位:

  1. 为什么Spring Cloud Gateway MVC没法把客户端断开的信号正确传递给后端的SseEmitter
  2. 有没有什么配置或者代码改造的方式,能让Gateway MVC正确处理这种SSE长连接的关闭?
  3. 是不是Gateway MVC在处理异步Servlet请求(比如SseEmitter这类)时,有什么特殊的机制我没配置对?

如果有朋友遇到过类似问题,或者对Spring Cloud Gateway MVC的异步请求处理逻辑熟悉的,麻烦给点思路,谢谢!

火山引擎 最新活动