阶段2(下):运维专题 Docker/Nginx/Linux/CI-CD/Git 的课程、企业场景、面试题和可运行示例
This commit is contained in:
1 parent
5eb67c5d08
commit
b8010a74f3
42 files changed
+3533
-55
No files matched your search
@@ -0,0 +1,109 @@
|
||||
---
|
||||
id: devops.nginx.scenario.ai-gateway-sse-canary
|
||||
title: AI 应用网关:流式输出不卡顿、按用户灰度新版本
|
||||
type: scenario
|
||||
domain: devops
|
||||
stack: [nginx, fastapi]
|
||||
tags: [SSE, 流式输出, 灰度发布, 限流, 网关]
|
||||
level: intermediate
|
||||
prerequisites: [devops.nginx.https-stream-limit]
|
||||
est_hours: 2
|
||||
related: [devops.nginx.interview, devops.docker.scenario.compose-dev-env]
|
||||
week: [7, 13, 14]
|
||||
---
|
||||
|
||||
# AI 应用网关:流式输出不卡顿、按用户灰度新版本
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
企业内部的 AI 问答助手:前端单页应用,后端 Python FastAPI 调用大模型并以 SSE 流式返回。上线后遇到三个问题:
|
||||
|
||||
1. 经过 Nginx 后,回答不再逐字出现,而是等十几秒后整段出现;长回答偶尔 504。
|
||||
2. 新版本的检索策略想先给内部 20 个同事试用,没有灰度手段,只能全量发布。
|
||||
3. 有人写脚本刷接口,模型调用费用突增。
|
||||
|
||||
## 约束
|
||||
|
||||
- 只有一台入口服务器上的 Nginx,不引入新的网关产品。
|
||||
- 灰度要能按用户指定,也要能随时关闭。
|
||||
- 正常用户的体验不能受限流影响。
|
||||
|
||||
## 方案设计
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
B[浏览器] --> N[Nginx]
|
||||
N -->|/ 静态资源| S[dist/]
|
||||
N -->|/api/ 限流 + 按 Cookie 选择| ST[AI 服务 稳定版]
|
||||
N -->|canary=1| CA[AI 服务 灰度版]
|
||||
N -->|/ai/stream 关闭缓冲| ST
|
||||
```
|
||||
|
||||
1. **流式**:`/ai/stream` 单独一个 location,`proxy_buffering off`、`proxy_cache off`、`proxy_read_timeout 300s`;后端每 15 秒发一条 `: ping` 心跳,防止中间设备断开空闲连接。
|
||||
2. **灰度**:`map $cookie_canary $api_backend`,内部同事访问一个“加入体验”页面,由后端为其设置 `canary=1` 的 Cookie;关闭灰度只需把 map 默认值都指向稳定版并 reload。也可以用 `split_clients` 按比例放量。
|
||||
3. **限流**:Nginx 按 IP 做第一道粗粒度限流(`limit_req`,返回 429);后端再按用户 / 租户做精细的配额(Redis,见 [Redis 限流](../../../backend/redis/03-lock-ratelimit.md)),并记录每个用户的模型调用量。
|
||||
|
||||
## 关键代码
|
||||
|
||||
完整配置模板和自动化测试见 [examples/gateway](../examples/gateway/):
|
||||
|
||||
```nginx
|
||||
map $cookie_canary $api_backend {
|
||||
default api_stable;
|
||||
"1" api_canary;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
limit_req zone=api_limit burst=5 nodelay;
|
||||
proxy_pass http://$api_backend;
|
||||
}
|
||||
|
||||
location /ai/stream {
|
||||
proxy_pass http://api_stable;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
```
|
||||
|
||||
FastAPI 端的 SSE 响应头和心跳(示意):
|
||||
|
||||
```python
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
|
||||
async def event_stream(chunks):
|
||||
async for text in chunks: # chunks:大模型返回的增量文本
|
||||
yield f"data: {text}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
|
||||
def sse(chunks) -> StreamingResponse:
|
||||
return StreamingResponse(event_stream(chunks), media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
||||
```
|
||||
|
||||
测试用真实 Nginx 验证了四点:SPA 回退、反向代理并传递 `X-Forwarded-For`、带 `canary=1` Cookie 的请求进入灰度后端、SSE 第一条事件不被缓冲、超过限流返回 429。
|
||||
|
||||
## 取舍
|
||||
|
||||
| 方案 | 优点 | 缺点 |
|
||||
| --- | --- | --- |
|
||||
| **Nginx map / split_clients(本方案)** | 无新组件,配置简单 | 规则复杂时难维护;改规则要 reload |
|
||||
| API 网关产品(APISIX、Kong 等) | 动态配置、插件丰富 | 多一套系统要运维 |
|
||||
| 应用内特性开关 | 粒度最细(按功能) | 需要改代码,新旧逻辑并存 |
|
||||
| Kubernetes + 服务网格 | 按权重、按头部灰度都很方便 | 需要 K8s 和网格基础设施 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 只在 `/api/` 关了缓冲,SSE 走的其实是另一个 location。
|
||||
- 开了 gzip 且把 `text/event-stream` 加进 `gzip_types`,流式又被攒起来。
|
||||
- `proxy_read_timeout` 理解成总时长:它是两次读之间的最大间隔,心跳能避免超时。
|
||||
- 灰度 Cookie 由前端随意设置:任何人都能进灰度。应由后端校验身份后设置。
|
||||
- 限流只在 Nginx 按 IP:公司出口是同一个 IP,所有同事一起被限。内部系统应按用户维度在后端限流。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “AI 助手经过 Nginx 后流式输出变成整段,原因是 Nginx 默认缓冲代理响应。我给流式接口单独配了 location,关闭缓冲、放宽读超时,后端加心跳;用 map 按 Cookie 把内部同事路由到灰度版本;在 Nginx 做按 IP 的粗限流、后端按用户做配额。我还写了自动化测试,用真实 Nginx 验证第一条事件不被缓冲、灰度和限流生效。”
|
||||
Reference in new issue
Block a user