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

Spring-Angular SseEmitter仅本地可用,Google Cloud部署失效求助

SSE推送在Google Cloud环境无法送达的排查分析

问题背景

使用Spring SseEmitter实现企业变更通知推送,本地环境(含本地部署生产版本)功能正常,但部署到Google Cloud(GCP)平台后消息始终无法送达。Spring服务器日志偶现以下异常:

  • AsyncRequestTimeoutException
  • HttpMediaTypeNotAcceptableException

相关代码与配置

Spring SSE端点

@GetMapping(value = "/notifications", headers = "Accept=*/*", consumes = MediaType.ALL_VALUE, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter getCurrentUserNotificationsEmitter() {
    return companyNotifierService.createEmitter(SessionUtils.getCurrentCompanyUuid(), SessionUtils.getCurrentUserUuid());
}

Angular消费端代码

listen(): void {
  const eventSource = new EventSource(`/webfront${this.serviceUrl}/notifications`, { 
                         withCredentials: true });
  eventSource.addEventListener('update', () => {/* 处理变更通知 */});
  eventSource.onmessage(() => console.log('heartbeat'));
  // 异常时重启连接
  eventSource.onerror(() => { eventSource.close(); listen(); })
}

服务端消息发送代码

void notifyUser(String userId, String companyUuid){
    final SseEmitter emitter;
    // 从数据结构中找到对应用户的emitter
     emitter.send(SseEmitter.event()
                   .reconnectTime(30000)
                   .data("company-updated", MediaType.TEXT_PLAIN)
                   .id(UUID.randomUUID().toString())
                   .name("update"));
}

Express代理服务器配置(生产环境)

// 生产环境与IE11测试用Web服务器,其他场景使用ng server
const express = require('express');
const https = require('https');
const path = require('path');
const fs = require('fs');
const httpProxy = require('http-proxy');

const logger = require('morgan');
const errorHandler = require('errorhandler');

const _httpsOptions = {
  key: fs.readFileSync('../security/localhost.key'),
  cert: fs.readFileSync('../security/localhost.crt')
};

const app = express();
const proxy = new httpProxy.createProxyServer({target: 'https://localhost:8763', secure: false});

app.set('port', process.env.PORT || 8773);
app.use(logger('dev'));

app.use(express.static(path.join(__dirname, '../../dist/clientng')));

if (app.get('env') === 'development') {
  app.use(errorHandler());
}

app.all('/auto/*', function (req, res) {
  console.log(req.protocol);
  console.log(req.hostname);
  const url = req.protocol + '://' + req.hostname + ':' + app.get('port') + '/#' + req.url;
  res.redirect(url);
});

app.all('/*', function (req, res) {
  proxy.web(req, res, function (err) {
    console.log(err);
    if (err) throw err;
  });
});

https.createServer(_httpsOptions, app).listen(app.get('port'), function () {
  console.log('Express server listening on port ' + app.get('port'));
  fs.writeFile('node.pid', process.pid.toString(), function (err) {
    if (err) {
      console.log(err);
    }
  });
});

Spring MVC异步配置

mvc:
  async:
    request-timeout: 210000 # ms

异常日志片段

org.springframework.web.context.request.async.AsyncRequestTimeoutException: null
        at org.springframework.web.context.request.async.TimeoutDeferredResultProcessingInterceptor.handleTimeout(TimeoutDeferredResultProcessingInterceptor.java:42)
        ...(省略栈跟踪)

14:11:21.354 [http-nio-8080-exec-6] WARN  o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver - Failure in @ExceptionHandler public lu.legitech.lexnow.json.common.BasicErrorMessage lu.legitech.lexnow.controller.ErrorController.uncaughtException(java.lang.Exception)
org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
        at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:306)
        ...(省略栈跟踪)

可能的原因与解决方案

1. GCP负载均衡/Ingress超时不匹配

GCP HTTP(S)负载均衡默认30秒空闲超时,远小于Spring配置的210秒异步超时。当连接空闲超过30秒,负载均衡会主动断开连接,导致SSE连接中断,推送无法送达,同时触发AsyncRequestTimeoutException。

解决方案:

  • 创建Backend Config配置,将超时时间设置为与Spring异步超时一致(210秒):
    apiVersion: cloud.google.com/v1
    kind: BackendConfig
    metadata:
      name: sse-backend-config
    spec:
      timeoutSec: 210
    
  • 在Ingress资源中关联该Backend Config:
    annotations:
      cloud.google.com/backend-config: '{"default": "sse-backend-config"}'
    

2. Express代理缺失长连接配置

当前http-proxy配置未设置长连接与超时参数,在GCP环境中可能提前断开空闲连接。

解决方案:
修改代理配置,添加超时与长连接参数:

const proxy = new httpProxy.createProxyServer({
  target: 'https://localhost:8763',
  secure: false,
  proxyTimeout: 210000,  // 匹配Spring超时
  timeout: 210000,
  keepAlive: true,
  keepAliveMsecs: 30000
});

// 同时设置Express服务器超时
app.set('timeout', 210000);

3. SSE心跳机制不完善

服务端未定期发送心跳包,GCP中间网络组件会断开长时间无数据的连接。虽然Angular端监听了心跳,但无服务端主动推送的心跳,连接仍会被判定为空闲。

解决方案:
在创建SseEmitter时启动定时心跳任务(间隔小于负载均衡超时,比如25秒):

// 在companyNotifierService.createEmitter方法中添加
SseEmitter emitter = new SseEmitter(210000L);
// 使用定时任务池执行心跳
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> {
  try {
    // 发送心跳消息,触发客户端onmessage
    emitter.send(SseEmitter.event().data("heartbeat"));
  } catch (IOException e) {
    // 连接失效,移除emitter并关闭定时任务
    removeEmitter(userId, companyUuid);
    executor.shutdown();
  }
}, 0, 25000, TimeUnit.MILLISECONDS);

4. 异常处理器不兼容SSE格式

HttpMediaTypeNotAcceptableException是因为全局异常处理器在SSE连接超时后,试图返回JSON格式的BasicErrorMessage,但SSE连接的Accept头为text/event-stream,无对应消息转换器支持。

解决方案:
为SSE连接单独处理超时异常,返回符合SSE格式的消息后关闭连接:

@ExceptionHandler(AsyncRequestTimeoutException.class)
public void handleSseTimeout(SseEmitter emitter) {
  try {
    emitter.send(SseEmitter.event()
      .name("timeout")
      .data("连接超时,正在重连...")
      .reconnectTime(30000));
    emitter.complete();
  } catch (IOException e) {
    emitter.completeWithError(e);
  }
}

5. SSL证书配置不符合生产环境要求

当前Express使用本地自签名证书,在GCP生产环境中可能导致SSL握手失败或连接不稳定。

解决方案:
使用GCP托管的SSL证书(Managed SSL Certificates)替换本地自签名证书,确保客户端与Express、Express与Spring后端之间的SSL连接正常。


内容的提问来源于stack exchange,提问作者César Castro Aroche

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 07:55:20