当前位置: 首页 > news >正文

AI 推理服务优雅下线:Drain 状态与等待中的请求处理

AI 推理服务优雅下线:Drain 状态与等待中的请求处理

一、K8s 直接杀 Pod,推理请求全丢了,用户看到 502,老板脸绿了

推理服务和普通 Web 服务不一样的地方在于:一个请求可能跑几十秒甚至几分钟。K8s 默认的terminationGracePeriodSeconds: 30根本不够用。滚动更新时,旧 Pod 被 SIGTERM 杀死,正在跑的推理请求直接中断,用户再重试就报错。你以为是模型崩了,结果只是 Pod 被杀得太快。

优雅下线要做四件事:

  1. 摘流量—— 不要再给这个 Pod 发新请求
  2. 等现有的—— 正在跑的请求让它跑完(合理超时内)
  3. 释放资源—— 优雅关闭连接池、数据库连接、GPU 上下文
  4. 记录状态—— 把未完成的请求状态持久化,别丢了

二、优雅下线的关键时序与状态转移

核心思路:先在 Service 层面摘掉 Endpoint,再给 Pod 发 SIGTERM。这样新请求不会路由到这个 Pod,只有已经在处理的请求需要等。

三、生产级优雅下线实现

Go 推理服务优雅退出

package main import ( "context" "net/http" "os" "os/signal" "sync" "sync/atomic" "syscall" "time" ) // InferenceServer 带优雅下线能力的推理服务 type InferenceServer struct { server *http.Server activeRequests int64 // 当前正在处理的请求数 draining int32 // 是否进入 draining 模式 wg sync.WaitGroup } func NewInferenceServer(addr string) *InferenceServer { mux := http.NewServeMux() s := &InferenceServer{ server: &http.Server{Addr: addr, Handler: mux}, } mux.HandleFunc("/v1/infer", s.handleInfer) mux.HandleFunc("/healthz", s.handleHealthz) mux.HandleFunc("/readyz", s.handleReadyz) return s } func (s *InferenceServer) handleReadyz(w http.ResponseWriter, r *http.Request) { if atomic.LoadInt32(&s.draining) == 1 { w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte("draining")) return } // 还检查模型是否加载完毕 w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) } func (s *InferenceServer) handleHealthz(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) } func (s *InferenceServer) handleInfer(w http.ResponseWriter, r *http.Request) { // 进入 draining 模式后拒绝新请求 if atomic.LoadInt32(&s.draining) == 1 { w.Header().Set("Retry-After", "5") w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte(`{"error":"server is draining, please retry"}`)) return } atomic.AddInt64(&s.activeRequests, 1) defer atomic.AddInt64(&s.activeRequests, -1) // 模拟推理处理(实际场景是 GPU 推理) ctx := r.Context() select { case <-time.After(30 * time.Second): w.Write([]byte(`{"result":"inference done"}`)) case <-ctx.Done(): // 客户端断开了,不用等了 return } } func (s *InferenceServer) WaitForDrain(timeout time.Duration) error { deadline := time.Now().Add(timeout) ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() for time.Now().Before(deadline) { active := atomic.LoadInt64(&s.activeRequests) if active == 0 { return nil } <-ticker.C } return context.DeadlineExceeded } func (s *InferenceServer) Shutdown(ctx context.Context) error { // Step 1: 标记为 draining,拒绝新请求 atomic.StoreInt32(&s.draining, 1) // Step 2: 等待当前 in-flight 请求处理完 if err := s.WaitForDrain(25 * time.Second); err != nil { log.Printf("drain timeout, %d requests still active", atomic.LoadInt64(&s.activeRequests)) } // Step 3: 关闭 HTTP Server(给剩余连接 5s 缓冲) shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() return s.server.Shutdown(shutdownCtx) } func (s *InferenceServer) Run() error { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) go func() { <-sigCh // 收到终止信号后启动优雅关闭 log.Println("received shutdown signal, start draining...") if err := s.Shutdown(context.Background()); err != nil { log.Printf("shutdown error: %v", err) } }() return s.server.ListenAndServe() }

K8s Deployment 配置

apiVersion: apps/v1 kind: Deployment metadata: name: inference-server spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 # 关键:不允许不健康 Pod 存在 template: spec: # 关键参数:给推理请求充足的时间完成 terminationGracePeriodSeconds: 120 # 2 分钟 containers: - name: inference image: inference-server:v2.3.0 ports: - containerPort: 8080 lifecycle: preStop: exec: command: - /bin/sh - -c - | # 等 K8s 网络规则传播(10s 足够) sleep 10 # 调用自身优雅关闭接口 curl -X POST http://localhost:8080/admin/drain # 给足够时间处理 in-flight 请求 sleep 105 readinessProbe: httpGet: path: /readyz port: 8080 periodSeconds: 5 failureThreshold: 2 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 30 periodSeconds: 10 resources: limits: nvidia.com/gpu: 1

推理请求端重试策略

