--- id: devops.k8s.scenario.agent-rollout title: AI Agent 服务上 K8s:发布时流式回答不中断、慢启动、密钥管理和按负载扩容 type: scenario domain: devops stack: [k8s, docker, nginx, cicd] tags: [滚动更新, 优雅退出, SSE, 探针, HPA, Secret, 回滚] level: intermediate prerequisites: [devops.k8s.core-objects, devops.docker.image-dockerfile] est_hours: 2.5 related: [devops.k8s.interview, devops.cicd.release-rollback] week: [14] --- # AI Agent 服务上 K8s:发布时流式回答不中断、慢启动、密钥管理和按负载扩容 ## 业务背景与问题 Agent API 从虚拟机迁移到公司的 K8s 集群后: 1. 每次发布,正在进行中的流式回答被切断,用户看到“网络错误”; 2. 新版本启动需要预热(加载提示词模板、建立连接池),偶尔被重启循环; 3. 有人把模型 API Key 直接写在 Deployment 的环境变量里提交到了仓库; 4. 高峰期响应变慢,但 CPU 使用率并不高,HPA 没有扩容。 ## 约束 - 集群由平台团队维护,应用团队只能修改自己的清单。 - 回答可能持续 60 秒以上。 ## 方案设计 1. **优雅退出**: - 应用收到 SIGTERM 后:就绪检查立即返回失败、不再接受新请求、等待进行中的流结束(最长 80 秒)后退出; - `preStop` 短暂等待,给 Ingress / Service 摘除端点的时间; - `terminationGracePeriodSeconds: 90` 大于最长回答时间; - 滚动更新 `maxUnavailable: 0`。 2. **慢启动**:startupProbe 给足启动时间;就绪探针在预热完成后才返回成功。 3. **密钥**:轮换已泄露的 Key;清单中只引用 Secret;Secret 由密钥管理系统同步(或平台提供的方案);CI 中加密钥扫描。 4. **扩容指标**:AI 服务主要在等待模型返回,CPU 低但并发连接数高。改用“每个 Pod 的进行中请求数”作为自定义指标(需要平台支持指标适配器),或者先按经验提高副本下限。 5. **发布与回滚**:镜像固定版本;CI 中先 `kubeconform` + 规范测试;发布后观察错误率和首 token 延迟,异常时 `kubectl rollout undo`。 ## 关键代码 FastAPI 应用的优雅退出(示意): ```python import asyncio, signal active_streams = 0 shutting_down = False @app.get("/readyz") async def readyz(): if shutting_down: raise HTTPException(503) # 让 K8s 尽快摘除这个 Pod return {"ok": True} async def drain(timeout: float = 80): global shutting_down shutting_down = True loop = asyncio.get_running_loop() deadline = loop.time() + timeout while active_streams > 0 and loop.time() < deadline: await asyncio.sleep(0.5) # 等待进行中的流式回答结束 ``` (Uvicorn 自身也会在收到 SIGTERM 后停止接收新连接并等待请求完成,具体参数如 `--timeout-graceful-shutdown` 以官方文档为准。) 完整清单:[examples/agent-api](../examples/agent-api/)。SSE 在 Nginx 层的配置见 [AI 网关场景](../../nginx/scenarios/ai-gateway-sse-canary.md)。 ## 取舍 | 选择 | 优点 | 缺点 | | --- | --- | --- | | **优雅退出 + 长宽限期(本方案)** | 用户无感知 | 发布时间变长 | | 前端断线自动续传(从断点继续) | 对任何中断都有效 | 后端要保存生成进度,复杂 | | 发布时间放在低峰期 | 简单 | 治标不治本 | ## 易错点 - 宽限期比最长回答时间短,SIGKILL 依然切断。 - 应用作为 shell 脚本的子进程启动,收不到 SIGTERM(使用 exec 形式的启动命令)。 - 就绪探针在退出阶段仍然返回成功,新请求继续进来。 - 以为 Secret 是加密的。 ## 面试怎么讲 > “迁移到 K8s 后发布会切断流式回答。我做了优雅退出:收到 SIGTERM 后就绪探针立即失败,停止接新请求,等进行中的流结束;preStop 留出摘除端点的时间,宽限期设为 90 秒,大于最长回答;滚动更新不减少可用副本。慢启动用 startupProbe。泄露的 Key 立即轮换,清单只引用 Secret,CI 加密钥扫描。扩容方面,AI 服务 CPU 不高但并发高,改用进行中请求数作为指标。”