大模型推理服务的高并发架构:从请求排队到异步流式输出的全链路优化

📅2026/7/13 14:45:01 👁️次浏览
大模型推理服务的高并发架构:从请求排队到异步流式输出的全链路优化
大模型推理服务的高并发架构从请求排队到异步流式输出的全链路优化推理服务不是简单的模型API在生产环境中支撑数千QPS的推理请求需要在排队策略、流式传输、内存管理和容错机制四个维度做系统性的架构设计。本文基于实际项目中vLLM推理框架的部署经验逐一拆解这些关键环节。一、请求入队策略三种模式的适用场景与取舍推理服务的请求到达是高度不均匀的——峰值可能是均值的5-10倍。入队策略决定了系统在流量洪峰下的行为模式选错策略会在P99延迟和吞吐量之间产生不可接受的tradeoff。FIFO队列先入先出是最基础的实现。其优势是公平性——每个请求按到达顺序处理无饥饿问题。但在长序列推理场景下一个生成2048 tokens的请求会阻塞后续所有短文本请求导致队头阻塞Head-of-Line Blocking。优先级队列通过引入QoS分级来缓解队头阻塞。实际落地中将请求分为三个优先级P0实时交互用户对话场景TTFT要求500msP1准实时批量摘要生成TTFT要求2sP2离线批处理数据标注、向量化等异步任务优先级队列的实现不是无代价的——存在低优先级请求的饥饿风险。解决方案是引入老化机制Aging低优先级请求在队列中等待超过阈值如30秒后自动升级优先级。批处理合并Batching是提升GPU利用率的核心手段。单条推理请求的GPU利用率通常只有20-30%因为矩阵运算规模不足以填满计算单元。Continuous Batching技术允许在处理过程中动态插入新请求而非等待整批完成。实测数据在A100 GPU上采用Continuous Batching后吞吐量从12 req/s提升到45 req/s提升约3.75倍。flowchart TB subgraph Entry[请求入口层] A[HTTP/SSE Request] -- B{Header 解析} B --|X-Priority: 0| C[P0 实时队列] B --|X-Priority: 1| D[P1 准实时队列] B --|X-Priority: 2| E[P2 批处理队列] end subgraph Scheduler[调度层] C -- F[优先级调度器] D -- F E -- F F -- G{Aging Check} G --|等待30s| H[优先级提升] G --|正常| I[Continuous Batcher] H -- I end subgraph Inference[推理引擎层] I -- J[Token 生成循环] J -- K[KV Cache 管理] K -- L[GPU Kernel 执行] L -- J L -- M[流式输出] end subgraph Output[输出层] M -- N[SSE Writer] N -- O[背压检测] O --|客户端断开| P[资源回收] end生产环境的关键配置/** * 优先级队列调度器 —— 支持老化机制的推理请求调度 * * 核心设计 * 1. 三级优先级队列P0/P1/P2P0最高优先 * 2. 老化机制防止低优先级饥饿等待超过 agingThresholdMs 自动升级 * 3. 批处理合并连续批处理模式最大化 GPU 利用率 */ public class PriorityAwareScheduler { // 三级优先级队列使用 PriorityBlockingQueue 保证线程安全 private final PriorityBlockingQueueScheduledRequest[] queues; // 老化阈值P2 请求等待超过此值自动升级到 P1 private static final long AGING_THRESHOLD_MS 30_000L; // 调度器主循环开关 private volatile boolean running true; SuppressWarnings(unchecked) public PriorityAwareScheduler() { this.queues new PriorityBlockingQueue[3]; for (int i 0; i 3; i) { this.queues[i] new PriorityBlockingQueue( 1024, Comparator.comparingLong(ScheduledRequest::getEffectivePriority) ); } } /** * 提交推理请求到对应优先级队列 * param request 推理请求包装对象 * param priority 0P0实时, 1P1准实时, 2P2批处理 */ public void submit(InferenceRequest request, int priority) { if (priority 0 || priority 2) { throw new IllegalArgumentException(优先级必须在0-2之间, 收到: priority); } ScheduledRequest scheduled new ScheduledRequest( request, priority, System.currentTimeMillis() ); queues[priority].offer(scheduled); } /** * 获取下一个待处理的请求带老化机制 * 先检查 P0 队列 → P1 队列 → P2 队列含老化升级 * * param maxBatchSize 最大批处理大小 * return 从各队列中取出的请求批次 */ public ListInferenceRequest nextBatch(int maxBatchSize) { ListInferenceRequest batch new ArrayList(maxBatchSize); long now System.currentTimeMillis(); // 按优先级依次拉取 for (int pri 0; pri 3 batch.size() maxBatchSize; pri) { PriorityBlockingQueueScheduledRequest queue queues[pri]; ScheduledRequest scheduled queue.poll(); if (scheduled ! null) { batch.add(scheduled.getRequest()); continue; } // 如果当前优先级队列为空检查更低优先级是否有老化请求 for (int lower pri 1; lower 3; lower) { ScheduledRequest candidate queues[lower].peek(); if (candidate ! null (now - candidate.getEnqueueTime()) AGING_THRESHOLD_MS) { // 老化升级从低优先级队列取出 candidate queues[lower].poll(); if (candidate ! null) { candidate.setEffectivePriority(pri); batch.add(candidate.getRequest()); } } } } return batch; } /** 调度请求包装类 */ static class ScheduledRequest { private final InferenceRequest request; private final int originalPriority; private long effectivePriority; // 动态优先级老化后会降低数值 private final long enqueueTime; ScheduledRequest(InferenceRequest request, int priority, long enqueueTime) { this.request request; this.originalPriority priority; this.effectivePriority priority; this.enqueueTime enqueueTime; } InferenceRequest getRequest() { return request; } long getEffectivePriority() { return effectivePriority; } long getEnqueueTime() { return enqueueTime; } void setEffectivePriority(int newPriority) { this.effectivePriority newPriority; } } }二、HTTP/SSE 流式输出的后端设计大模型推理的核心体验差异在于首Token延迟TTFT。用户不能等5秒看到完整结果——必须在几百毫秒内看到第一个字。SSEServer-Sent Events是实现这一目标的标准协议。SSE 相比 WebSocket 的优势在于单向数据流的场景下更轻量——无需握手升级协议浏览器原生支持自动重连且与 HTTP/2 多路复用天然兼容。后端实现的核心模式是Reactive Streams。以 Spring WebFlux 为例/** * 推理结果流式推送控制器 * * 使用 Spring WebFlux 的 Reactive 模型实现 SSE 流式输出。 * Flux 的背压机制能自动感知客户端消费速度避免内存堆积。 */ RestController RequestMapping(/api/v1/inference) public class StreamingInferenceController { private final InferenceEngine inferenceEngine; public StreamingInferenceController(InferenceEngine inferenceEngine) { this.inferenceEngine inferenceEngine; } /** * 流式推理接口 * * 返回类型 MediaType.TEXT_EVENT_STREAM 触发浏览器 SSE 解析。 * Flux 支持背压如果客户端消费慢上游自动降速。 * * param request 推理请求prompt 参数 * return SSE 事件流每个事件包含一个生成的 token */ PostMapping(value /chat, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxServerSentEventString streamInference( RequestBody InferenceRequest request) { // 参数校验prompt 不能为空max_tokens 需在合理范围 if (request.getPrompt() null || request.getPrompt().isBlank()) { return Flux.error(new IllegalArgumentException(prompt 不能为空)); } int maxTokens Math.min(request.getMaxTokens(), 4096); return inferenceEngine.generateStream(request.getPrompt(), maxTokens) // 将每个 token 包装为 SSE 事件 .map(token - ServerSentEvent.Stringbuilder() .data(token) .id(String.valueOf(System.nanoTime())) .event(token) .build()) // 发送完成信号 .concatWith(Flux.just( ServerSentEvent.Stringbuilder() .event(done) .data([DONE]) .build())) // 客户端断开时清理资源 .doOnCancel(() - { inferenceEngine.cancelGeneration(request.getSessionId()); }) // 异常处理返回错误事件而非中断连接 .onErrorResume(e - Flux.just( ServerSentEvent.Stringbuilder() .event(error) .data({\error\: \ e.getMessage() \}) .build())); } }三、背压控制与客户端断连处理流式场景下两个问题不可回避一是客户端消费速度跟不上生成速度需要背压二是客户端中途断连需要及时回收GPU资源。背压控制在Reactive框架中有天然支持——Flux/Mono的onBackpressureBuffer和onBackpressureDrop策略。对于推理场景推荐onBackpressureLatest如果下游消费慢丢弃中间token只保留最新的。原因在于推理是token序列跳过中间某些token后继续发送后续token对客户端毫无意义。正确的做法是当缓冲区满时触发降级——从流式模式切换为批量返回模式。客户端断连处理在GPU资源昂贵的场景下至关重要。一个A100 GPU的推理实例成本约$3-5/小时如果客户端断连后不回收会导致资源泄漏。检测机制有两种SSE心跳检测每15秒发送: heartbeat\n\nSSE注释格式不触发data事件连续3次无响应视为断连ResponseBodyEmitter回调Spring的onCompletion/onTimeout回调直接感知连接状态/** * 推理会话生命周期管理器 * * 关键职责 * 1. 追踪每个活跃推理会话的 GPU 资源占用 * 2. 在客户端断开后及时回收 KV Cache 和 GPU 内存 * 3. 心跳检测僵尸连接 */ Component public class InferenceSessionManager { // sessionId → 推理会话元数据 private final ConcurrentHashMapString, SessionContext activeSessions new ConcurrentHashMap(); // 定时心跳检测线程 private final ScheduledExecutorService heartbeatExecutor Executors.newSingleThreadScheduledExecutor(); public InferenceSessionManager() { // 每 15 秒扫描一次僵尸会话 heartbeatExecutor.scheduleAtFixedRate( this::purgeZombieSessions, 15, 15, TimeUnit.SECONDS ); } /** * 注册新会话绑定 GPU 资源引用 */ public void register(String sessionId, GpuContext gpuContext) { activeSessions.put(sessionId, new SessionContext( sessionId, gpuContext, System.currentTimeMillis() )); } /** * 客户端正常断开时释放 GPU 资源 * * return 释放的 GPU 显存量MB */ public long release(String sessionId) { SessionContext ctx activeSessions.remove(sessionId); if (ctx null) return 0; // 释放 KV Cache 和模型上下文 long freedMemory ctx.gpuContext.freeKVCache(); // 确保没有残留的 CUDA 内存分配 ctx.gpuContext.releaseTensorMemory(); return freedMemory; } /** * 清理心跳超时的僵尸会话 * 阈值超过 45 秒无心跳的会话视为僵尸强制回收 */ private void purgeZombieSessions() { long now System.currentTimeMillis(); long zombieThreshold 45_000L; activeSessions.entrySet().removeIf(entry - { SessionContext ctx entry.getValue(); if (now - ctx.lastHeartbeat zombieThreshold) { // 强制回收 GPU 资源 ctx.gpuContext.forceRelease(); return true; } return false; }); } static class SessionContext { final String sessionId; final GpuContext gpuContext; volatile long lastHeartbeat; SessionContext(String sessionId, GpuContext gpuContext, long startTime) { this.sessionId sessionId; this.gpuContext gpuContext; this.lastHeartbeat startTime; } } }四、GPU 内存管理与 KV Cache 复用KV Cache 是推理服务内存占用的主要来源。一个简单的计算对于 LLaMA-70B 模型FP16每个 token 的 KV Cache 约 2.5MB40层×2×8192维×2字节×2KV。生成 2048 tokens 的序列单个请求占用约 5GB 显存。KV Cache 的复用策略是降低成本的核心Prefix Caching当多个请求共享相同的系统提示词System Prompt时只计算一次前缀的 KV Cache后续请求直接复用。实测效果在客服机器人场景中System Prompt 约 500 tokens100 并发请求下节省约 8GB 显存。PagedAttentionvLLM 方案将 KV Cache 分割为固定大小的 Block如 16 tokens允许不同请求的 Block 在不连续的物理内存中存储通过虚拟地址映射实现逻辑连续。这种方式将显存利用率从传统方案的 20-30% 提升到约 80%。Swapping 策略当显存不足时将低优先级请求的 KV Cache 换出到 CPU 内存速度降低约 20 倍待 GPU 空闲时再换回。这是用延迟换吞吐的经典tradeoff——但对于 P2 批处理请求多等 2 秒是可以接受的。sequenceDiagram participant Client as 客户端 participant Gateway as API Gateway participant Scheduler as 优先级调度器 participant Batcher as Continuous Batcher participant Engine as 推理引擎(GPU) participant CacheMgr as KV Cache 管理器 Client-Gateway: POST /chat (SSE) Gateway-Scheduler: 请求入队 (含优先级) Note over Scheduler: 老化机制检查 Scheduler-Batcher: 出队 Continuous Batch Batcher-CacheMgr: 查询 Prefix Cache CacheMgr--Batcher: 命中缓存 (System Prompt) Batcher-Engine: 提交推理批次 activate Engine loop Token 生成循环 Engine-CacheMgr: 写入 KV Cache Block Engine--Batcher: Token LogProb Batcher--Client: SSE: token alt 客户端断开 Client--xBatcher: TCP RST Batcher-CacheMgr: 回收 KV Cache Batcher-Engine: 取消生成 deactivate Engine end end Engine--Batcher: [EOS] Token deactivate Engine Batcher--Client: SSE: [DONE] Batcher-CacheMgr: 标记 Cache 可回收五、总结推理服务的高并发架构本质上是在四个维度上做tradeoff决策排队策略FIFO简单公平但存在队头阻塞优先级队列改善了实时体验但引入了饥饿风险需老化机制对冲Continuous Batching通过动态组批将GPU利用率从20-30%提升到接近80%。流式输出SSE协议在单向流场景下比WebSocket更轻量Reactive Streams的背压机制是防止内存堆积的关键屏障每15秒一次的心跳检测能及时发现僵尸连接。客户端断连GPU资源是推理服务的核心成本——一个断连未回收的请求可能浪费$3-5/小时的算力。基于回调定时扫描的双重检测机制是生产环境的标配。KV Cache管理Prefix Caching在共享前缀场景下可节省数十GB显存PagedAttention通过虚拟内存式的块管理将显存利用率提升到80%Swapping策略用CPU内存作为GPU显存的swap空间实现延迟换吞吐。这四个维度不是孤立优化的——排队策略影响批处理效率批处理效率影响KV Cache的碎片化程度KV Cache的碎片化又反过来限制并发数。系统性地理解它们之间的耦合关系才能在给定的GPU预算下达到最优的吞吐量-延迟平衡。