import httpx import asyncio from typing import Optional class InferenceClient: """带优雅重试的推理客户端""" def __init__(self, base_url: str, max_retries: int = 3): self.base_url = base_url self.max_retries = max_retries self.client = httpx.AsyncClient(timeout=120.0) async def infer(self, prompt: str) -> Optional[dict]: for attempt in range(self.max_retries): try: resp = await self.client.post( f"{self.base_url}/v1/infer", json={"prompt": prompt} ) if resp.status_code == 200: return resp.json() # 503 + Retry-After:服务在 draining if resp.status_code == 503: retry_after = int( resp.headers.get("Retry-After", "5") ) await asyncio.sleep(retry_after) continue resp.raise_for_status() except httpx.TimeoutException: # 推理超时,可能正在被 kill if attempt < self.max_retries - 1: await asyncio.sleep(2 ** attempt) continue raise except httpx.ConnectError: # 连接失败,Pod 可能已经退了 await asyncio.sleep(1) continue return None

四、边界分析与架构权衡

关键边界

边界场景风险缓解措施
推理请求超过 terminationGracePeriod被 SIGKILL 强制终止合理设置 MaxInferTime,超过则返回部分结果
GPU 显存中的 KV Cache 丢失请求需要重新计算支持断点续推(Checkpoint)
HPA 缩容过多 Pod剩余 Pod 过载限制缩容速率(scaleDown.stabilizationWindowSeconds: 300)
preStop hook 执行失败Pod 直接 SIGTERMHook 失败不影响容器停止
Client 侧没有重试用户直接看到错误Sidecar/API Gateway 层做透明重试
Graceful shutdown 触发 OOM等待期间内存增长限制 in-flight 请求上限(队列满即 503)

权衡决策

Drain 等待时间 vs 可用性:Drain 时间越长,请求成功率高,但发布变慢。生产环境建议:推理 P50 延迟 × 2 + 10s 缓冲。例如 P50=20s,则 terminationGracePeriod 设为 50s。

是否强制中断:超过 Drain 时间的请求,是记日志然后 kill,还是拼命等?推荐 kill 后依赖客户端重试。推理请求应设计为幂等(相同 prompt 返回相同结果或会话 ID),重试没副作用。

sidecar Drain 顺序:如果 Pod 有 Envoy/Istio sidecar,容器关闭顺序很关键。Sidecar 要比主容器晚退出,否则主容器发给 sidecar 的请求会失败。Istio 1.15+ 支持EXIT_ON_ZERO_ACTIVE_CONNECTIONS

五、总结

优雅下线看起来是运维问题,实际上是架构问题。你的服务如果不支持重试、不支持幂等、不支持部分结果返回,优雅下线做得再好也没用。

三个必须做:

  1. K8s 层:preStop hook + terminationGracePeriodSeconds > 推理最大耗时
  2. 应用层:Drain 标记 + in-flight 计数 + 503 + Retry-After
  3. 客户端:指数退避重试 + 连接池自动发现新 Endpoint

一个自检问题:你的推理服务,从发 SIGTERM 到完全停止,需要多少秒?如果答案是"不知道",这篇文章值得收藏。

http://www.jsqmd.com/news/1201814/

相关文章:

  • 2026 武汉中考落榜女生首选卫校 湖北现代科技学校护理招生条件 - 湖北找学校
  • 计算机毕业设计之基于SpringCloud的客服管理平台的设计与实现
  • OpenHuFu安全多方计算:秘密共享与差分隐私技术应用
  • AI [Unable to reach the model provider]
  • Elasticsearch RTF终极指南:中文优化的企业级搜索发行版
  • 2026年五大数据建模工具横评 - 科技焦点
  • Warp终端护眼指南:3步打造舒适高效的开发者视觉环境
  • ssm基于SSM的在线预约导游系统
  • 2026铜陵中考升学: 铜陵学子新选择!合肥理工【3+2中高职贯通班】,五年拿统招全日制大专文凭! - 最新资讯
  • 【架构实战】数据加密:敏感数据的存储与传输保护
  • 模型版本管理架构:每次上线不只是换一个权重文件
  • AgentForge未来路线图:MCP标准与新一代工具系统深度解析
  • 打造你的个性化数字伙伴:DyberPet桌面宠物框架深度解析
  • 2026广州闲置钻戒变现实用攻略,同城上门鉴定全程录像更安心 - 禹竞奢收行
  • JSP实习管理系统
  • Agent 事件驱动架构:用消息队列解耦 Agent 内部模块
  • 智能的PHP开发工具PhpStorm v2024.1全新发布——支持PHPUnit 11.0
  • 2026年水地源热泵品牌靠谱排行榜,选这几家准没错 - 官方资讯
  • 终极浏览器广告拦截指南:uBlock Origin让你的网络世界更干净、更安全、更快速
  • 雅典中国官方售后服务中心|地址与售后服务电话权威信息通告(2026年7月更新) - 亨得利官方服务中心
  • (十六)在判定假设下加密的安全证明(三)
  • Radare2高级功能:动态调试与漏洞利用实战
  • 计算机毕业设计之jsp新生儿疾病筛查信息系统
  • NeuroRebuild驱动的评标现场镜像孪生三维重构与全息智管平台
  • vue3中文字滚动代码
  • 上海翡翠回收避坑|5家主流平台实测对比!冰种/糯种翡翠这样卖最保值 - 奢侈品回收知识分享
  • 毕业论文不用“单工具硬扛”:8款AI工具组局,让研究想法3天“长成”答辩级论文 - 掌桥科研-AI论文写作
  • jspm足球联赛管理系统
  • 生命涌现的小龙虾技能之【Pet Picky Eater Detection | 宠物选择性拒食识别】简介
  • otel-desktop-viewer 与 Jaeger、Prometheus 对比:本地调试工具选型指南