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 坑
- CDN 挡在 Nginx 前面时:只配 Nginx 不够,CDN 的响应缓存也要关,源站加
Cache-Control: no-cache兜底,否则流式响应被 CDN 截断; - 心跳超时不对称:后端 15 秒一条心跳,但某一层(比如云负载均衡)的空闲超时只有 60 秒,把心跳间隔压到 10 秒以内才稳定;
- 把 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 秒,无数据即断。拉长超时并让后端按时发心跳即可。