独立 HTTP Gateway
状态: 已被 Standalone 模式 取代 - 创建日期: 2026-08-31 - 修订: 2026-09-01
摘要
添加一个单独的、实验性 Go 推理网关,并且不要添加第二套语义编排。
第一步是把与 Envoy 无关的请求和响应编排从 pkg/extproc 提取到传输中立的 semanticruntime.Engine。现有 ExtProc 服务和新的 HTTP 网关都成为该单一引擎的适配器:
- ExtProc 适配器继续处理 Envoy protobuf、流阶段和改写响应;
- HTTP 适配器处理公开 HTTP、SSE、客户端取消和 HTTP 错误映射;
- 共享引擎处理访问、配额、插件、信号、投影、决策、算法、逻辑模型选择、Looper 和响应语义;
GatewayExecutor只拥有独立路径的物理后端执行;- 现有 Envoy + ExtProc 部署、端口、协议、默认值和外部行为保持不变。
独立网关作为自己的二进制、进程、镜像、引导和选择加入部署发布。它不调用 ExtProc gRPC,也不构造 Envoy 消息。
这不是现有二进制中的 --gateway 模式,不是对本地 ExtProc 的 HTTP 调用,也不是 ExtProc 编排的副本。
问题
Semantic Router 目前通过 Envoy ExtProc 承接公开推理流量。该部署正确地将语义模型选择与上游传输分开,但若天真地把它扩展成独立网关,会带来两个问题:
- 复用
pkg/extproc会把 Envoy protobuf、改写响应和流回调拖进公开 HTTP 服务器——传输特定类型会变成共享 API。 - 新的 HTTP 监听器调用本地 ExtProc 会保留不必要的进程协议,重复请求生命周期状态,并迫使网关重建 Envoy 的改写/流行为。
第三个问题决定架构:语义阶段顺序仍集中在 pkg/extproc 的 req_filter_*、processor_* 和 RequestContext 中。若干叶子包(protocolcodec、llmprotocol、looper)已经传输中立,随着相关进行中工作落地,更多请求路径也会中立——但今天没有完整的传输中立编排边界。只重新组装叶子包的独立网关会创建第二套编排。测试可以检测两个实现之间的漂移;它们不能消除重复。因此提取共享引擎是网关的前提,而不是后续工作。
今天还存在两条响应路径,必须在网关内统一:ExtProc 路径为插件、缓存、记忆、回放、指标和结算重建响应语义,而 pkg/looper/client.go 为 Looper 调用执行提供商响应处理。两者今天都不是完整的传输中立解码器;同时运行两者会双重解码,只运行其一可能绕过响应侧语义策略。共享响应流水线解决这个问题:提供商字节恰好解码一次,语义策略在中立对象上运行,客户端编码最后发生。专用 pkg/backendinvoker——尝试、凭证和终端证据的唯一权威——是从这些现有路径显式提取/创建的 P0 目标,而不是现有组件。
最后,放在网关前的 Service 可以负载均衡网关副本,但不能为逻辑模型已经选定的请求挑选物理后端。即使作为实验,网关也必须拥有显式物理流量层——它不能把一切转发到一个共享上游,并声称流量契约已验证。
目标
- 在一个 Go 进程中服务公开推理,无需 Envoy 或 ExtProc gRPC。
- 提取一个由 ExtProc 和 HTTP 适配器共享的传输中立 Semantic Engine。
- 复用当前分类器、算法、插件、协议编解码器和 Looper 实现;把访问、配额、凭证、出口和结算当作共享引擎上定义的 P1 目标契约,而不是现有组件。
- 严格分离语义逻辑模型选择与物理后端选择。
- 保持当前 Envoy + ExtProc 部署、生成配置、默认值、拓扑和可观测行为不变;允许内部委托给共享引擎。
- 通过同一中立协议契约支持 OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages。
- 定义正确的缓冲、流式、取消、重试、回退和终端结算行为。
- 在不宣称生产就绪的前提下,验证生产级流量控制、治理、可观测性和安全契约。
- 通过当前请求生命周期内的类型化执行器在进程内运行 Looper 模型调用,从不针对网关自己的监听器。
- 先证明 Docker 部署,再添加单独的 Kubernetes 实验。
- 保持控制面可替换,并位于同步请求路径之外。
非目标
- 不替代 vLLM、vLLM Production Stack、Kubernetes,或任何服务平台的副本生命周期。
- 不把产品 CRUD、PostgreSQL 期望状态、仪表盘或智能体状态移入网关。
- 不使网关成为持久 worker 注册表或第二套路由事实来源。
- 不为网关重复请求/响应语义编排。
- 不复制另一网关的端点名称、状态复制、CLI 标志表面或扩展 ABI。
- 没有已知为零的传输证据时,不做 HTTP 状态码重试或跨模型回退。
- 不把公开网关监听器嵌入现有 ExtProc 二进制。
- 引擎提取不得改变当前 ExtProc Looper 传输、部署拓扑或公开行为。
- 在 G0 显式决定支持级别和默认值之前,不要把网关描述为生产就绪。
- 在基准隔离出有界 Go 瓶颈之前,不要用 Rust 重写数据面。
架构决策
- 实验隔离。 网关拥有自己的二进制、监听器、就绪、生命周期、镜像、引导 schema 和选择加入部署。当前默认值不变;ExtProc 从不依赖实验网关。
- 一个共享引擎。
semanticruntime拥有传输中立编排;两个适配器都调用它。引擎不导入extproc、Envoy 或 HTTP writer。 - 两级路由。 语义运行时选择逻辑模型修订;随后网关流量层为该模型选择物理后端。
- 每个请求一个组合范围。 语义发布、BackendSnapshot、CredentialPublication、身份、截止时间、配额租约和结算从
Begin到终端状态保持固定。 - 一次中立编解码。 提供商字节解码一次为中立响应/事件;语义响应策略在该对象上运行;客户端线编码最后发生。
- 基于证据的执行。 重试、回退、用量和成本使用持久尝试证据;缺失证据意味着未知,从不意味着零。
- 显式能力失败。 当配方需要端点、编解码器、插件、流、重试或执行器能力而不可用时,发布失败——行为从不因适配器不同而悄悄跳过。
- 没有重复的语义权威。 语义发布、访问和配额身份保持其当前所有者和数据流;唯一的新控制循环管理物理 BackendSnapshot。
- 先 Docker 后 Kubernetes。 P0 在 Docker 中构建其证据;P1 添加 Kubernetes 发现/部署。
- 完成不是支持。 只有 G0 可以更改默认值或网关的生产支持状态。
目标架构
部署形态
| 形态 | 状态 | 公开传输 | 语义执行 | 物理后端流量 |
|---|---|---|---|---|
| Envoy + ExtProc | 当前默认,外部不变 | Envoy | 共享引擎 + ExtProc 适配器 | Envoy 或当前安装的传输适配器 |
| 独立网关 | 选择加入实验 | vllm-sr-gateway HTTP 服务器 | 同一共享引擎 + HTTP 适配器 | GatewayExecutor + BackendInvoker |
| 外部网关 | 可能的未来 | 外部网关 | 共享引擎的类型化适配器 | 外部网关,必须声明能力 |
组件所有权
| 组件 | 唯一职责 | 不负责 |
|---|---|---|
semanticruntime.Engine | 中立请求阶段、语义决策、逻辑模型、Looper、响应阶段、终端状态 | Envoy protobuf、HTTP writer、后端地址、连接 |
| ExtProc 适配器 | 将 Envoy 消息映射到中立输入/动作,保持当前阶段和改写契约 | 第二套语义编排、网关传输、物理后端选择 |
| HTTP 适配器 | 公开端点、可信入站元数据、HTTP 状态/头/体、SSE 刷新、客户端断开 | 语义选择、凭证查找、重试决策 |
| Dispatch 编译器 | 有序逻辑模型修订、超时/重试/回退权威、语义修订/请求身份 | 物理地址、在线健康、明文凭证 |
GatewayExecutor / ExecutionScope | 在请求开始时固定后端/凭证/截止时间、准入、物理计划组装、调用 BackendInvoker | 语义模型选择、尝试生命周期、产品期望状态 |
| 后端目 录 | 一个不可变 BackendSnapshot 修订:能力、权重、健康覆盖 | 语义发布、持久产品 CRUD |
BackendInvoker | 后端选择器、每后端许可、凭证固定、物理尝试、安全重试/回退、协议翻译、尝试日志、上游取消 | 入口点/决策计算、重新读取活动修订指针 |
ModelExecutor | 带固定范围的 Looper 子调用、每调用准入/证据、中立结果 | 公开监听器重入、调用方重新认证、第二次语义决策 |
| 响应流水线 | 中立响应/事件策略、缓存、记忆、回放、用量、终端结算 | 重新解析公开字节、控制后端选择 |
GatewayExecutor 是物理执行的唯一入口。没有与 BackendInvoker 重叠的第二个“流量管理器”:准入、队列、健康和选择器可以是小组件,但尝试生命周期、重试、回退、凭证和响应终端权威留在 BackendInvoker。
推荐技术栈
| 关注点 | 选择 | 原因 |
|---|---|---|
| 语言 | 仓库当前 Go 模块 | 直接复用协议、语义、访问、配额、凭证、出口、尝试和结算契约;无 FFI,无第二实现 |
| 入站服务器 | 带显式 http.Server 限制的标准 net/http;启用 TLS 时使用 HTTP/2 | 成熟的取消、流式、连接生命周期、pprof;在证据要求之前没有其他代理引擎 |
| 流式 | 中立事件解码器/编码器 + 带有界缓冲/反压的 SSE writer | 事件策略和客户端线输出在一个生命周期中 |
| 出站传输 | 通过按后端安全域守卫的 net/http.Transport 池的后端调用器 | 保留已知为零、凭证固定、TLS/mTLS、SSRF 和取消契约 |
| 配置 | yaml.v3 严格解码、规范校验、脱敏有效配置、不可变编译结构 | 未知字段在监听器启动前失败;匹配现有配置习惯 |
| 并发 | context、errgroup、组件监督器、类型化租约、用于不可变快照的 atomic.Pointer | 显式取消、失败策略、排空和修订交换 |
| CLI | 小型标准库 flag.FlagSet 子命令,可由 Python 产品 CLI 包装 | 小型实验表面,没有新 CLI 框架 |
| 指标 | 现有 Prometheus 客户端 | 复用注册表和命名约定 |
| 追踪 | 现有 OpenTelemetry SDK + W3C 传播 | 跨适配器、语义、Looper 和上游的跨度 |
| 日志 | 现有带强制脱敏的结构化 Zap 门面 | 一条隐私规则,没有新日志栈 |
| 测试 | Go 单元/竞态/模糊、假时钟/RNG、httptest、Docker E2E,然后 Kubernetes E2E | 从纯契约到取消、流、抖动和部署行为 |
核心网关不导入 Traefik,也不继承提供商依赖表面。Kubernetes 客户端依赖仅在 P1 到达,并隔离在候选源适配器内。
共享 Semantic Engine
引擎与会话
引擎暴 露请求范围会话,并且不导入或暴露 ExtProc processor 方法。两个适配器都调用该 API;具体 Go 类型可能演进,但形态保持:
type Engine interface {
Begin(context.Context, Ingress) (*Session, error)
}
type Session interface {
Prepare(context.Context) (Action, error)
ResponseProcessor() responsepipeline.Processor
Abort(context.Context, error) error
Close() error
}
Ingress 只携带有界可信传输元数据、源线格式、不透明已认证身份(原始凭证在适配器、Begin 之前消费)、请求字节或已解码中立请求,以及适配器绑定的请求范围 ModelExecutor 能力。它从不携带 Envoy 类型、HTTP writer、后端地址或提供商密钥。
Action 是封闭联合:
ImmediateAction— 认证拒绝、配额拒绝、缓存命中、快速响应、协议错误或已完成编排;DispatchAction— 已改写中立请求、源信封和已校验逻辑DispatchPlan。
Looper 不是第三种适配器动作。引擎通过会话绑定的 ModelExecutor 执行有界多次调用,然后返回普通动作,因此阶段顺序从不沉入适配器。
会话拥有当前混入 RequestContext 的请求范围状态,在内部拆成协议状态、语义状态、访问/分派状态和响应状态。适配器只看到动作、元数据和终端方法。
配置生成解析
每个请求按此顺序固定其组合执行范围:
- 适配器消费调用方凭证,并产生不透明已认证身份。
- 独立 HTTP 组合创建
GatewayExecutionScope,固定 BackendSnapshot、CredentialPublication、绝对截止时间和请求身份;ExtProc 适配器改为绑定现有受控 HTTP ModelExecutor。 Engine.Begin校验活动语义发布身份。- 捕获精确命名空间、配额分区、发布世代/修订和路由摘要。
- 会话只携带不透明认证结果、不可变语义范围和中立 ModelExecutor 句柄。
- 每个范围租约保持到终端结算和响应体关闭。
基于文件的路由通过同一会话 API 固定启动发布。语义发布或 BackendSnapshot 热重载只影响新请求——从不影响进行中会话、重试、回退或 Looper 子调用。
逻辑计划与物理计划
传输中立的 dispatchplan 包定义包含以下内容的逻辑计划:
- 语义发布和请求身份;
- 所选决策身份;
- 有序逻辑模型修订和路由键;
- 源/后端线格式;
- 一个整请求/流超时;
- 有界同模型重试权威;
- 有界优先级回退权威;
- 所需尝试/终端证据。
逻辑计划从不包含后端地址、提供商密钥、集群名称或在线健康值。
gatewaycontract 仅在真实适配器边界处理逻辑计划的序列化/签名/回放保护。实验期间现有 ExtProc 编码不变;独立网关在内存中传递已校验计划——从不把自己的计划 Base64 编码进头、再移除并再次解码。
独立 GatewayExecutor 将逻辑计划与固定的 BackendSnapshot 和 CredentialPublication 组合成物理计划链,从不跟随更新的活动指针。
准入、健康和选择器只缩小候选集;它们不能在发布之外添加模型或后端。BackendInvoker 仍是尝试、重试、回退、凭证和终端证据的唯一权威。
响应流水线
两个适配器使用一条中立响应流水线:
provider bytes
-> provider codec decoder
-> neutral Response / Event
-> semantic response processor
-> client codec encoder
-> public bytes
会话实 现缓冲、事件、终端和中止钩子。ExtProc 适配器将 Envoy 响应阶段映射到这些钩子;独立路径从 BackendInvoker 直接调用它们。缓冲改写在客户端编码前完成;流式事件改写在每个事件编码前运行;终端结算消费同一编解码引擎的终端,从不重新解析公开字节。
响应能力显式分类:
| 类别 | 含义 |
|---|---|
request_safe | 在分派前运行;独立于响应交付 |
stream_event_safe | 在客户端可见之前观察或改写一个中立事件 |
terminal_observer | 完成后的账务、缓存、记忆、回放或遥测;不能收回已发送字节 |
buffer_required | 必须看到完整响应;流式配方必须拒绝发布,或显式选择缓冲交付 |
仅缓冲的幻觉/越狱改写不得描述为流式保护,除非存在已审阅的事件安全实现。
包布局与依赖规则
下面的布局是目标设计。尚不存在的包由需要它们的 P0 任务创建;此处没有在 pkg/extproc 逐步委托之外重命名或移动现有包。
| 包 | 职责方向 |
|---|---|
pkg/semanticruntime | 引擎、会话、中立请求阶段、语义响应阶段、每世代运行时隔离 |
pkg/dispatchplan | 逻辑计划类型、校验、请求摘要、编译器、适配器能力要求 |
pkg/gatewaycontract | 跨进程编码、签名、回放保护、头/过滤器状态限制 |
pkg/responsepipeline | 语义运行时和 BackendInvoker 共享的最小中立响应/事件契约 |
pkg/gatewayserver | 公开/管理 net/http 监听器、端点适配器、SSE writer、就绪、排空 |
pkg/gatewayexecutor | 独立准入、固定执行范围、物理计划组装、BackendInvoker 调用 |
pkg/backenddirectory | 不可变 BackendSnapshot、BackendSource、能力索引、健康覆盖 |
pkg/trafficcontrol | 可组合准入、队列、选择器、主动/被动健康、熔断、负载反馈;不拥有尝试 |
pkg/backendinvoker | 尝试生命周期、凭证固定、安全重试/回退、编解码、日志、取消(作为 P0 的一部分从当前调用路径提取) |
pkg/backendegress | 允许列表、DNS 固定、SSRF 保护、TLS 传输、重定向策略(与调用器一并提取) |
pkg/looper | 出站端口支持注入的 HTTP 或进程内 ModelExecutor;算法保持中立 |
pkg/extproc | 保持 Envoy 适配器和改写契约;语义编排逐步委托给 semanticruntime |
cmd/gateway | 仅实验进程的组件组装和启动 |
依赖规则:
semanticruntime不导入extproc、gatewayserver、Envoy 或后端传输。gatewayserver可以导入semanticruntime;反向禁止。extproc可以导入semanticruntime和responsepipeline;从不导入gatewayserver、gatewayexecutor或trafficcontrol。- 网关包不导入
extproc或 Envoy。 dispatchplan不导入适配器或后端实现。- BackendInvoker 只消费中立响应钩子;它从不接收 ExtProc 会话。
- 公开适配器不解析 ProviderCredential,也不构造后端地址。
gatewayexecutor组装物理计划,但将尝试生命周期委托给 BackendInvoker。- 进程组装留在
cmd/gateway;共享工厂保持传输中立。
这些规则由依赖测试强制,而不是审阅警惕。
配置与 BackendSnapshot 控制循环
静态与动态配置
引导拥有进程级内容:监听器、TLS、BackendSource、流量安全上限、可观测性导出器、管理策略和关闭超时。现有运行时发布继续拥有租户、入口点、配方、模型、凭证引用、配额和语义策略。CLI 只覆盖少数运营参数。
type BackendSnapshotCandidate struct {
Source string
Revision string
ObservedAt time.Time
Backends []BackendDefinition
}
type BackendSource interface {
Run(context.Context, chan<- BackendSnapshotCandidate) error
}
BackendSources 发送完整快照;不允许对活动映射做增量改写。P0 支持严格文件/静态源和受守卫 DNS;Kubernetes 在 P1 到达。控制循环保留每个源的最新候选,执行有界最新获胜合并,并记录丢弃、过期和重连。
编译、预热与激活
每个 BackendSnapshot 候选按顺序通过:
- schema 校验和规范归一化;
- 确定性后端身份、引用解析、冲突检测;
- 能力、路由键、凭证绑定、安全域和协议兼容性检查;
- 编译后端目录、选择器输入和流量安全策略;
- DNS 固定和必要连接预热;
- 规范摘要计算(相同候选被跳过);
- 构造不可变
BackendSnapshot; - 原子切换活动指针并记录期望/已应用状态;
- 旧快照的请求租约排空到零后关闭它。
失败的必需后端从不激活部分快照;保留带结构化原因的 last-known-good。源丢失不会清空活动快照;就绪按过期策略降级, 同时已验证快照继续服务。
语义发布复制、激活和状态保持其当前所有者和数据流。新控制循环从不编译配方、信号、插件或租户策略。
组件监督器
每个长生命周期组件声明失败策略:
fatal:监听器、语义发布副本、BackendSnapshot 控制器——进程按顺序退出;restartable:退避/抖动重启,预算耗尽时升级到降级/致命;degraded:安全服务继续,带显式就绪/诊断原因。
恐慌恢复从不只记录日志。每个 goroutine 都有所有者、上下文和关闭顺序。
请求生命周期
缓冲请求与响应
- HTTP 适配器建立请求 ID、截止时间和取消,并校验方法、内容类型和头/体限制。
- 认证器消费调用方凭证并产生不透明身份;原始 bearer 从不进入会话。
- HTTP 组合创建
GatewayExecutionScope,固定 BackendSnapshot、CredentialPublication 和绝对截止时间。 Engine.Begin固定语义发布;中立编解码器恰好解码请求一次。- 引擎运行请求插件、信号、投影、决 策、算法、访问/配额和逻辑模型选择。
- 缓存命中、策略拒绝和快速响应产生
ImmediateAction,由 HTTP 适配器直接映射。 DispatchAction进入同一GatewayExecutionScope;GatewayExecutor 取得请求级准入/队列租约,并将逻辑计划与固定快照组合成物理PlanChain。- BackendInvoker 通过 PlanChain 选择器挑选后端,取得每后端尝试租约,解析凭证/安全域,并拥有物理尝试、安全重试/回退和日志/证据。
- 提供商响应恰好解码一次,经过会话的中立响应流水线,并编码成客户端格式。
- 一条终端路径完成用量/成本/配额/回放/缓存/记忆/指标结算,并释放每个租约。
ExtProc 路径运行相同的步骤 4–6,将动作映射回现有 Envoy 改写契约;Envoy 继续拥有上游传输。它不创建 GatewayExecutionScope,也不导入网关包。
流式响应
- 仅在上游成功且客户端状态确认后才提交头。
- 每个提供商事件先成为中立事件,运行
stream_event_safe策略,然后编码并刷新。 - SSE writer 使用有界缓冲,并尊重下游反压和客户端取消。
- 一旦发送响应头或客户端可见字节,禁止可能双重计费的重试。
[DONE]、提供商终端事件、EOF、协议错误、超时和断开都映射到一个终端。terminal_observer插件在交付结果已知后运行;无法证明的用量记录为未知,并保持配额围栏。buffer_required配方 在激活后从不悄悄把流式切换为缓冲。
Looper 多模型调用
网关 Looper 从不自调用,也从不重入公开或管理监听器:
parent session
-> Looper algorithm
-> GatewayModelExecutor
-> pinned GatewayExecutionScope
-> GatewayExecutor
-> BackendInvoker
-> neutral child result
-> Looper algorithm
-> parent response processor
GatewayModelExecutor 继承父请求的语义发布、BackendSnapshot、CredentialPublication、身份、允许模型集、截止时间、取消、配额分区和账务范围。每个子调用有自己的分派/尝试 ID、准入和证据。外层请求插件运行一次;声明为 per_model_call 的插件每调用运行;最终响应插件运行一次。
必须配置最大调用次数、并行度、递归深度、token、成本和时间预算;耗尽任一预算则失败即关闭。P0 只支持有界缓冲子调用;需要不支持的流式编排的配方在激活时失败。ExtProc 组合保持现有受控 HTTP 执行器;网关组合绑定 GatewayModelExecutor。两者使用同一引擎阶段顺序;ExtProc 的外部 Looper 行为不变。
中止与终端状态
客户端断开、队列超时、上游超时、插件拒绝、恐慌、排空和进程关闭都调用一条幂等终端路径。终端释放语义发布、BackendSnapshot、配额、队列、后端、凭证、体和流租约;记录尝试/用量证据;并保证恰好一次结算。
重试与回退不变量
- 每个请求一个绝对截止时间;重试/回退从不刷新预算。
- 只有
known-zero证据——证明请求从未被后端接受或计费——才授权重试。 - 请求体写入、收到响应头或发送客户端可见字节之后,默认禁止重试。
- 同模型重试只在同一模型的合格后端中选择,并在逻辑计划的权威和重试预算内。
- 跨模型回退只沿发布中固定的优先级层前进;候选集从不动态增长。
- 超时和 连接重置默认是未知结果;它们从不被假定为零用量。
- 每次尝试有唯一 ID、开始/结束时间、所选后端、证据和终端分类。
- 结算对成功、失败、取消、部分流和未知用量恰好执行一次。
流量控制
后端目录
目录是一个 BackendSnapshot 修订的不可变后端集。每个后端携带逻辑模型绑定、协议能力、地址引用、安全域、凭证引用、权重、区域和静态元数据。在线健康/负载/熔断状态是单独的原子覆盖,只能把候选标记为不合格——它从不修改语义发布或快照。
后端选择器
- P0:随机、轮询、加权轮询、确定性亲和,以及二选一(对活跃请求 + EWMA 延迟/负载的 P2C,每个信号带新鲜度和回退)。
- P1:一致性/前缀/有界负载哈希、可信粘性键、缓存感知调度、异常剔除、自适应并发。
- 每项策略都按能力门控;拓扑变更、空候选集、过期信号和极端权重有确定性测试。