OmniRoute 准入通道(Admission Lanes)架构解析:字节级准入、自适应虚拟通道与扇出探针
导读
Admission Lanes(准入通道)是 OmniRoute 内置的两套进程内限流/准入系统:一套以「字节预算」为粒度的进程级准入(gate 在请求体缓冲/解析之前),另一套以「租户键」为维度的自适应运行时虚拟通道(gate 在 provider 分发之前)。两套系统作用域互补,配合 #9654 引入的扇出探针(fan-out probe) 后,能同时抑制"大请求体放大堆内存"与"单会话突发打挂其他租户"两类经典故障。读完本文,你将掌握:两套 lane 各自守什么门、在何处上报、如何通过环境变量调优,以及 combo/fusion 多路分发时 per-target 准入的设计权衡。
本文配套源码位于 docs/architecture/admission-lanes.md,实现代码见 src/shared/middleware/chatBodyAdmission.ts、src/shared/middleware/admissionBudget.ts 与 open-sse/services/admission。
概览:为什么 OmniRoute 需要两套 lane 系统
OmniRoute 的进程内有两套作用域完全不同的 lane 系统,二者互补,运维者在观测时务必先搞清楚"自己在看的是哪一套":
| 系统 | 文件/目录 | 作用域 | 守什么门 | 是否默认启用 |
|---|---|---|---|---|
| System 1 字节级进程级准入 | chatBodyAdmission.ts | 进程全局单控制器 | POST /v1/chat/completions、/v1/messages、/v1/responses 等 chat 形态路由的请求体缓冲/解析路径(防堆放大 #4380) |
是(无条件生效) |
| System 2 自适应运行时虚拟通道 | open-sse/services/admission | 按租户键的 provider 分发 | 队列成本、延迟引导的限流自适应、通道排队、通道指标 | 否(opt-in,需 OMNIROUTE_CHAT_VIRTUAL_LANES=true) |
| Fan-out 探针(#9654 Wave 2) | 依赖 System 2 | 每个 combo/fusion 扇出目标 | 基于父请求租户通道的 per-target 分发前 gate | 随 System 2 开启 |
#9654 的判据 1("一个会话的突发不会把另一个会话打成 503")由 System 1 无条件强制,由 System 2 在 opt-in 后强制。
1. 字节级进程级准入(chatBodyAdmission.ts)
1.1 作用域与设计动机
Byte-level 准入守卫的是 buffered-body/heap 路径:POST /v1/chat/completions、/v1/messages、/v1/responses 及其他 chat 形态路由。其直接对抗的故障是 heap amplification(堆放大,#4380):大体积的 coding-agent 请求体在被解析、翻译、压缩、分发的过程中会衍生出多份瞬时内存表示(原始 UTF-8 buffer + JS 字符串 + 解析后的对象图 + 翻译后的对象图会短暂共存)。仅靠堆快照无法阻止两个健康请求同时进入这条高分配路径,因此该模块在解析之前就进行进程本地的重量级容量预留(reserve),且硬限制基于实际读到的字节数而非不可信的 Content-Length 头。
从 chatBodyAdmission.ts 头部注释 可以看到,模块宣称的对齐目标正是 "one connection's burst cannot starve others"(#9654)。
1.2 一个进程全局控制器,而非 per-key 通道(#10110)
重点演进:每个 API key(哈希后)或
anonymous会话都在对同一个共享预算做准入,会话的哈希 ID 只用作公平调度的 key(round-robin 分发 waiter),绝不做容量分片。
文档明确标注:旧版本曾描述过"每 key 独立容量的 lane",该模型在 #10110 被移除,原因是它允许未认证的伪造凭据把进程级上限放大。代码在 chatBodyAdmission.ts 中同样给出了注释:pre-#10110 的设计为每个会话 new 一个 controller,等于把进程上限乘以最多 64 个 lane,让伪造凭据可以分片容量。
因此现在的实现是单例 perConnectionAdmissionController(见 getController() 恒返回同一实例),会话身份仅作为 per-key FIFO 队列 的分组键,容量释放时按 key 做 round-robin 轮转唤醒,保证某客户端突发无法独占所有空闲槽位。
1.3 Gate:#503-fanout 引入的自动推导「摄入字节预算」
旧的 CHAT_MAX_HEAVY_IN_FLIGHT 请求数上限(默认 1)把 coding-agent 的扇出行为(多 subagent/多 CLI,请求体经常 > 256 KB)压缩成了约 1 的有效并发,在完全正常的负载下就会打出 503。
新模型下:
- 仅当运维者显式设置
OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT时,请求数上限才生效; - 不设置时,准入改由
OMNIROUTE_CHAT_MAX_INFLIGHT_BYTES门控——这是一个由进程真实内存上限自动推导的字节预算,实现在 src/shared/middleware/admissionBudget.ts。
推导公式(见 computeIngestByteBudget 的常量定义):
- 取 V8 堆上限 与 cgroup/容器上限 中更紧的那一个作为 effective ceiling;
ceiling × 25%(INGEST_HEAP_FRACTION) ÷ 8(INGEST_AMPLIFICATION 瞬时放大系数);- 结果夹在 8 MiB 与 2 GiB 之间(
MIN_INGEST_BUDGET_BYTES/MAX_INGEST_BUDGET_BYTES)。
因此该预算能从 512 MB 容器自动伸缩到 32 GB 桌面,无需任何环境变量调参。显式覆盖(OMNIROUTE_CHAT_MAX_INFLIGHT_BYTES)同样经过相同的钳制。cgroup 上限通过 Node 的 process.constrainedMemory()(Node >= 19.6/20.13)读取;缺失时回退到 V8 堆上限(source: "v8_heap")。预算计算是纯函数且解析后缓存(resolveIngestByteBudget 首次调用后缓存),热路径上不会反复读取 cgroup/V8 状态。
1.4 三类被拒路径与资源压力协同
结合 admitChatRequest 的执行顺序,可以清晰看到字节级准入门的工作流:
- 内部自环子请求(vision-bridge 等 describe 调用)走
isInternalAdmissionBypass:跳过 lease 预留(父请求已持有),但仍强制硬字节上限; - critical 资源压力:任一字节被摄入前,若
pressureSeverity() === "critical",立即以503 resource_pressure甩负载(shed),并记录 shed reason; Content-Length已知且 > hardMaxBytes → 立即413(对应chatAdmissionRejectionResponse的413语义);Content-Length≥ largeBodyBytes 但预算放不下(!canFitBudget)→413 body_exceeds_budget,即文档所说"一个无法装进有效预算的请求体立即失败";- 未知长度(chunked):只在超过 heavy 阈值后才做有界嗅探(bounded sniff),小请求不浪费重量级容量;摄入期间若总字节 > hardMax →
413,若 ≥ largeBody 且预算不足 →413 body_exceeds_budget,若无法 reserve → 503; - 只有"单个请求本身可服务、但彼此争用"的情况才进入有界公平队列(
acquireHeavyWithin内的#dispatchFair+ 队列字节堆阀maxQueuedBytes,默认 4 MB)。
资源压力追踪器是多信号的实时探测器(V8 堆比例、cgroup、PSI、OOM 事件,见 open-sse/utils/resourcePressurePolicy.ts):
high压力下缩短有界等待时间;critical压力下在任何字节被摄入之前立即以503 resource_pressure甩负载。
1.5 调优参数总表(System 1)
| 环境变量 | 含义 | 默认值 |
|---|---|---|
OMNIROUTE_CHAT_MAX_INFLIGHT_BYTES |
覆盖自动推导的字节预算(走相同 8 MiB–2 GiB 钳制) | 自动推导 |
OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT |
遗留的请求数上限,opt-in;不设置时视为无限 | 未设置 → 不生效 |
OMNIROUTE_CHAT_ADMISSION_QUEUE_MS |
排队等待时长,超时返回 503 | 2000 ms |
OMNIROUTE_CHAT_ADMISSION_MAX_QUEUED_BYTES |
排队字节的堆阀值(防止大请求体扎堆驻留放大堆) | 4 MB |
OMNIROUTE_CHAT_VIRTUAL_TTL_MS / OMNIROUTE_CHAT_VIRTUAL_MAX_SESSIONS |
自 #10110 起废弃的空操作(为了配置兼容被接受、被忽略) | — |
源码中还可见其余可调项:OMNIROUTE_CHAT_LARGE_BODY_BYTES(大请求体阈值,默认 256 KB)、OMNIROUTE_CHAT_HARD_MAX_BODY_BYTES(硬上限,默认 50 MB)、CHAT_ADMISSION_HEAP_SHED_RATIO(堆压 shed 比例,默认 0.75)、CHAT_ADMISSION_HEALTHY_HEADROOM(健康堆快速路径的有界余量)与 OMNIROUTE_CHAT_HARD_MAX_MESSAGES(历史条数硬上限,默认 0 即关闭,将超长会话交给压缩管线处理),详见 chatBodyAdmission.ts 常量区。
1.6 上报:health 快照中的 chatAdmission
GET /api/monitoring/health → chatAdmission(#11244),由 src/app/api/monitoring/health/route.ts 调用 perConnectionAdmissionController.snapshot() 产出(snapshot 结构见 chatBodyAdmission.ts)。除了 activeHeavy、waiting、queuedBytes、shedTotal、shedsByReason 之外,还包含 #503-fanout 新增字段:
inflightBytes/maxInflightBytes:当前摄入中的字节与预算上限;budgetSource:v8_heap|cgroup|override;pressureSeverity:normal|high|critical;countCapEnabled:默认部署下为false—— 恰好证明真正生效的是字节预算而非遗留的请求数上限。
ChatAdmissionShedReason 的类型定义(chatBodyAdmission.ts)将 shed 原因收敛为 queue_timeout / queued_bytes_budget / body_exceeds_budget / inflight_bytes_budget / resource_pressure,与 shedsByReason 一一对应,可作为排障依据。
2. 自适应运行时虚拟通道(open-sse/services/admission)
2.1 作用域与本质
System 2 位于 open-sse/services/admission,其 index 注释称其为 "Pure weighted adaptive admission-control core"(纯加权自适应准入控制核心,无路由/设置/环境 wiring)。它的关注点是 tenant-key 的 provider 分发准入:队列成本、延迟引导的 limit 自适应、通道排队与通道指标。
核心控制器为 AdaptiveAdmissionController,纯类型契约(AdmissionRequest/AdmissionLease/AdmissionSnapshot、off | shadow | enforce 三态 mode、normal | high | critical 压力级别、租户快照结构)定义在 types.ts。关键点:
AdmissionRequest.tenantKey是不透明公平键,绝不进快照(opaque fairness key; never exposed in snapshots);- 成本可以由调用方直接传
cost,也可以只传features(bodyBytes、estimatedInputTokens、messageCount、toolCount、requestedFanout、streaming),由 cost.ts 内的estimateAdmissionCost结合 cost 配置推导。
2.2 Gate:opt-in,默认共享队列行为
System 2 是 opt-in:除非设置 OMNIROUTE_CHAT_VIRTUAL_LANES=true,否则该自适应控制器保持共享队列行为(即 #9654 判据 1 只有在运维者开启 lanes 后才成立)。配置结构见 types.ts 的 AdaptiveAdmissionConfig,其中显式标注了 "Per-tenant virtual admission lanes (#9654). Default: false" 的 virtualLanes 字段。
配置解析(config.ts)做了严格校验:minLimit <= maxLimit、criticalDecreaseFactor <= decreaseFactor、lowUtilizationThreshold < highUtilizationThreshold、shortLatencyAlpha > longLatencyAlpha 等,越界直接抛 RangeError。
2.3 调优参数
| 配置项 | 含义 | 默认值(config.ts) |
|---|---|---|
mode |
off / shadow / enforce |
shadow |
minLimit / maxLimit / initialLimit |
并发/成本限额的上下界与初值 | 必填 |
maxQueueCount |
排队条数上限 | 必填 |
maxQueueCost |
排队成本上限 | 必填 |
defaultMaxWaitMs |
默认最大等待时间 | 5000 ms |
windowMs |
自适应窗口 | 1000 ms |
decreaseFactor / criticalDecreaseFactor |
降限因子 | 0.8 / 0.5 |
increaseStep / maxIncreasePerWindow |
升限步长与单窗口上限 | 1(同步长) |
shortLatencyAlpha / longLatencyAlpha |
双 EWMA 延迟系数 | 0.5 / 0.1 |
highUtilizationThreshold / lowUtilizationThreshold |
利用率升降阈值 | 0.7 / 0.3 |
latencyGradientThreshold |
延迟梯度阈值 | 0.25 |
virtualLanes |
是否开启租户虚拟通道 | false |
2.4 上报:health 快照中的 adaptiveAdmission
GET /api/monitoring/health → adaptiveAdmission(health route 中经 getAdaptiveAdmissionRuntime().snapshot() 取数),其中:
laneCount/laneQueuedCount/laneQueuedCost:租户通道规模与排队量;laneTenants:不透明 lane ID 列表,绝不暴露原始 key(见 types.ts 的 laneTenants 注释 "opaque keys, never raw API keys");virtualLanes:快照中"通道已开启"的权威标志位(true/false);- 其余如
admittedCount、rejectedCount、currentLimit、utilization、双 EWMA 延迟、pressure等均可在AdmissionSnapshot中直接观测。
3. 扇出探针:combo/fusion 的 per-target 准入(#9654 Wave 2)
3.1 动机
Combo(priority / round-robin)与 fusion 会在一个父请求下扇出 N 个模型目标。自 #9654 Wave 2 起,每个扇出目标在分发前都由一个 per-target 探针(PerTargetAdmissionHook,由 createPerTargetAdmissionHook 构建,类型契约见 types.ts)对着父请求的租户通道做 gate。
- 作用域:combo、fusion 以及 chaos engine 分发的每一个扇出目标。System 1(字节级)不受影响——它从不探测扇出目标;
- Gate:随 System 2 opt-in。未设置
OMNIROUTE_CHAT_VIRTUAL_LANES时为 no-op——因为该模式下父请求已经持有共享队列的 lease,再探测会造成双重计数、误伤 combo 目标; - 探测只发生在 provider 分发路径,字节级摄入路径对扇出目标天然"看不见"。
3.2 语义一:严格非阻塞——skip, never queue
maxWaitMs 0:通道满时跳过该目标,由 combo 的 fallback 机制(或 fusion 的 survivor panel)顶上。这是刻意设计——扇出目标是冗余工作,为它排队等于往通道本来要阻止的拥塞上继续堆负载。因此:
defaultMaxWaitMs只作用于父请求;- 扇出探针永不等待,且故意没有任何 knob能让它们等待(issue 历史显示,加等待 knob 曾产生 #9654 要阻止的大规模 502/504;除非运维者报告"被跳过的扇出目标伤害了响应质量",否则不会回归该能力)。
3.3 语义二:admit 即释放
被允许的探针立即释放它的 lease:它是容量门(capacity gate),不是占用(hold)。父请求自己的 lease 已经覆盖了整个扇出;若再持有 N 个,会膨胀共享 active cost、把其他租户拒之门外。
同时它是 best-effort,不是 reservation:通道可能在 probe 与分发之间被重新填满,所以在高争用下,探针可能"准入了一个在真正分发时又满了的通道"。
3.4 语义三:按真实扇出体定价
探针的成本用目标实际请求体来估算——包括由其 stream 标志导出的请求类别,与父请求路径完全一致。因此:
- fusion panel 成员(
stream: false)按它们真正会占据的非流式类别定价; - priority/RR 目标按用户实际请求的类别定价。
3.5 上报与观测缺口
- 第一个目标之后发生的探针跳过,会累计进 combo 的按请求
fallbackCount(沿用既有 fallback 语义,可在 combo 日志看到); - fusion 在所有 panel 成员都被跳过时返回 503;
- 当前快照上没有聚合计数器(例如
virtualFanoutSkipped)——文档明确指出:如果运维者报告"看不出 lane gate 跳过扇出目标有多频繁",那正是需要新增该计数器的触发点。
4. 仪表盘上看到的是哪一套?(判别指南)
运维者面对 health 快照时按以下规则快速判别:
adaptiveAdmission.laneCount/laneTenants→ 看的是 adaptive virtual lanes(System 2);adaptiveAdmission.virtualLanes === true→ 第 3 节的扇出探针也处于激活状态;- 载荷中
virtualLanes缺失或为false→ 说明OMNIROUTE_CHAT_VIRTUAL_LANES未设置——此时 字节级 lanes(System 1)仍然有效,但adaptiveAdmission下的一切(以及所有扇出 gating)尚未生效,直到开启该变量为止。
System 1 的状态则看 chatAdmission 字段:重点观察 countCapEnabled === false(确认默认部署下字节预算是实际约束)、budgetSource(预算来源)、pressureSeverity 与 shedsByReason(分布式 shed 归因)。
5. 为什么两套系统共存
- 字节级 lanes 约束的是内存密集的 parse/compress 路径——它阻止大请求体在缓冲/解析/压缩阶段放大堆内存(#4380);
- 自适应 lanes 约束的是每个租户的分发成本——它让 provider 分发阶段的并发/成本按租户自适应收敛;
- #9654 的判据 1("一个会话的突发不会让另一个会话 503")由 System 1 无条件强制、由 System 2 在 opt-in 后强制;两套系统在各自的作用域上互补,不会相互替代。
一句话总结:看到 chatAdmission 字段,那是字节级 System 1 在汇报"摄入大门";看到 adaptiveAdmission 与 virtualLanes,那是 System 2 + 扇出探针在汇报"分发大门"。运维与排障时先按此分辨,再决定调 OMNIROUTE_CHAT_MAX_INFLIGHT_BYTES 还是 OMNIROUTE_CHAT_VIRTUAL_LANES 体系下的自适应参数。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00