SSE (Server-Sent Events) 流式长链接代理:Nginx 缓冲禁用与 TCP Keepalive 调优
SSE (Server-Sent Events) 流式长链接代理:Nginx 缓冲禁用与 TCP Keepalive 调优

在基于大语言模型(LLM)的应用架构中,流式响应(Streaming Output)已经成为标配体验。相比于 WebSocket 双向全双工通道的复杂维护成本,基于 HTTP 协议的单向长链接技术——Server-Sent Events (SSE) 成为了 AI 对话场景的首选。
然而,在生产环境中把 SSE 应用从本地开发机搬到 Kubernetes 容器集群或 Nginx 反向代理层后,前端开发者经常会遇到一连串诡异的故障:
- 字符顿挫与积压:前端并没有收到如丝般顺滑的“打字机”效果,而是卡顿 5~10 秒后突然“喷”出一大坨文字;
- 长连接莫名中断:每当模型思考或生成时间超过 60 秒,请求直接报错
504 Gateway Timeout或net::ERR_INCOMPLETE_CHUNKED_ENCODING; - 同域鉴权与跨域 Preflight 损耗:跨域 SSE 无法平滑携带 Cookie,且每次都要发起一次 OPTIONS 预检请求。
要彻底解决这些顽疾,必须从前端 Ingress Nginx 路由规约、BFF 网关转发到底层 TCP 连接保活进行全链路重构。
生产级架构拓扑:同域规约与 BFF 流式代理
为了杜绝 CORS 预检请求开销并保障同域 HttpOnly Cookie 自动携带,推荐采用微前端/单页应用与 API 网关同域聚合架构:
Client (Browser)
| (HTTPS / 同域: https://app.example.com)
v
Ingress-Nginx Controller (SSL Termination + 路由分发)
|
+---> / --> Static Assets (Nginx Pod / CDN)
|
+---> /api/v1/stream --> BFF Gateway (Node.js / Go)
|
| (gRPC / HTTP Stream)
v
LLM Inference Service (vLLM / Triton)
Nginx 与 Ingress 配置的核心坑点剖析
造成 SSE“顿挫喷涌”的核心元凶是 Nginx 默认启用的 反向代理缓冲区(Proxy Buffering) 与 Gzip 压缩缓冲。
Nginx 默认会把上游微服务返回的数据存入 proxy_buffer,直到凑满一定字节(如 4k/8k)或上游响应结束才一次性下发给客户端。对于 SSE 这种以微小 Chunk 持续下发的场景,缓冲机制必须被强制关闭。
1. Ingress-Nginx CRD 生产配置
在 Kubernetes 环境下,必须在 Ingress 资源的 metadata.annotations 中明确配置流式代理参数:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-gateway-ingress
namespace: production
annotations:
kubernetes.io/ingress.class: "nginx"
# 彻底关闭反向代理缓冲,确保每个 SSE event 立即推送到客户端
nginx.ingress.kubernetes.io/proxy-buffering: "off"
# 关闭对流式响应的 gzip 压缩,防止 gzip 收集 buffer
nginx.ingress.kubernetes.io/proxy-request-buffering: "off"
# 延长读写超时时间,防止 LLM 长思考(Deep Think)触发 504
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# 开启上游 HTTP/1.1 与 Keepalive 支持
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
nginx.ingress.kubernetes.io/configuration-snippet: |
# 强制清除 Transfer-Encoding 缓存干扰
chunked_transfer_encoding on;
proxy_set_header Connection "";
# 禁用 Nginx fastcgi/proxy 的 gzip 缓冲
gzip off;
spec:
rules:
- host: app.example.com
http:
paths:
- path: /api/v1/stream
pathType: Prefix
backend:
service:
name: bff-gateway-svc
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: frontend-static-svc
port:
number: 80
BFF 网关实现:严谨的 SSE 管道与保活心跳
在 BFF 网关层(以 Node.js / TypeScript 为例),必须正确设置标准 SSE 协议头,并在模型推理间歇期插入心跳注释包(Comment Ping),防止链路中途的 NAT 网关或负载均衡器因空闲超时(Idle Timeout)强行掐断 TCP 链接。
import { IncomingMessage, ServerResponse } from 'http';
export function handleSSEStream(req: IncomingMessage, res: ServerResponse, upstreamStream: AsyncIterable<string>) {
// 1. 设置标准 SSE 响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // 告诉 Nginx/OpenResty 禁用本响应的缓冲
});
res.flushHeaders?.();
// 2. 发送初始建立帧
res.write(`event: ready\ndata: {"status":"connected"}\n\n`);
// 3. 建立 15 秒保活心跳(Ping Comment),防止防火墙掐断
const heartbeatTimer = setInterval(() => {
if (!res.writableEnded) {
// SSE 规范:以冒号开头的行会被浏览器解析器忽略,作为心跳包
res.write(`: ping - ${Date.now()}\n\n`);
}
}, 15000);
// 4. 消费上游推流并写入客户端
(async () => {
try {
let eventId = 1;
for await (const chunk of upstreamStream) {
if (res.writableEnded) break;
const payload = JSON.stringify({ text: chunk });
res.write(`id: ${eventId++}\nevent: message\ndata: ${payload}\n\n`);
}
// 发送结束帧
res.write(`event: done\ndata: [DONE]\n\n`);
} catch (err: any) {
const errorMsg = JSON.stringify({ error: err.message || 'Stream processing failed' });
res.write(`event: error\ndata: ${errorMsg}\n\n`);
} finally {
clearInterval(heartbeatTimer);
res.end();
}
})();
// 5. 监听客户端异常断开,及时清理上游资源
req.on('close', () => {
clearInterval(heartbeatTimer);
});
}
前端 Fetch 流式解析与自动重连健壮性
浏览器原生 EventSource 只支持 GET 请求且无法自定义 Header(如无法传递特殊的 Authorization 或复杂 JSON 请求体)。在生产环境中,推荐使用基于 fetch + ReadableStreamDefaultReader 自研流式解析器:
export interface SSEOptions {
url: string;
body: Record<string, any>;
onMessage: (data: any) => void;
onError?: (err: Error) => void;
onFinish?: () => void;
}
export async function consumeSSEStream(options: SSEOptions) {
const controller = new AbortController();
try {
const response = await fetch(options.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
body: JSON.stringify(options.body),
signal: controller.signal,
credentials: 'same-origin', // 携带同域 Cookie
});
if (!response.ok || !response.body) {
throw new Error(`SSE Request failed with HTTP status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop() || ''; // 截留未闭合的数据帧
for (const block of lines) {
if (!block.trim() || block.startsWith(':')) continue; // 过滤空行与心跳
const dataLine = block.split('\n').find(line => line.startsWith('data: '));
if (dataLine) {
const rawData = dataLine.slice(6).trim();
if (rawData === '[DONE]') {
options.onFinish?.();
return;
}
try {
options.onMessage(JSON.parse(rawData));
} catch {
options.onMessage({ text: rawData });
}
}
}
}
options.onFinish?.();
} catch (error: any) {
if (error.name !== 'AbortError') {
options.onError?.(error);
}
}
}
TCP Keepalive 内核参数关键调优
除了应用层配置,底层 Linux TCP 栈也是长连接存活的关键。若云厂商负载均衡器(如 ALB/SLB)默认 90 秒清除静默会话,宿主机内核需做相应调优:
# 探测前空闲等待时间(秒)
sysctl -w net.ipv4.tcp_keepalive_time=60
# 探测报文发送间隔(秒)
sysctl -w net.ipv4.tcp_keepalive_intvl=10
# 探测失败重试次数
sysctl -w net.ipv4.tcp_keepalive_probes=3
长链接治理是云原生基础设施与前端体验深度咬合的典型场景。只有确保从内核 TCP 保活、Nginx 无缓冲直通、BFF 心跳注入到前端健壮解析器四位一体,才能支撑起稳定丝滑的大模型流式对话服务。
更多推荐

所有评论(0)