Deliver SSE protocol hardening for SAE/HTTP2 paths, add graceful shutdown health behavior, and improve SSA retry UX for transient stream failures. For AIA, persist attachment extraction results in database with cache read-through fallback, plus production cache safety guard to prevent memory-cache drift in multi-instance deployments; also restore ASL SR page scrolling behavior. Made-with: Cursor
128 lines
6.2 KiB
Markdown
128 lines
6.2 KiB
Markdown
# **SAE 生产环境 SSE 协议故障诊断与终极防御指南**
|
||
|
||
**故障现象:** SAE 部署后,前端偶发 net::ERR\_HTTP2\_PROTOCOL\_ERROR,后端日志显示请求已接收甚至已完成。第二次请求恢复正常。
|
||
|
||
**故障定性:** 云原生环境下的经典长连接断裂与 HTTP/2 协议翻译冲突。
|
||
|
||
**核心认知:** 在 Serverless 容器中,不要试图“防止连接断开”(做不到),必须通过“前端智能重连”和“后端优雅停机”来容错。
|
||
|
||
## **一、 为什么你们的修复“治标不治本”?**
|
||
|
||
你们已经做了非常出色的网络层修复:
|
||
|
||
1. **去除了 Connection: keep-alive**:防止 HTTP/2 严格模式下因为禁用的连接专有头部(Connection-Specific Headers)导致强制 RST\_STREAM。
|
||
2. **条件化了 Connection: Upgrade**:防止 Nginx 把普通的 SSE 长轮询当成 WebSocket 升级,导致协议错乱。
|
||
|
||
**为什么还是会偶发失败?**
|
||
|
||
因为当 SAE 进行滚动更新(Rolling Update)时,旧的 Node.js Pod 会收到 SIGTERM 信号准备退出。此时,阿里云 CLB(负载均衡)的连接池中可能还有几十条保持活跃的 HTTP/2 物理连接。
|
||
|
||
如果浏览器复用了这条即将被回收的 HTTP/2 通道来发起新的 SSE 请求,或者旧 Pod 直接被底层硬杀(Kill \-9),Nginx 往后端转发时会遭遇 Connection Refused 或 Broken Pipe。Nginx 无法优雅地把这个错误包装成 HTTP 状态码,只能粗暴地向客户端发送一个 HTTP/2 GOAWAY 帧,浏览器收到后就会报出 ERR\_HTTP2\_PROTOCOL\_ERROR。
|
||
|
||
## **二、 终极防御三板斧(Cloud-Native Resilience)**
|
||
|
||
要 100% 消除用户的报错感知,必须在前端、Nginx 和后端落地以下三个机制:
|
||
|
||
### **🪓 第一斧:前端 SSE 智能断线重连 (Intelligent Retry)**
|
||
|
||
这是解决问题的绝对核心!对于大模型对话(Chat)和分析执行(Execute),前端绝不能因为一次底层网络闪断就把红色错误拍在用户脸上。
|
||
|
||
**改造方案:** 在 useSSAChat.ts 或底层 SSE 请求库中(推荐使用微软的 @microsoft/fetch-event-source 库,它自带强大的重试机制),拦截网络层错误并静默重试。
|
||
|
||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||
|
||
async function startSseStream(url: string, payload: any) {
|
||
let retryCount \= 0;
|
||
const MAX\_RETRIES \= 3;
|
||
|
||
await fetchEventSource(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
|
||
// 关键:拦截底层 HTTP/2 协议错误并决定是否重试
|
||
async onopen(response) {
|
||
if (response.ok) retryCount \= 0; // 连接成功,重置计数器
|
||
},
|
||
|
||
onerror(err) {
|
||
retryCount++;
|
||
if (retryCount \> MAX\_RETRIES) {
|
||
throw err; // 超过重试次数,才真正向用户报错
|
||
}
|
||
// 记录日志,但不抛出异常
|
||
console.warn(\`\[SSE\] 网络闪断,正在进行第 ${retryCount} 次重连...\`, err);
|
||
// 返回一个延迟时间 (指数退避) 告诉底层库多久后重连
|
||
return Math.min(1000 \* (2 \*\* retryCount), 5000);
|
||
},
|
||
|
||
onmessage(msg) {
|
||
// 处理正常的 SSE 事件
|
||
}
|
||
});
|
||
}
|
||
|
||
*效果:即便 SAE 正在滚动部署,CLB 断开了连接,浏览器会在 1 秒内静默发起第二次请求,此时 CLB 已经指向了新 Pod,用户毫无感知,只会觉得“这次思考慢了一秒”。*
|
||
|
||
### **🪓 第二斧:Nginx 层彻底关闭缓存 (Disable Buffering)**
|
||
|
||
如果你们的 Ingress 或前置 Nginx 开启了代理缓冲(Proxy Buffering),它是 SSE 流式输出的绝对天敌,也是加剧协议错误的元凶。
|
||
|
||
**改造方案:** 必须为 /api/v1/ssa/\*/chat 等 SSE 接口单独关闭 Nginx 缓冲。
|
||
|
||
location \~ ^/api/v1/ssa/.\*(?:chat|stream)$ {
|
||
proxy\_pass http://backend\_upstream;
|
||
|
||
\# 以下三行是 SSE 救命神药
|
||
proxy\_http\_version 1.1;
|
||
proxy\_buffering off; \# 严禁 Nginx 缓存 Chunk 数据
|
||
proxy\_cache off; \# 严禁缓存
|
||
chunked\_transfer\_encoding on;
|
||
|
||
\# 彻底清理可能导致 H2 冲突的 Headers
|
||
proxy\_set\_header Connection '';
|
||
|
||
\# 延长超时时间(应对大模型深度思考)
|
||
proxy\_read\_timeout 120s;
|
||
}
|
||
|
||
### **🪓 第三斧:Node.js 后端优雅停机 (Graceful Shutdown)**
|
||
|
||
为什么部署期间连接断裂那么惨烈?因为 Node.js 默认在收到 SAE 的部署停止信号(SIGTERM)时会立刻自杀。
|
||
|
||
**改造方案:** 在 main.ts 或 app.module.ts 中实现 Graceful Shutdown。当收到退出信号时,拒绝新的请求,但**给已经建立的 SSE 长连接留出 30 秒的执行时间**。
|
||
|
||
// Node.js Express/NestJS 优雅停机示例
|
||
let isShuttingDown \= false;
|
||
|
||
// 1\. 探针接口:一旦开始停机,立刻告诉 SAE/CLB "我不健康了",不要再给我派发新请求
|
||
app.get('/health', (req, res) \=\> {
|
||
if (isShuttingDown) {
|
||
return res.status(503).send('Shutting down');
|
||
}
|
||
res.status(200).send('OK');
|
||
});
|
||
|
||
// 2\. 捕获系统终止信号
|
||
process.on('SIGTERM', () \=\> {
|
||
console.log('\[System\] 收到 SIGTERM 信号,准备优雅停机...');
|
||
isShuttingDown \= true;
|
||
|
||
// 停止接收新连接
|
||
server.close(() \=\> {
|
||
console.log('\[System\] 所有现有连接已处理完毕,安全退出。');
|
||
process.exit(0);
|
||
});
|
||
|
||
// 强制超时机制:如果过了 30 秒还有 SSE 流没跑完(比如陷入死循环),强制退出
|
||
setTimeout(() \=\> {
|
||
console.error('\[System\] 优雅停机超时 (30s),强制退出。');
|
||
process.exit(1);
|
||
}, 30000);
|
||
});
|
||
|
||
## **三、 架构师总结**
|
||
|
||
你们在本地开发环境(HTTP/1.1 直连,无负载均衡,无 Pod 销毁)永远无法复现这个问题。
|
||
|
||
**这不单纯是网络配置问题,这是 Serverless 架构下的“状态保持”冲突。** 请优先让前端团队加上 **@microsoft/fetch-event-source 的静默重连机制**,这不仅能解决 SAE 部署期间的报错,还能解决未来医院网络环境不稳定、用户切网(WIFI 换 5G)导致的所有诡异长连接断开问题。这是投入产出比(ROI)最高的一记绝杀。 |