模型服务网关的请求路由多模型负载均衡与优先级调度的设计一、当 5 个请求分给 3 个模型谁先谁后是个问题一个企业级的算法判题系统通常不止接入一个模型。可能在内部部署了开源的 Qwen云上接入了百度的文心还有 OpenAI 的 GPT 系列作为备选。这些模型的能力有高有低价格有贵有便宜响应速度有快有慢。前端源源不断地涌来判题请求后端网关需要做两个决策把这个请求派给哪个模型以及当多个请求在排队时谁优先被处理。这就是模型服务网关的核心职能——请求路由。一个好的路由策略能在资源有限的情况下最大化服务质量高优先级的付费用户走最快的模型低优先级的免费用户走兜底模型复杂的代码分析交给推理能力强的模型简单的语法检查交给轻量模型。二、路由决策的三个维度模型网关的路由不是简单的随机分发或轮询它需要在三个维度上同时做决策。维度一优先级。不是所有请求都同等重要。实时判题场景用户在前端等着结果和批量判题场景离线分析历史提交对时延的要求完全不同。如果两者的请求进了同一个队列批量任务会大量积压拖慢实时请求的响应。优先级调度通常用双队列实现一个高优队列一个低优队列调度器总是先消费高优队列高优队列为空时才处理低优队列。为了防止低优队列永远得不到处理即饥饿可以设置一个时间上限超过上限后强制处理低优请求。维度二模型能力匹配。不同题目适合不同模型。简单的语法判断代码能不能编译通过用轻量模型就够了响应快成本低。但复杂的算法复杂度分析这段代码的最坏情况时间复杂度是多少就需要推理能力强的大模型。把能力匹配做在路由层本质上是用计算资源的差异化分配换取整体成本效益的最大化。维度三健康状态。模型实例会宕机、会超时、会在高并发下响应变慢。路由层必须实时感知每个实例的健康状态。实现上通常用两种手段被动的失败计数连续失败 N 次后剔除和主动的健康探测定时发送探活请求。被剔除的实例会进入半开状态定时发送少量请求试探是否恢复确认恢复后重新加入可用池。三、优先级调度器的生产实现下面给出一个带双队列优先级调度的路由服务实现。核心是使用两个独立的优先级队列调度器按权重从两个队列中取任务。/** * 模型网关优先级调度器 * * 设计要点 * 1. 双队列分离高优和低优请求物理隔离 * 2. 权重调度避免低优请求饥饿 * 3. 队列容量上限防止内存溢出 */ public class PriorityModelRouter { // 高优先级队列容量较小确保快速消费 private final BlockingQueueJudgeTask highPriorityQueue new LinkedBlockingQueue(200); // 低优先级队列容量较大允许积压 private final BlockingQueueJudgeTask lowPriorityQueue new LinkedBlockingQueue(1000); // 高优和低优的调度权重每处理 7 个高优处理 3 个低优 private static final int HIGH_WEIGHT 7; private static final int LOW_WEIGHT 3; // 连续处理高优请求的计数用于饥饿检测 private int consecutiveHighCount 0; // 饥饿阈值连续处理 HIGH_STARVATION_THRESHOLD 个高优后强制处理一个低优 private static final int HIGH_STARVATION_THRESHOLD 20; /** * 提交判题任务到对应优先级队列 * * param task 判题任务 * throws RejectedTaskException 队列满时抛出 */ public void submit(JudgeTask task) { BlockingQueueJudgeTask targetQueue task.isHighPriority() ? highPriorityQueue : lowPriorityQueue; if (!targetQueue.offer(task)) { // 队列满根据优先级决定是否降级 if (task.isHighPriority()) { // 高优请求队列满尝试塞入低优队列很少发生 if (!lowPriorityQueue.offer(task)) { throw new RejectedTaskException(所有队列已满); } } else { throw new RejectedTaskException(低优先队列已满); } } } /** * 调度循环按权重从两个队列取任务分发 * * 在独立线程中运行 */ public void dispatchLoop() { while (!Thread.currentThread().isInterrupted()) { try { JudgeTask task selectNextTask(); if (task ! null) { // 根据任务复杂度选择模型池然后转发 ModelInstance target selectModel(task); target.dispatch(task); // 更新调度计数 if (task.isHighPriority()) { consecutiveHighCount; } else { consecutiveHighCount 0; } } else { // 两个队列都为空短暂休眠 Thread.sleep(50); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } /** * 按权重选择下一个任务 * * 规则 * - 有高优请求时优先处理高优 * - 但连续处理太多高优后强制处理低优防止饥饿 * - 只有高优队列空时才处理低优 */ private JudgeTask selectNextTask() { // 饥饿保护连续处理高优超过阈值强制处理低优 if (consecutiveHighCount HIGH_STARVATION_THRESHOLD) { JudgeTask lowTask lowPriorityQueue.poll(); if (lowTask ! null) { consecutiveHighCount 0; return lowTask; } } // 正常调度优先高优高优空时取低优 JudgeTask task highPriorityQueue.poll(); if (task ! null) { return task; } return lowPriorityQueue.poll(); } /** * 根据任务复杂度匹配合适的模型 */ private ModelInstance selectModel(JudgeTask task) { if (task.getComplexity() Complexity.SIMPLE) { // 简单任务优先轻量模型响应快成本低 return modelPool.getLightweightInstance(); } else { // 复杂任务分配推理能力强的模型 return modelPool.getHeavyInstance(); } } // 注入的模型池此处省略具体实现 private ModelPool modelPool; }这个调度器的核心设计是双队列 饥饿保护。如果不做饥饿保护高优队列持续有请求时低优队列里的请求可能永远得不到处理。饥饿保护的策略不复杂连续处理了一定数量的高优请求后强制从低优队列取一个请求出来处理。还有一个需要权衡的参数队列容量。容量设太小请求容易被拒绝容量设太大积压的请求会导致用户等待时间过长且内存有溢出风险。合理做法是根据系统压测数据确定容量然后在配置中心动态调整。四、负载均衡算法的选择优先级调度解决了先处理谁的问题负载均衡解决的是交给哪个实例的问题。最简单的方案是轮询但在模型服务场景下效果不好。因为不同请求的计算量差异很大——一道复杂的动态规划题可能让模型推理 30 秒而一道简单的语法检查可能只需要 2 秒。轮询不考虑当前负载可能把多个繁重任务分给同一个实例而另一个实例闲着。更好的方案是最少连接算法把请求分配给当前处理中请求数最少的实例。这个方案隐式地考虑了请求处理的耗时差异——处理慢的实例积压的请求多新请求也就不会再分配过去。还有一个容易忽略的设计点连接复用。每次 HTTP 请求都重新建连TLS 握手和 TCP 三次握手的开销不可忽略。路由层应该维护一个连接池对相同模型实例复用长连接减少建连开销。五、总结模型服务网关的请求路由本质上是把有限的计算资源不同能力的模型实例分配给不同优先级和复杂度的任务。优先级调度用双队列解决了实时请求和批量请求的冲突模型匹配用能力差异化分配降低了整体成本负载均衡用最少连接算法提升了资源利用率。这三个层面的设计配合起来就是一套实用的多模型路由方案。
模型服务网关的请求路由:多模型负载均衡与优先级调度的设计
模型服务网关的请求路由多模型负载均衡与优先级调度的设计一、当 5 个请求分给 3 个模型谁先谁后是个问题一个企业级的算法判题系统通常不止接入一个模型。可能在内部部署了开源的 Qwen云上接入了百度的文心还有 OpenAI 的 GPT 系列作为备选。这些模型的能力有高有低价格有贵有便宜响应速度有快有慢。前端源源不断地涌来判题请求后端网关需要做两个决策把这个请求派给哪个模型以及当多个请求在排队时谁优先被处理。这就是模型服务网关的核心职能——请求路由。一个好的路由策略能在资源有限的情况下最大化服务质量高优先级的付费用户走最快的模型低优先级的免费用户走兜底模型复杂的代码分析交给推理能力强的模型简单的语法检查交给轻量模型。二、路由决策的三个维度模型网关的路由不是简单的随机分发或轮询它需要在三个维度上同时做决策。维度一优先级。不是所有请求都同等重要。实时判题场景用户在前端等着结果和批量判题场景离线分析历史提交对时延的要求完全不同。如果两者的请求进了同一个队列批量任务会大量积压拖慢实时请求的响应。优先级调度通常用双队列实现一个高优队列一个低优队列调度器总是先消费高优队列高优队列为空时才处理低优队列。为了防止低优队列永远得不到处理即饥饿可以设置一个时间上限超过上限后强制处理低优请求。维度二模型能力匹配。不同题目适合不同模型。简单的语法判断代码能不能编译通过用轻量模型就够了响应快成本低。但复杂的算法复杂度分析这段代码的最坏情况时间复杂度是多少就需要推理能力强的大模型。把能力匹配做在路由层本质上是用计算资源的差异化分配换取整体成本效益的最大化。维度三健康状态。模型实例会宕机、会超时、会在高并发下响应变慢。路由层必须实时感知每个实例的健康状态。实现上通常用两种手段被动的失败计数连续失败 N 次后剔除和主动的健康探测定时发送探活请求。被剔除的实例会进入半开状态定时发送少量请求试探是否恢复确认恢复后重新加入可用池。三、优先级调度器的生产实现下面给出一个带双队列优先级调度的路由服务实现。核心是使用两个独立的优先级队列调度器按权重从两个队列中取任务。/** * 模型网关优先级调度器 * * 设计要点 * 1. 双队列分离高优和低优请求物理隔离 * 2. 权重调度避免低优请求饥饿 * 3. 队列容量上限防止内存溢出 */ public class PriorityModelRouter { // 高优先级队列容量较小确保快速消费 private final BlockingQueueJudgeTask highPriorityQueue new LinkedBlockingQueue(200); // 低优先级队列容量较大允许积压 private final BlockingQueueJudgeTask lowPriorityQueue new LinkedBlockingQueue(1000); // 高优和低优的调度权重每处理 7 个高优处理 3 个低优 private static final int HIGH_WEIGHT 7; private static final int LOW_WEIGHT 3; // 连续处理高优请求的计数用于饥饿检测 private int consecutiveHighCount 0; // 饥饿阈值连续处理 HIGH_STARVATION_THRESHOLD 个高优后强制处理一个低优 private static final int HIGH_STARVATION_THRESHOLD 20; /** * 提交判题任务到对应优先级队列 * * param task 判题任务 * throws RejectedTaskException 队列满时抛出 */ public void submit(JudgeTask task) { BlockingQueueJudgeTask targetQueue task.isHighPriority() ? highPriorityQueue : lowPriorityQueue; if (!targetQueue.offer(task)) { // 队列满根据优先级决定是否降级 if (task.isHighPriority()) { // 高优请求队列满尝试塞入低优队列很少发生 if (!lowPriorityQueue.offer(task)) { throw new RejectedTaskException(所有队列已满); } } else { throw new RejectedTaskException(低优先队列已满); } } } /** * 调度循环按权重从两个队列取任务分发 * * 在独立线程中运行 */ public void dispatchLoop() { while (!Thread.currentThread().isInterrupted()) { try { JudgeTask task selectNextTask(); if (task ! null) { // 根据任务复杂度选择模型池然后转发 ModelInstance target selectModel(task); target.dispatch(task); // 更新调度计数 if (task.isHighPriority()) { consecutiveHighCount; } else { consecutiveHighCount 0; } } else { // 两个队列都为空短暂休眠 Thread.sleep(50); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } /** * 按权重选择下一个任务 * * 规则 * - 有高优请求时优先处理高优 * - 但连续处理太多高优后强制处理低优防止饥饿 * - 只有高优队列空时才处理低优 */ private JudgeTask selectNextTask() { // 饥饿保护连续处理高优超过阈值强制处理低优 if (consecutiveHighCount HIGH_STARVATION_THRESHOLD) { JudgeTask lowTask lowPriorityQueue.poll(); if (lowTask ! null) { consecutiveHighCount 0; return lowTask; } } // 正常调度优先高优高优空时取低优 JudgeTask task highPriorityQueue.poll(); if (task ! null) { return task; } return lowPriorityQueue.poll(); } /** * 根据任务复杂度匹配合适的模型 */ private ModelInstance selectModel(JudgeTask task) { if (task.getComplexity() Complexity.SIMPLE) { // 简单任务优先轻量模型响应快成本低 return modelPool.getLightweightInstance(); } else { // 复杂任务分配推理能力强的模型 return modelPool.getHeavyInstance(); } } // 注入的模型池此处省略具体实现 private ModelPool modelPool; }这个调度器的核心设计是双队列 饥饿保护。如果不做饥饿保护高优队列持续有请求时低优队列里的请求可能永远得不到处理。饥饿保护的策略不复杂连续处理了一定数量的高优请求后强制从低优队列取一个请求出来处理。还有一个需要权衡的参数队列容量。容量设太小请求容易被拒绝容量设太大积压的请求会导致用户等待时间过长且内存有溢出风险。合理做法是根据系统压测数据确定容量然后在配置中心动态调整。四、负载均衡算法的选择优先级调度解决了先处理谁的问题负载均衡解决的是交给哪个实例的问题。最简单的方案是轮询但在模型服务场景下效果不好。因为不同请求的计算量差异很大——一道复杂的动态规划题可能让模型推理 30 秒而一道简单的语法检查可能只需要 2 秒。轮询不考虑当前负载可能把多个繁重任务分给同一个实例而另一个实例闲着。更好的方案是最少连接算法把请求分配给当前处理中请求数最少的实例。这个方案隐式地考虑了请求处理的耗时差异——处理慢的实例积压的请求多新请求也就不会再分配过去。还有一个容易忽略的设计点连接复用。每次 HTTP 请求都重新建连TLS 握手和 TCP 三次握手的开销不可忽略。路由层应该维护一个连接池对相同模型实例复用长连接减少建连开销。五、总结模型服务网关的请求路由本质上是把有限的计算资源不同能力的模型实例分配给不同优先级和复杂度的任务。优先级调度用双队列解决了实时请求和批量请求的冲突模型匹配用能力差异化分配降低了整体成本负载均衡用最少连接算法提升了资源利用率。这三个层面的设计配合起来就是一套实用的多模型路由方案。