前言
前面完成了多模态图文 RAG 的 C++ 接口封装,我们已经能在后端服务里调用 LLM、Embedding、CLIP 多模态模型。当项目从小 demo 走向生产环境,当存在多个模型实例、多个业务方同时调用模型服务的时候,不能让业务客户端直接连接底层推理服务。
底层推理服务(如 llama.cpp、vLLM、text-generation-webui)的核心职责只有一件:GPU/CPU 上跑模型推理。鉴权、计费、限流、路由、熔断、日志审计、请求重试、流式转发这些通用流量治理逻辑,如果全部塞进推理服务内部,会极大增加模型服务代码复杂度,还会引入很多不稳定因素。
LLM 服务网关,就是 AI 集群的统一流量入口。本质是面向大模型场景的专用 API 网关,承接所有客户端请求,做前置校验和流量调度,再转发到后端各个模型实例;同时把模型返回结果(尤其是 SSE 流式输出)透传给客户端。
普通 Web 网关 Nginx、OpenResty 可以做基础反向代理,但原生缺少 LLM 场景专属能力:Token 计量、长任务并发控制、FunctionCall 透传、模型灰度路由、额度扣减。自研 C++ 网关就可以补齐这些定制化能力,也是面试里非常加分的项目亮点。
本篇会从业务痛点、整体架构、模块拆解、算法选型、完整可扩展 C++ 代码、性能压测指标、线上故障案例、面试深挖问题完整展开。
一、LLM 网关业务痛点与架构总览
1.1 直连模型服务的一系列工程问题
- 多实例多模型管理混乱:同时部署 7B、13B、Embedding、CLIP 多模态模型,每个模型多实例扩容,客户端需要维护大量后端地址,模型实例上下线需要业务方改配置,无法无感扩容缩容。
- 算力资源无法管控:任何人拿到接口地址就可以无限调用,长文本 prompt 大量消耗 GPU 显存,没有配额、鉴权,算力资源容易被耗尽。
- 缺少熔断保护,故障雪崩:某一个推理节点 OOM、GPU 显存溢出报错,请求继续往故障节点转发,大量请求超时堆积,拖垮整个业务。
- 无法做灰度与版本管理:模型迭代升级,新旧版本模型需要并行验证,直连方式很难按比例切流做灰度测试。
- 流式 SSE 转发困难:LLM 主流采用 SSE 长流式输出,普通反向代理容易出现超时截断、chunk 粘包,同时很难实时统计流式输出的 token 消耗。
- 缺少全链路观测:调用耗时、输入输出 token、错误码、用户调用量无法统一采集,无法做计费、告警、故障排查。
1.2 LLM 网关分层架构
整体分为四层:
- 接入层:异步网络 IO 服务,接收 HTTP/HTTPS 请求,解析请求头、请求体,参数合法性校验,限制请求体最大长度,过滤非法请求。
- 安全与计量层:API Key 鉴权、用户身份解析、用户 Token 额度校验、预扣额度、调用日志落库、全链路 TraceID 注入。
- 流量调度层:模型路由、灰度权重分发、后端节点负载均衡、健康检查、限流排队、熔断降级、请求优先级调度。
- 代理转发层:异步请求转发、SSE 流式 chunk 透传、后端结果解析、token 统计、失败自动重试、错误封装返回。
后端集群:LLM 推理实例、Embedding 服务、多模态 CLIP 服务,全部对客户端透明,只和网关通信。
1.3 网关存储依赖
- Redis:存储 API Key 信息、用户剩余 token 配额、后端节点健康状态、限流计数器;
- MySQL:持久化调用记录、账单、用户信息;
- 日志系统:全链路日志、访问日志、异常日志。
二、网关核心模块深度原理
2.1 API Key 鉴权 + Token 配额管控
鉴权流程
客户端请求 Header 携带Authorization: Bearer xxx-api-key。网关收到请求,提取 ApiKey,查询 Redis 缓存:Key 是否存在、是否启用、所属用户 ID。缓存失效时回源 MySQL 查询。非法 key 直接返回 401 拒绝访问。
配额管理(LLM 场景核心)
传统接口按请求次数计费,大模型场景算力消耗由输入 token + 输出 token 总量决定,必须按 token 做额度管控。 流程分为预扣 + 结算两步:
- 请求到达网关,解析 prompt,调用 token 计算器预估输入 token 数量,预扣用户额度;额度不足直接返回 429,拒绝调用;
- 模型推理完成(流式输出需要等待流结束),拿到模型返回真实输入 + 输出 token,做额度修正;
- 如果客户端中途断开流式连接,模型中断生成,要做额度回滚,避免扣取未生成 token 的费用。
坑点:token 预估存在误差,预估大于实际消耗,需要异步补偿返还差额;预估偏小,可能出现超额度调用。工程上预留一定的缓冲额度。
2.2 多模型路由与灰度发布
请求体中携带model字段,网关路由表匹配模型名称,转发到对应的后端实例集群。 路由支持三种策略:
- 固定路由:模型名称绑定固定后端集群,例如
llm-7b路由到 7B 推理集群,embedding-bge路由向量模型; - 版本路由:model 字段携带版本,
llm-7b-v1、llm-7b-v2,新旧模型集群隔离; - 权重灰度路由:同一个模型名称配置新旧两组节点,按流量百分比分配。例如 90% 流量到老版本,10% 切到新版本,持续观测错误率、token 生成速度,无异常再逐步放大流量。
灰度观测指标:请求错误率、首 token 耗时 TTFT、单 token 生成速度、GPU 显存使用率。一旦指标恶化,网关自动切回全量老版本,无需人工改配置。
2.3 负载均衡 + 后端节点健康检查
同一个模型集群会部署多个推理实例,横向扩容分担压力。 负载均衡可选策略:
- 轮询:简单平均分发,适合请求算力消耗差距不大场景;
- 最小连接数:优先把请求分配给当前并发任务更少的后端节点,LLM 长推理任务首选策略。
主动健康探测:网关后台起定时器,周期性向后端推理节点发送健康探测请求(/health 接口)。探测不仅检查端口连通性,还要验证模型是否加载完成、GPU 是否正常。
- 连续 N 次探测失败,节点标记为不健康,路由时直接跳过,不再分发流量;
- 恢复后,连续多次探测成功,重新加入可用节点列表。
只探测端口是典型线上大坑:端口能通,不代表模型加载完成,GPU 可能 OOM 卡死,但是端口依旧存活。
2.4 LLM 专属限流、排队、熔断降级
普通 Web 接口用 QPS 限流,但是 LLM 推理是长耗时任务,单次请求可能持续几秒甚至几十秒,QPS 参考意义很小。
LLM 网关限流核心指标:后端并发推理任务数,控制同一时间在 GPU 上运行的请求总数,保护显存不被打满。
- 请求排队队列:并发达到上限,新请求进入内存等待队列;配置队列最大长度,队列满直接返回 429;每个请求设置排队超时,超时直接失败,避免请求无限堆积。
- 请求优先级:支持业务优先级,高优先级业务请求优先出队,抢占排队位置,保障核心业务。
- 熔断机制:统计每个后端节点错误率,单位窗口内错误比例超过阈值,触发熔断,冷却窗口期内不再转发流量,等待节点恢复。
- 降级策略:集群过载时,可以自动降级:切换到轻量化备用模型、或者直接返回预设提示信息,保护核心业务不发生雪崩。
2.5 SSE 流式透传(LLM 网关最特殊模块)
普通 HTTP 代理是等待后端完整接收 response,再返回客户端。LLM 流式 SSE 不可以这样做。 模型服务会持续返回data: {...}的 chunk 片段,网关需要收到一段,立刻转发一段,一边透传 chunk,一边实时累加统计输出 token。
难点:
- 长连接保活,设置心跳包,防止中间网络代理断开长连接;
- 客户端中途断开连接,网关需要立刻通知后端模型中断推理,释放 GPU 资源,避免无效占用显存;
- chunk 存在粘包、分包,网关需要正确分割 SSE 数据流,不能把多条消息合并。
三、C++ 高性能网关完整核心代码(基于 Boost.Asio 异步 IO)
说明:下面是可扩展工程骨架,生产环境可基于此继续完善鉴权、Redis 交互、健康检查定时器、埋点日志。
#include <iostream> #include <string> #include <vector> #include <map> #include <mutex> #include <queue> #include <boost/asio.hpp> #include <boost/asio/ssl.hpp> using namespace boost::asio; using ip::tcp; // 后端模型节点元信息 struct ModelNode { std::string host; int port; bool healthy; int current_conn; // 当前并发任务数,最小连接数负载均衡使用 std::mutex mtx; }; // 模型路由表:模型名称 -> 后端节点列表 std::map<std::string, std::vector<ModelNode>> model_route_table; std::mutex route_mtx; // 全局排队任务队列,简单实现请求排队 struct Task { std::string model_name; std::string request_body; }; std::queue<Task> task_queue; std::mutex queue_mtx; const int MAX_QUEUE_SIZE = 200; // 最小连接数负载均衡,选择健康、当前并发最少的节点 ModelNode* selectMinConnNode(const std::string& model_name) { std::lock_guard<std::mutex> lk(route_mtx); auto it = model_route_table.find(model_name); if (it == model_route_table.end()) return nullptr; auto& node_list = it->second; ModelNode* target = nullptr; int min_conn = INT_MAX; for(auto &node : node_list) { if(node.healthy && node.current_conn < min_conn) { min_conn = node.current_conn; target = &node; } } return target; } // 异步转发SSE流式请求,透传chunk,统计token void forwardSSE(io_context& io, const std::string& model, const std::string& body) { ModelNode* node = selectMinConnNode(model); if(node == nullptr) { std::cout << "[Gateway] 无可用健康后端节点, model:" << model << std::endl; return; } // 增加节点并发计数 { std::lock_guard<std::mutex> lk(node->mtx); node->current_conn ++; } std::cout << "[Gateway] 转发请求到 " << node->host << ":" << node->port << std::endl; // 异步http连接,SSE chunk流式透传逻辑 // 收到后端返回data chunk,直接转发给客户端,解析chunk内token增量,累加统计 // 连接结束/断开时,减少节点并发计数 // node->current_conn --; } // 任务消费协程,消费排队队列 void taskWorker(io_context& io) { while(true) { Task task; { std::lock_guard<std::mutex> lk(queue_mtx); if(!task_queue.empty()) { task = task_queue.front(); task_queue.pop(); } else { break; } } forwardSSE(io, task.model_name, task.request_body); } } // 请求入队接口 bool enqueueRequest(const std::string& model, const std::string& body) { std::lock_guard<std::mutex> lk(queue_mtx); if(task_queue.size() >= MAX_QUEUE_SIZE) { return false; //队列已满,返回429 } task_queue.push({model, body}); return true; } int main() { io_context io; // 初始化路由表,部署两组llm7b实例、一组embedding实例 { std::lock_guard<std::mutex> lk(route_mtx); model_route_table["llm-7b"] = { {"127.0.0.1",8081, true, 0}, {"127.0.0.1",8082, true, 0} }; model_route_table["embedding-bge"] = {{"127.0.0.1",8090, true,0}}; } std::cout << "C++ LLM Gateway Started, listen on 8000" << std::endl; // 启动http监听、任务消费循环 return 0; }代码扩展方向:
- 接入 hiredis,读取 API Key、用户额度;
- 增加定时器,周期性执行后端健康探测;
- 增加全链路 TraceID,日志携带 trace_id;
- 增加 token 预估、预扣额度逻辑;
- SSE 分包解析,实时解析返回的 token 数量;
- 请求超时定时器,清理长时间未返回的请求。
四、线上工程踩坑全集
- SSE 长连接超时:LLM 生成慢,默认 HTTP 代理超时断开。网关需要单独配置长读写超时,并且定时发送心跳注释包,维持连接。
- 并发计数竞态:多线程并发修改节点 current_conn,不加锁会导致并发统计错乱,限流失效,GPU 过载。
- token 统计不一致:流式输出客户端中途断开,模型停止生成,如果网关没有回滚预扣额度,会造成用户多扣费。
- 健康检查只检测端口:GPU OOM,进程存活但推理卡死,端口正常,网关依旧分发请求,大量请求全部超时。健康探测要调用模型真实推理接口。
- 请求体无上限:超长 prompt,几 MB 的请求包,不做限制,会造成网关内存持续暴涨,内存溢出。
- 缺少全链路 Trace:网关、模型服务日志没有统一 trace_id,出现超时、报错,无法追踪请求流转,排查困难。
- 排队队列无限堆积:没有最大队列长度和排队超时,流量洪峰时,请求全部堆积,所有请求大面积超时。
五、可观测指标(网关监控大盘必备)
- 网关 QPS、成功 / 失败请求数;
- TTFT(首 token 生成耗时)、总推理耗时;
- 输入 token 总量、输出 token 总量;
- 排队等待时长、队列积压请求数量;
- 每个后端实例并发连接数、错误率;
- 401 鉴权失败、429 限流拒绝请求数量;
- 熔断触发次数、降级触发次数。
六、面试高频深挖问答
Q1:自研 C++ LLM 网关,相比 Nginx 反向代理优势在哪?A:Nginx 适合通用 HTTP 流量转发,对 LLM 场景定制能力弱。自研 C++ 网关可以深度集成 token 预估、额度扣减、按并发任务限流、SSE 的 token 实时统计、模型灰度权重、请求优先级调度。私有化项目中,可以减少中间组件依赖,全链路可控,方便接入业务计费逻辑。缺点是需要自行开发维护,开发成本更高。
Q2:什么是 TTFT?网关层面可以做哪些优化降低 TTFT?A:TTFT 是首 token 输出耗时,代表用户看到第一个文字的等待时间。网关侧优化:减少排队等待、健康节点精准路由、请求优先级调度,把高优先级请求优先调度;减少网关内部拷贝开销,异步 IO 降低转发延迟。TTFT 瓶颈大多在 GPU 推理侧。
Q3:用户客户端断开 SSE 连接,网关要做什么操作?A:网关立刻向后端推理服务发送中断请求,终止本次生成,释放 GPU 显存;回滚用户预扣的 token 额度;日志记录用户主动断开事件,上报监控。如果不中断,GPU 会继续生成无用 token,持续占用显存。
Q4:熔断和限流的区别?A:限流是预防,防止大量请求打满后端资源,在流量进入时做拦截;熔断是故障容错,后端节点已经大量报错,网关主动停止向故障节点转发流量,等节点恢复,属于故障发生后的保护手段。
Q5:网关做 token 预扣,如果预估 token 大于实际消耗,怎么处理?A:推理结束拿到真实 token 消耗,计算差额,异步返还多余额度到用户账户,后台任务补偿,同时日志记录差额,用于后续优化 token 预估模型准确度。
七、总结
- LLM 网关是 AI 服务生产化的核心组件,核心能力不是简单反向代理,而是面向大模型长推理任务的流量治理、权限计量、多实例调度。
- LLM 场景不能照搬传统 Web 的 QPS 限流,优先控制后端并发推理任务,搭配请求排队、熔断降级保护 GPU 算力。
- SSE 流式透传是 LLM 网关核心难点,不仅要转发数据流,还要处理连接中断、token 统计、资源释放。
- C++ 异步 IO 适合自研私有化轻量网关,可深度定制业务逻辑,是简历上体现架构落地能力的亮点。
- 网关层必须配套完善监控指标,TTFT、token 消耗、队列积压、错误率是线上运维最核心观测指标。