拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从能推理到扛得住:模型部署平台的服务编排实战

1. 从“能推理”到“扛得住”模型上线后的第一道坎在聊模型部署平台的服务编排之前我想先描述一个真实场景你的团队花了大半年训练、微调、评测终于把模型的精度做到了线上可用的水平结果到了部署阶段事情没那么简单了。第一版方案很简单——把模型封装成推理服务挂在容器里暴露一个HTTP接口前端调用一下能出结果Demo跑通大家都很开心。然后压力就来了。同一个平台里不只有一个模型而是有几十个模型在跑。有的做意图识别有的是实体抽取有的是Agent里那个负责工具调用的模型还有的是给用户做摘要生成的生成式大模型。这些模型有的是两三GB的BERT系列有的是70B级别的LLM对GPU显存的需求完全不同推理耗时从十几毫秒到十几秒都有。它们的调用方还不一样——线上真实流量、内部实验、批处理任务、联调环境——彼此之间还会抢资源。这时候人们才意识到模型推理服务能跑起来和整个推理平台能稳定、高效、可控地对外提供服务是两件完全不同的事。前者是单个服务的生命周期管理后者是平台层面如何处理“一堆服务之间如何协作、如何被调度、如何被治理”的问题。这个层面就是所谓的服务编排。这篇文章我会结合之前搭过的推理平台讲讲服务编排到底编排了什么哪些环节必须在一开始想清楚哪些坑是后面才暴露出来的。适合做AI平台、做推理加速、做后端架构的工程师参考也适合准备从“单模型部署”走向“多模型平台”的团队看看。2. 编排的对象是什么比“服务发现”多出三个维度很多人听到“服务编排”第一反应是微服务里的服务注册与发现。但AI模型部署平台里的编排范围要大得多。它要处理的不只是“某个服务在哪里”而是四个相互纠缠的问题。2.1 模型版本与运行时的绑定关系传统微服务里服务版本通常和代码发布绑定灰度、回滚都是围绕代码版本做的。模型部署则多了一个维度模型权重版本。一个推理镜像可以承载多个模型版本也可能一个模型版本对应多种推理配置。比如同一个Qwen模型有FP16版本、INT8量化版本、AWQ量化版本它们的显存占用、吞吐、精度都不一样。编排系统必须能清晰表达“某个模型版本跑在哪个镜像上、用的什么量化方式、绑定了几张卡”否则线上出了精度问题你连是哪个权重文件出了问题都定位不到。我见过最混乱的情况是团队用文件名区分模型版本模型文件通过NFS挂载到容器里更新时直接往同一个路径下覆盖文件。结果就是推理服务还在运行底层权重已经被换掉了线上请求前一半用旧权重后一半用新权重评测指标波动了一整周才找到原因。2.2 推理服务的拓扑关系第二个要编排的是服务之间的调用拓扑。如今的AI应用几乎没有“单模型单请求”的形态。一个Agent请求进来先过意图识别然后任务规划再调用多个工具工具返回结果后送进LLM做总结。这中间还穿插着RAG检索、prompt模板渲染、敏感词过滤、结果缓存。每个环节都可能是一个独立的模型服务。如果这些服务之间的调用关系是代码里硬编码的那么每加一个新模型、改一个prompt都要重新发布调用方的服务效率和稳定性都很差。服务编排要做的是把这种拓扑关系从代码里抽出来变成平台层的可配置项。哪个请求走哪个模型、多路候选模型怎么合并这些规则应该能被运维人员直接调整而不是每次改需求都动代码。2.3 异构资源与多维度的调度约束第三个维度也是最消耗精力的部分推理服务的调度约束比普通微服务复杂得多。普通服务编排只需要关心CPU和内存。推理服务则要看GPU型号、显存大小、是否支持FP8、有没有NVLink、同节点能不能再塞一个同样吃显存的服务。还要考虑显存碎片——一张80G的A100如果已经部署了一个需要50G显存的服务剩下的30G可能正好差一点塞不下另一个需要32G的模型这30G就浪费了。编排器如果能感知这类约束就能把多个模型合理地拼到同一张卡上对资源利用率的影响非常大。另外还要考虑模型冷启动时间。普通容器镜像拉取一般是秒级到分钟级大模型加载权重进显存可能要好几分钟热加载甚至要十几分钟。调度器如果按普通服务的思路来调度只看资源够不够不看服务是否已经完成预热流量就会打到还不可用的实例上造成大量超时。2.4 推理链路里的人类因素最后一个维度容易忽略但实际项目里往往最折磨人——人。算法工程师要部署新模型做实验平台工程师要保证线上稳定运维同学要排查故障不同角色对平台的需求是冲突的实验要快最好一键部署线上要稳必须审批严格。服务编排如果只考虑技术因素不考虑权限、审批流、资源配额这些管理因素最后大概率会出现“算法偷偷把实验服务部署到了生产GPU池”这种事故。3. 编排层的四个核心组件缺了哪个都会出事明确了对象之后再看编排系统本身的架构。我搭过的平台里最基础也最关键的组件可以归纳为四块网关、调度器、注册中心、状态管理。3.1 网关所有流量的必经之路网关承担的不只是转发。在推理平台里它还应该承担以下职责路由根据请求里的模型名、版本号、用户标识把请求分发到对应服务的实例上协议转换下游服务可能是HTTP、gRPC也可能是Triton的KServe协议网关统一对外暴露一个标准入口限流与配额不同调用方有不同的QPS配额实验流量和线上流量要分开限制灰度新版本模型先在网关上切一小部分流量验证没问题再全量。我见过很多团队图省事直接用Kubernetes的Service做负载均衡把模型服务的Pod暴露出去就完事了。这么做在小规模没问题但一旦涉及灰度、多版本、按用户维度切流量Service的负载均衡策略根本不够用。网关这层必须独立出来而且要早早地独立出来否则后面往回补成本很高。3.2 调度器编排系统的大脑调度器负责回答一个问题给定一个部署目标模型副本数资源要求放在哪台机器上跑最合适普通K8s的调度器处理CPU、内存、亲和性已经比较成熟但如果部署推理服务还需要扩展调度逻辑。以显存调度为例要支持“多个模型共享一张GPU卡”这种诉求就需要在调度器里做显存级的资源上报和分配。K8s默认把GPU当成一个只能整体分配的资源一张卡被某个Pod占了其他Pod就不能在这张卡上跑了。要支持同一张卡跑多个小模型得自己实现Device Plugin动态分配显存段并且把显存使用量实时上报给调度器——否则调度器根本不知道这张卡还剩多少显存可以用。3.3 注册中心与版本感知网关做路由时必须知道“某个模型当前有哪几个可用版本、每个版本有哪些实例、实例的状态是什么”。这些信息放到注册中心里但它又要比普通微服务的注册中心多一个维度模型健康状态。普通服务的健康检查一般只查进程是否存活、端口是否通。模型服务的健康检查要复杂得多——进程是活的不代表模型加载完成模型加载完成了不代表它能正常跑推理。我们当时的做法是健康检查接口里除了返回HTTP 200还带上模型加载状态、显存占用、平均推理延迟这些指标网关或注册中心根据这些信息决定要不要把流量打过去。3.4 状态管理让编排系统知道模型当前在干什么状态管理管的是模型服务的生命周期状态Deploying正在加载模型、Ready可对外服务、Updating正在热更新、Scaling正在扩容/缩容、Failed失败了、Stopped已停止。这个状态机和普通服务的状态机最大的区别在于普通服务从启动到Ready通常只需要几秒模型服务可能要几分钟甚至十几分钟普通服务的更新可以用滚动更新简单实现模型服务的更新要谨慎考虑显存是否能同时容纳新旧两个版本、加载期间流量怎么处理。没有明确的状态定义就无法可靠地实现自动化运维——自动扩容、自动故障恢复都无从谈起。4. 会话亲和性多副本模型实例上的第一个大坑基础组件设计好之后进入真正折磨人的细节部分。第一个值得展开讲的是会话亲和性Session Affinity。这个问题在普通微服务里也存在但在模型推理平台上会变得特别明显因为它直接关系到推理结果的一致性。4.1 问题是怎么来的假设你部署了一个对话模型开了4个副本。正常的负载均衡策略是轮询——请求依次打到各个副本上。如果是无状态服务这样没问题。但对话模型通常是有状态的上下文可能缓存在实例内存里。用户第一句话打到了副本A第二句话被负载均衡转发到了副本B副本B没有前文的上下文整个对话就断了。有一种做法是把上下文放到外部的Redis里所有副本共享。这样确实解决了亲和性问题但代价是每次推理都要多一次网络往返去取上下文延迟增加而且一旦推理服务本身被设计成无状态了很多内部优化就做不了——比如KV Cache键值缓存大模型推理中用于缓存历史token的中间计算结果的复用。我们的方案是网关根据不同维度做粘滞路由短连接场景用客户端ID哈希到固定副本长连接场景直接绑定连接和目标副本的关系连接不换副本就不换。同时规定跨副本的状态只允许放在外部存储放在实例内存里的状态必须允许重建。这样可以兼顾性能和可靠性。4.2 代码层面的复现建议如果你在用Kubernetes Nginx Ingress做网关最简单的粘滞会话配置是apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: model-service-ingress annotations: nginx.ingress.kubernetes.io/affinity: cookie nginx.ingress.kubernetes.io/session-cookie-name: route nginx.ingress.kubernetes.io/session-cookie-expires: 172800 nginx.ingress.kubernetes.io/session-cookie-max-age: 172800 spec: rules: - host: model.example.com http: paths: - path: /v1/chat pathType: Prefix backend: service: name: chat-model-service port: number: 8000这种基于Cookie的粘滞在简单的场景下够用但要注意Cookie策略的粒度是整个Ingress路径不是模型版本。如果同一个路径后面挂了多个模型版本灰度场景Cookie粘滞会把用户固定在灰度版本上导致灰度流量无法精确控制。我们后来的做法是网关直接从请求体里提取用户ID或会话ID做哈希绕开Cookie的维度限制。4.3 什么时候可以放弃会话亲和也不是所有模型都需要会话亲和。纯无状态的模型——比如文本向量化、单轮分类、代码补全无上下文——完全不需要亲和轮询反而是更优的策略因为能最大化利用每个副本的吞吐。这是服务编排设计里经常要做的权衡有状态的模型做语义级粘滞无状态的模型做纯负载均衡。5. 调度策略的实战取舍从简单哈希到有状态路由调度策略是服务编排里最容易“过度设计”的部分。我聊过不少团队一上来就想着搞全局最优调度——根据每台机器的实时负载、显存碎片情况、模型访问热度动态迁移实例。现实往往是系统做复杂了问题没变少反而多了很多新的故障点。我们的演进路径是这样的供参考5.1 阶段一资源约束 简单打分最开始只在K8s原生调度的基础上做了两件事显存约束通过自定义Device Plugin上报和节点亲和把同一个模型的不同副本尽量分散到不同节点上避免单节点故障全挂。打分逻辑只有一条CPU使用率低的节点优先。这已经能覆盖80%的场景线上跑得很稳。这个阶段的经验是先保证调度器不会做出错误的决定再考虑它能不能做出最优的决定。一个“够用但不最优”的调度器远胜一个逻辑复杂、一有边界情况就调度出错的调度器。5.2 阶段二调用关系的亲和调度第二个阶段我们加了“调用关系亲和”策略。前面提到Agent场景里存在服务间的调用拓扑——请求先进意图识别再去LLM最后经过后处理模型。如果意图识别服务的实例和LLM服务的实例不在同一台机器上每次调用都要跨节点走网络延迟高而且增加核心交换机的压力。我们做了个很简单的策略如果服务A和服务B有高频的调用关系调度器就把它们尽量调度到同一个节点上或者至少在同一个机架内。实现方式是在调度器里加一个调用图模块统计历史调用频次生成调度亲和规则。这个策略上线后整体推理链路的P99延迟降了大概15%——跨节点变成了本机内存调用效果非常显著。5.3 阶段三有状态路由的冷热分离第三阶段解决的是“热点实例”问题。大模型服务的流量分布往往极不均衡某些用户用量大某些时段流量集中。如果轮询策略把所有请求均匀分发到所有副本热点用户会被分散到多个副本上每个副本都要缓存同一份上下文显存浪费严重。我们做了冷热分离根据会话活跃度把副本分成“热副本”和“冷副本”热副本只服务活跃用户的高频请求冷副本处理低频请求和突发流量。网关维护一份“用户ID → 副本”的映射表并且定期把长时间不活跃的用户从热副本上移走释放显存。这本质上就是分布式系统里的分区与迁移问题只是数据换成了KV Cache。6. 流量治理的一个反直觉结论不能在所有失败模式上都做重试流量治理——限流、熔断、降级、重试——在普通微服务架构里有一套成熟的做法但用到模型推理服务上照搬会出问题。最直观的例子是重试。普通HTTP服务超时了重试一次没有什么代价。模型推理服务不一样——一个生成式模型的请求可能要推理几十秒消耗的GPU时间很长。如果网关层盲目重试一个已经超时的生成请求相当于把一个实例的负载翻倍。更糟的是如果下游模型服务真的出了故障所有请求都在重试会对已经濒临崩溃的服务做最后一击。这在实际故障里叫“重试风暴”会让原本只是局部故障的系统瞬间整体雪崩。我们的策略是重试只允许在CPU密集型的预处理服务上做GPU推理服务一律不做同步重试。对于必须保证不丢的请求网关把请求投递到消息队列由异步消费者去执行推理执行失败再重投最多三次并且设置退避间隔。同步接口则直接返回错误由上游业务自己决定怎么处理。限流方面也有一个容易忽视的点模型的QPS不是唯一需要注意的指标并发数和总Token数才是。一个请求可能很长生成的Token多占用的GPU时间久。如果只限制QPS每个请求都很长服务照样会被压垮。我们在网关层同时设置了两个维度的配额每秒请求数RPS和每秒生成Token总数TPS任何一个维度超限都触发限流。7. 状态管理设计模型在分布式世界里也需要“心跳”前面讲注册中心时提到了健康检查的复杂性这里展开讲讲状态机设计的具体细节因为这是后续所有自动化运维动作的基础。7.1 状态定义与流转一个完整的模型推理服务状态机比普通服务要多好几个状态状态含义触发方式INITIALIZING容器启动权重文件加载中新部署/重启BLOCKED依赖的上游服务不可用当前阻塞重试超限READY_CRITICAL已就绪但处于缩容保护期手动/策略触发DRAINING正在排空存量请求准备下线缩容/发布SHELVED已停止但权重文件保留便于快速恢复长期无流量有一种情况特别值得注意显存不足不等于服务不健康。模型服务在推理高峰时可能显存占用逼近上限出现OOM风险但它还能正常响应请求。如果在健康检查里把显存高占用当成不健康状态调度器就会频繁重启实例反而引发连锁故障。我们的做法是显存占用只上报、不参与健康判定是否触发扩容由独立的监控模块根据历史趋势决定。7.2 状态机驱动自动化运维有了准确的状态定义很多运维操作就可以自动化了自动扩容监控到某个Ready状态的实例平均延迟持续升高且显存还有余量则自动加副本新副本状态变为INITIALIZING加载完成后自动纳入负载均衡池故障自愈某个实例连续健康检查失败状态置为Failed调度器重新调度一个新实例到其他节点流量自动切换优雅下线发布新版本时旧实例先进入DRAINING状态排空存量请求后再销毁避免“正在生成一半的回答”突然断掉。这些规则说起来简单实际运行时对状态上报的时效性要求非常高。我们的心跳间隔是5秒健康检查超时是3秒连续2次失败才判定异常。心跳太频繁会产生大量无效上报太疏则故障感知延迟太长。这个参数是需要结合自己的业务容忍度去调的没有标准答案。8. 部署标准与镜像版本化让编排层真正“看见”模型编排系统的所有决策——调度、路由、伸缩——都必须基于对模型服务的准确描述。如果模型的元数据版本、量化方式、显存需求、推理框架版本不完整编排层就是瞎的。这一章节聊聊我们怎么定义部署标准踩过的坑也一并说。8.1 镜像Tag规范模型推理镜像的Tag不能只有日期或者简单的v1、v2必须能追溯到完整的模型信息。我们的Tag规范是{模型名}-{参数量}-{量化方式}-{推理框架}-{权重版本}-{构建时间} # 示例 qwen2.5-72b-awq-vllm-v1.2.4-202501151200 qwen2.5-7b-fp16-triton-v2.0.1-202501161430Tag里带了权重版本就能保证镜像不可变——同一个Tag永远指向同一个内容。如果权重更新了Tag必须变。这跟代码部署里的“不可变制品”原则一致。这个规范看着简单但执行时很容易被绕过。最常见的情况是算法同学觉得“我就测一下不用走正式的镜像流程”直接用调试容器里的环境跑推理跑通了临时做个Tag部署上线。结果就是线上服务用的镜像在镜像仓库里找不到对应Tag出了问题想回滚都找不到旧版本。我们的解法是推理平台只允许通过平台接口创建部署不允许直接在集群里手工起推理Pod从机制上保证审计链路完整。8.2 模型元数据描述镜像之外部署时还要附上一份完整的模型描述文件编排系统根据这份文件决定调度策略。我列一个最小可用的字段集modelName: qwen2.5-72b modelVersion: v1.2.4 framework: vllm container: image: registry.example.com/models/qwen2.5-72b-awq-vllm-v1.2.4-202501151200 ports: - name: http port: 8000 resources: gpu: count: 4 totalMemoryGB: 320 preferredModel: [A100-80G, H800-80G] cpu: 32 memory: 128Gi scaling: minReplicas: 1 maxReplicas: 8 metric: gpu_utilization targetValue: 60 cooldownSeconds: 300 healthCheck: path: /health timeoutSeconds: 3 periodSeconds: 5 maxFailures: 2 traffic: weight: 10 stickySession: true stickyKey: user_id这份文件不仅是部署用的配置也是平台自动生成监控大盘、按模型维度统计成本的依据。8.3 版本兼容矩阵最后必须维护一份“推理框架版本 × CUDA版本 × GPU型号”的兼容矩阵。AI领域各组件版本迭代太快不锁版本的话镜像哪天就挂给你看。docker镜像加固、依赖锁定这类安全措施也是必须纳入部署流水线的。我们踩过的真实例子某次升级了Triton版本结果在A100上一切正常在所有V100上推理结果不一致——CUDA的浮点运算差异导致的。这个问题在普通软件工程里根本不会遇到但在AI部署领域要时刻警惕。9. 弹性伸缩与冷启动先算清楚GPU的账再谈自动扩缩容弹性伸缩是每个搞平台的人都想做的功能真正落地后会遇到一个普通微服务领域不存在的难题扩容速度追不上流量增长。9.1 冷启动的时间账普通Web服务扩容新实例30秒内就能接流量。大模型推理服务呢我来拆一下70B模型实例的冷启动账环节耗时拉取镜像镜像约10GB20~40s加载权重140GBNVMe盘60~120s初始化CUDA/推理引擎10~30s预编译优化/图捕获30~60s总计2~4分钟你得先接受这个现实这里的扩容无法做到秒级响应。那就不能用“延迟超过阈值就立刻扩容”这种响应式策略必须改成预测式伸缩——基于历史流量曲线、定时任务、上游业务的活动计划提前把副本数加上去。9.2 缩容时的“慢保护”缩容同样有坑。流量下降后副本数缩到最小值GPU资源释放给其他服务使用——这个逻辑听起来没问题但有两个特殊情况一是热副本被缩掉了。前面提到会话亲和性会把活跃用户绑定到固定副本如果这个副本因为低流量被缩容了用户的下一个请求就被路由到冷副本上下文全部丢失体验断崖式下滑。我们的解法是在缩容评估时排除有活跃会话的副本宁可让它空闲也不能杀掉它。二是缩容要留足重置时间。显存释放给其他模型后如果几分钟后流量又涨回来了再扩容又得重新加载权重来回折腾。我们规定每次缩容后至少有5分钟的冷却时间窗口期间不接受新的缩容指令避免抖动。9.3 常驻副本策略最终我们用的策略是“分层弹性”核心高价值模型保持常驻副本即使低流量也不缩容保证RT长尾模型调用频率低但必须可用允许缩到零副本用Serverless的方式加载——有请求时才冷启动但客户端要接受第一次请求有额外延迟中间层模型按预测式伸缩策略调度。10. 可观测性线上故障排查的唯一依靠最后必须聊聊可观测性。服务编排系统做得再完善线上总会有你没想到的故障模式。这时候可观测性做得好不好直接决定故障定位时间是5分钟还是5小时。10.1 指标、链路、日志的AI特有维度监控指标除了常规的请求量、错误率、延迟推理平台还需要关注这几个维度显存吞吐GB/s如果实际低于理论值大概率是推理引擎配置出了问题KV Cache命中率对话模型场景下命中率低说明会话亲和策略失效各时间戳的分解队列等待耗时、预处理耗时、模型推理耗时、后处理耗时——如果队列等待耗时高说明实例数不够如果推理耗时长说明模型配置或者GPU规格有问题模型版本维度的事件流部署、扩缩容、健康检查失败等事件要能和日志、指标关联起来才能还原故障全链路。链路追踪方面我们用的是OpenTelemetry标准把模型名和版本号作为span的attribute打进去。这样查问题时可以按“模型名版本实例ID”一键过滤出所有相关日志和指标。10.2 一套实用的监控告警规则告警规则不能只盯着“请求失败率高”这种最终指标还要有面向原因的中间指标不然告警出来了你没法定位。我们配置的规则大致分三层层级监控项告警条件接入层网关入口成功率成功率99% 持续2分钟服务层推理实例平均延迟P99业务容忍上限 持续5分钟资源层节点显存使用率85% 持续10分钟中间层和资源层的告警才是能指导扩容的“先行指标”——显存使用率上升通常比请求成功率下降早出现几分钟抓住它能让你在故障发生前就做好应对。10.3 一次真实故障的排查复盘最后分享一次真实的故障复盘还原一下可观测性如何帮我们快速定位问题。现象某天下午平台整体的请求成功率从99.9%骤降到95%持续了大概10分钟后又恢复正常。排查过程先看网关层指标确认故障范围是整个平台还是某个模型——结果是只有某个对话模型异常再看该模型的实例级指标发现其中一台副本在异常时间段内健康检查失败其他副本正常说明不是模型整体的问题看那台副本的节点指标发现节点上的另一个模型的显存占用异常飙升把整张卡的显存吃完了导致目标副本OOM重启再看那个异常模型的部署记录发现是算法团队当天上线了一个新版本显存配置填写错误写低了20GB调度器按错误配置调度导致同节点的显存超分。链路还原后修复动作非常清晰暂停该版本的流量修正显存配置重新部署同时在平台侧加了新规——显存配置必须经过评测任务验证系统不允许直接提交未验证的显存参数。这次排查全程用了20多分钟。如果没有按模型维度切分的指标、没有事件流和版本记录的关联这个时间可能要乘以10。11. 排错过程中沉淀的检查清单以我个人的体会模型部署平台服务编排的项目里最终沉淀出的不只是一个系统还有一套排查问题的肌肉记忆。这里整理一份检查清单遇到线上问题可以照着过问题范围是单个模型还是多个模型单个是路由或实例问题多个是平台或资源问题是否新版本刚上线先对比新旧版本的资源占用和推理耗时是否有节点在故障期间被其他服务挤占了显存看节点级指标而不是只看实例级健康检查是否把资源高占用误判成了异常检查健康检查逻辑里是否混入了资源指标是否有人在“非标准流程”下直接改了线上配置对照部署审计日志看是否有绕过平台的操作当前实例的连接数和活跃会话数是否和负载均衡策略匹配确认没有出现会话亲和失效告警阈值是否设置得过灵敏有时候不是系统坏了是监控一直在误报持续了几天没人信了。在我们的实践里排错时间的大头往往不在“定位”而在“确认自己的动作不会引发二次问题”。这就是为什么前面反复强调状态机和可观测性——它们能给你一种底气操作有记录状态有反馈改错了能回滚。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门