Nginx 在 SSE 场景下配置方法(代理缓冲与心跳详解)

Nginx 在这个项目里干四件事:托管前端静态资源、把 /api 反向代理到 Spring Boot 后端、为 SSE 流式输出做专用的非缓冲转发、统一做 Gzip 压缩与缓存策略。其中 SSE 是特殊中的特殊——默认配置下 Nginx 会把流式响应缓冲成块再吐,用户看到的就是文章内容一卡一卡地跳,必须针对 SSE 路径单独关闭缓冲、关闭压缩、拉长超时。下面按职责拆开讲,配置都是线上在用的。

一、Nginx 的四个职责

职责 承担的内容 对应配置位置
静态托管 托管 Vue 构建出的 dist,处理 SPA 路由回退 location / + try_files
反向代理 /api 转发到后端 8080,透传真实 IP location /api/
SSE 转发 流式响应不缓冲、不压缩、不断流 location /sse/
传输优化 Gzip 压缩静态资源、静态文件强缓存 gzip on + expires

普通 API 和 SSE 在 Nginx 里的待遇完全不同,这是整套配置里最容易写错的地方。

二、静态托管与 SPA 路由回退

前端是 Vue 3 单页应用,构建产物是一堆静态文件。try_files $uri $uri/ /index.html 这行解决了刷新 404:用户直接访问 /article/42 这种前端路由时,磁盘上没有对应文件,Nginx 回退到 index.html,由 Vue Router 接管渲染。这行漏掉,线上表现为首页能开、一刷新就白屏。

server {
    listen 80;
    server_name writer.example.com;
    root /var/www/ai-writer/dist;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

带 hash 的静态资源配一年强缓存,index.html 不缓存,否则发版后用户拿到旧的入口文件,新版本永远加载不出来:

location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

三、普通 API 的反向代理

/api 转发到后端,几个 proxy_set_header 是必须的:后端靠 X-Real-IP 和 X-Forwarded-For 拿客户端真实地址做限流与日志,Host 头保持域名一致,避免重定向跳错。

location /api/ {
    proxy_pass http://127.0.0.1:8080/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

proxy_pass 结尾的斜杠决定路径怎么转发:带斜杠 http://127.0.0.1:8080/ 会把 /api 前缀剥掉,后端收到 /xxx;不带斜杠则保留 /api/xxx。这块要和后端的 @RequestMapping 对齐,我对齐后统一走”剥前缀”。

四、SSE 场景的特殊配置

AI 爆款文章创作器按「选题→大纲→初稿→润色」多阶段生成,前端通过一条 SSE 长连接接收流式内容。企业级 AI 网关的对话流也是一样的机制。Nginx 默认行为对 SSE 有三个破坏点:缓冲、压缩、超时。逐个处理:

4.1 关闭缓冲

proxy_buffering 默认开启,Nginx 会攒够 4KB 或响应结束后才往客户端吐数据。SSE 是边生成边写,一攒就变块状输出。在 SSE 的 location 里关掉缓冲,同时在响应头打上 X-Accel-Buffering: no,让后端和 Nginx 两层都明确不缓冲。

4.2 关闭压缩

gzip 对 SSE 是负优化。chunked 编码的数据被压缩后,客户端解压要等完整数据包,流式效果丢失。SSE 的 location 里 gzip off。

4.3 拉长超时与保持长连接

proxy_read_timeout 默认 60 秒,SSE 连接超过 60 秒没数据就被掐断。改成 1 小时,并强制 HTTP/1.1 + 空 Connection 头,避免长连接被误判成短连接关闭。

完整的 SSE location:

location /sse/ {
    proxy_pass http://127.0.0.1:8080/sse/;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_connect_timeout 300s;

    add_header X-Accel-Buffering no;
    add_header Cache-Control no-cache;
    gzip off;
    chunked_transfer_encoding on;
}

4.4 SSE 与普通 API 的配置对比

配置项 普通 API SSE
proxy_buffering 保持默认(开) off
proxyreadtimeout 60s 3600s
gzip 开启,压缩 JSON off
proxy_cache 默认 off
proxyhttpversion 默认 1.1 + Connection 置空

五、生产环境踩过的三个 SSE 坑

  1. CDN 挡在 Nginx 前面时:只配 Nginx 不够,CDN 的响应缓存也要关,源站加 Cache-Control: no-cache 兜底,否则流式响应被 CDN 截断;
  2. 心跳超时不对称:后端 15 秒一条心跳,但某一层(比如云负载均衡)的空闲超时只有 60 秒,把心跳间隔压到 10 秒以内才稳定;
  3. 把 SSE 配置误套到全局:曾在 http 块里全局 proxy_buffering off,普通 API 的吞吐立刻下降,因为缓冲本来是 Nginx 提升转发效率的手段。SSE 配置必须收在 location /sse/ 内。

六、企业级 AI 网关里的同款配置

「企业级 AI 网关」项目用 Spring Boot + Spring AI 接入多模型,对外暴露的 /v1/chat/completions 同样是 SSE 对话流。Nginx 侧的做法完全一致:单独 location、关闭缓冲、拉长超时。多出的工作是限流,网关层按模型维度做配额,Nginx 侧用 limit_req_zone 兜底防刷:

limit_req_zone $binary_remote_addr zone=ai_writer:10m rate=5r/s;

location /v1/chat/completions {
    limit_req zone=ai_writer burst=10 nodelay;
    proxy_pass http://127.0.0.1:8080/v1/chat/completions;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    add_header X-Accel-Buffering no;
    gzip off;
}

对话流和文章生成流是两条路径,但 Nginx 对它们的处理原则只有一个:不缓冲、不压缩、不断流。把这套 location 模板复制到任何 SSE 接口上,改路径即可复用。

七、验证配置是否生效

用 curl 带 -N 参数实测,-N 禁用 curl 自己的缓冲:

curl -N -H "Accept: text/event-stream" http://writer.example.com/sse/generate?topic=nginx

正常时内容按 token 逐字流出,没有停顿到块再跳的情况。再用 curl -I 确认响应头里带 X-Accel-Buffering: no,说明非缓冲生效。企业级 AI 网关上线时用同一套方法验证 /v1/chat/completions 的对话流,逐字吐出即为通过。

上线后持续观察两个指标:Nginx 的 upstream_response_time 分布和 SSE 连接数。用 nginx -V 确认模块齐全后,给日志加一个字段记录请求是否命中 /sse/ 路径,方便按连接时长排序定位异常长连接。SSE 长连接占的是 Nginx worker 的连接数,worker_connections 要按并发预估调大,一台上线时配到 10240,并在 keepalive 与超时参数上保持一致,避免连接被某个中间层提前回收。

常见问题(FAQ)

Q1:SSE 连上后一直不出数据怎么办?

检查 proxy_buffering off 是否真的配在了 SSE 的 location,并用 curl 看响应头是否带 X-Accel-Buffering: no。

Q2:普通接口能开缓冲,SSE 为什么要单独关?

缓冲会等数据攒够才转发,流式内容被切成大块,失去逐字渲染效果,所以只对 SSE 路径关闭。

Q3:SSE 连接一到 60 秒就断是为什么?

默认 proxy_read_timeout 是 60 秒,无数据即断。拉长超时并让后端按时发心跳即可。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
命令行手艺人的头像命令行手艺人普通用户

相关推荐

返回顶部