
MCP 网关这两年几乎是 AI 工程化绕不开的话题。我这边一直在做 ContextForge 和 Peta 两个组件的集成实验前者负责上下文聚合与压缩后者负责轻量路由和快速转发组合起来就是一个完整的 MCP 网关骨架。做下来最深的感触就是性能和简洁性之间不存在既要又要的银弹只存在根据业务场景做取舍的平衡术。这篇内容就从这两个组件出发把我在实际搭建网关过程中踩过的坑、调过的参数、推翻过的设计完整梳理一遍。1. 先从 MCP 网关说起为什么会有性能与简洁性的矛盾1.1 网关的定位和隐形层哲学MCPModel Context Protocol本质上解决的是大模型与外部工具、数据源之间的标准化通信问题。网关夹在中间职责很明确接收客户端的请求按规则路由到对应的 MCP Server把结果聚合后返回。理想状态下这一层应该隐形——用户感知不到它的存在只看到工具调用顺畅、响应及时。但现实是网关一旦承担了额外职责鉴权、限流、上下文管理、协议转换、日志审计它就从隐形层变成了显性的瓶颈层。我在项目里做过一次粗略统计一个包含 12 个工具调用的复杂请求如果不经过网关直连P95 延迟约 2.8 秒经过完整网关链路后P95 飙到了 6 秒以上。多出来的三秒多就是网关内部各个环节的累计开销。这也是为什么我在设计之初就把网关拆成两段ContextForge 负责重活Peta 负责轻活。这样做的首要目的不是性能而是隔离变化——上下文管理策略会频繁迭代而路由转发逻辑相对稳定拆开之后两边可以各自独立演进互不拖累。1.2 ContextForge 与 Peta 的初始职责划分ContextForge上下文存储器、Token 预算控制器、上下文压缩器、历史摘要生成器。它的存在是为了解决模型能接收的上下文有限但工具对话历史无限增长的矛盾。Peta连接管理、路由匹配、请求转发、响应拦截、超时控制。它本身不感知上下文语义只负责把请求快速送到正确的 MCP Server 上再原样把结果搬回来。这种拆分的隐含假设是上下文处理是需要思考的重操作链路可以长一点没关系而路由转发是纯 IO 操作必须尽量短。这个假设在理论上成立但在落地时你会发现两者在真实流量下会产生强烈的资源竞争——ContextForge 的重上下文压缩计算会抢占 CPU直接把 Peta 的转发延迟拖高。后面我会细讲这个问题。提示很多第一次做 MCP 网关的人会把性能优化理解成调超时时间、加大并发数但真正的瓶颈往往在架构层面——两个职责不同的组件抢同一份资源才是最隐蔽的性能杀手。2. 性能瓶颈到底在哪拆解 ContextForge 的重型操作2.1 上下文窗口管理与 Token 预算ContextForge 在 v1 版本里用的是滑动窗口 预算硬控策略。每次请求进来它会计算当前会话累计的 token 数如果超过预算就触发压缩流程。压缩流程内部做了三件事从最老的对话轮次开始裁剪、对裁剪的内容做摘要生成、把摘要回填进上下文。这个逻辑听起来合理但实际执行时开销极大。我做过一组对比实验操作类型平均耗时触发条件纯转发Peta 直通45ms上下文未超预算携带上下文ContextForge 轻校验120ms正常去重与元信息注入上下文压缩首次触发3.2s超过预算 20%上下文压缩连续触发5.8s连续多轮超预算问题出在连续触发上。如果用户连续几轮都在大量追加内容每轮都会触发一次压缩而压缩产生的摘要又要参与下一轮的 token 计算。这导致压缩操作产生了两倍开销一次是压缩本身的计算另一次是压缩结果的重新计算入账。2.2 工具调用的并行度与优先级第二个瓶颈是工具调用的并行调度。ContextForge 的定位是上下文管家于是我把工具调用的优先级排序也塞给了它。每轮请求进来它会先解析所有工具调用的依赖关系哪些工具的输出会作为另一个工具的输入然后按依赖拓扑排序构建一个执行 DAG。DAG 构建本身不慢慢的是它需要的元数据——每个 MCP Server 必须上报自己的输入输出 schema。而这些 schema 往往很大动辄几十 KBContextForge 要把它们加载进内存做笛卡尔积级别的依赖推断。我在一次压测中发现schema 加载耗时占了 ContextForge 总耗时的 41%。后来我的结论是工具调用的调度不该和上下文管理混在一起。并行调度是实时性敏感的操作而上下文管理是状态性操作两者应该走不同的处理路径。2.3 连接复用与会话保持还有一个容易被忽略的细节连接的复用。ContextForge 为了保持会话状态需要在内存里维护大量连接上下文connection session每个 session 持有目标 Server 的鉴权 token、会话偏好设置、历史摘要索引。这些 session 默认不过期除非显式调用清理接口。这就带来了两个问题。第一内存占用随会话增长无限膨胀——我跑了一个 200 个并发会话的压测ContextForge 的 RSS 从 480MB 涨到了 2.1GB。第二session 的查找和刷新机制每次都要加锁锁竞争在高并发下成了隐性瓶颈。实际上在标准的 MCP 网关场景里大部分请求并不需要保持状态——只有真正需要多轮记忆的会话才值得让 ContextForge 参与。无状态请求完全可以走 Peta 的轻量通道直接转发。3. Peta 的简洁之道用减法换性能的实践3.1 零依赖与精简中间层Peta 的设计原则和 ContextForge 完全相反能不用框架就不用框架能不加中间件就不加。它内部不依赖任何 Web 框架只用标准库的 net/http 包加一层自定义的轻量路由映射。整个路由表是一个 map[string]HandlerFunc键是路径模式值是处理函数。这样设计带来的性能收益很直观# 本机基准测试wrk 单线程 # Peta 直连转发不做任何额外处理 wrk -t4 -c200 -d30s http://localhost:7843/rpc/ping Running 30s test http://localhost:7843/rpc/ping 16 threads and 200 connections Requests/sec: 12,847.36 Transfer/sec: 3.38MB Latency Distribution 50% 2.89ms 75% 3.41ms 95% 6.72ms 99% 18.42ms这个数据说明一个很硬核的结论在网关层面最能提升性能的方式不是加缓存、加并发而是砍代码。每多一层中间件就多一次内存拷贝和函数调用积少成多就会量变引起质变。3.2 快速失败Peta 的校验策略Peta 的另一个设计取向是快速失败。它只做路由校验和基础格式校验不做深度的 schema 校验。这里我踩过一个坑。最初我让 Peta 直接使用 MCP Server 上报的 JSON Schema对每个请求体做完整校验。结果发现 schema 校验的平均耗时是 8ms/次虽然单看不多但 Peta 的目标是整体链路 P95 100ms8ms 的占比就非常可观了。后来我把校验逻辑改成了分级策略必查项请求体是否是合法 JSON、是否包含 method 字段、id 是否为字符串或数字。这三项可以在 0.1ms 内完成。可选查项params 结构是否匹配目标工具的入参 schema。这项默认关闭只在调试模式下开启。不查项返回数据的结构完整性。这个交给客户端自己去验证。这种做法本质上是在提前校验和信任边界之间做选择。网关不是业务系统不需要对数据质量负责它只需要保证请求格式能正确路由到目标服务即可。3.3 几个关键配置的取舍Peta 的配置项少得可怜我在 README 里专门写了一句Less config, more predictable。实际保留的配置项及取舍逻辑如下# peta/config.yaml server: port: 7843 max_conns: 10000 idle_timeout: 30s keepalive: true route: load_balancer: least_conn failover: true gateway: max_payload_size: 1048576 # 1MB默认拒绝更大请求 read_timeout: 5s write_timeout: 30s关于超时read_timeout 和 write_timeout 我分别给了 5s 和 30s。原因是读请求应该快客户端在等响应写响应可以慢下游 MCP Server 可能处理很久。但如果把两者设为同一个值就会出现误杀——下游 Server 正常处理中因为 read_timeout 到期被强制断开这种事我遇到不止一次。4. 权衡的实操过程把性能预算花在刀刃上4.1 性能基线与压力测试方法在动手调整架构之前我先把性能基线跑了出来。这很重要——没有基线所有优化都是自说自话。我用的压测方案很简单工具wrk 或 hey前者适合测吞吐后者适合测延迟分布场景模拟一个 MCP 会话内连续 10 轮工具调用每轮携带 2KB 左右的上下文指标P95 延迟、P99 延迟、错误率、CPU 占用、内存占用重点记录的是一个复合指标每百次成功请求需要多少次重试。这个指标直接反映网关的稳定性比单纯延迟更有参考价值。4.2 架构调整的四个关键决策压测完我做了四个关键决策每个决策背后都有明确的数据支撑决策一将 ContextForge 改成按需加载而不是全量加载。之前所有会话的上下文都存在内存里改成懒加载——只有该会话的请求到达时才把它对应的上下文数据从磁盘载入。代价是首次请求延迟增加约 30ms换来的是内存占用下降 65%。决策二Peta 独立部署不跟 ContextForge 跑在同一进程。这个决策让 Peta 的 P95 直接从 180ms 降到了 60ms。原因是 ContextForge 的压缩任务占用了大量 CPU和 Peta 的 IO 线程抢时间片。拆开后两边各自安好。决策三引入两层缓存策略。第一层在 Peta 侧缓存路由表、鉴权结果、Server 健康状态——这些数据是低频变化的缓存 10 秒完全够用。第二层在 ContextForge 侧只缓存已压缩的摘要结果不缓存原文。这样即使触发压缩流程重复请求也能直接命中摘要。决策四把上下文压缩触发阈值从固定值改成自适应值。之前是超过预算 20% 就压缩改成超过预算且连续两轮都超预算才压缩。这个变化避免了高频抖动场景下的重复压缩实测压缩触发次数减少了 47%。4.3 调整后的实测数据对比调整完成后的对比数据如下指标调整前调整后变化率P95 延迟完整链路2.4s1.1s-54%P99 延迟完整链路5.3s2.4s-55%错误率3.2%0.8%-75%内存占用峰值2.1GB780MB-63%CPU 占用峰值85%42%-51%其中最值得说的是错误率从 3.2% 降到 0.8%。这个改善不是靠扩容而是靠两条第一Peta 独立部署后不会再因为 ContextForge 的 CPU 抢占而响应超时第二缓存策略消除了大量重复的鉴权和压缩计算降低了资源竞争的概率。注意如果你在压测中发现某个指标优化效果不明显先别急着继续调参回头检查一下架构层面是否存在互相干扰的问题。我见过太多人执着于微调超时时间却忽略了更根本的部署架构问题。5. 常见问题与排查实录5.1 上下文过期引发的灵异失败现象调整后跑了一段时间我遇到过一个非常隐蔽的问题部分客户端偶尔会收到上下文不存在的报错而且复现概率很低。查了半天才发现这是 ContextForge 的懒加载机制和 Peta 的缓存机制起了冲突——Peta 侧缓存了会话健康状态为可用而 ContextForge 那边的会话已经因为长时间空闲被自动回收了。客户端拿到缓存的健康状态去请求直接就挂了。解决方法是给 Peta 和 ContextForge 之间加一个会话活跃心跳机制ContextForge 每次成功响应请求后会向 Peta 发送一个活跃标记Peta 据此更新自己的健康缓存。这样两边的状态可以保持基本一致。这个问题的教训是网关里的缓存不能只看命中率还得考虑数据一致性。多组件之间共享会话状态时一定要明确谁是状态的所有者其他组件只能做参考缓存而不是确定性缓存。5.2 并行工具调用导致的目标 LLM 限流另一个问题是并行调度引起的。原本改成 Peta 直通后工具调用可以快速并行发出结果同一时刻会有大量请求打到同一个 MCP Server 上比如同一个 LLM 后端直接触发目标端的限流策略返回 429。这个问题的表象是目标端限流根因却是网关层缺少批量调度控制。我在 Peta 侧加了一个轻量的并发令牌桶针对每个目标 Server 限制同时进行的调用数默认值 8。注意这个限流值必须低于目标 Server 的配额否则完全没意义——所以配置前一定要先确认下游的并发上限。5.3 超时参数设置的试探过程关于超时参数我前后试过好几轮。一开始我把 Peta 的 write_timeout 设成 60s以为足够宽松结果大量请求在 30s 左右就超时断开了。排查发现MCP Server 在处理某些长任务比如数据库查询、文件搜索时内部默认超时只有 20s超过这个时间 Server 自己就放弃了。网关层的超时设再长也没用因为下游已经不干活了。最终我把 write_timeout 定为 30s同时跟各 MCP Server 团队约定所有超过 5s 的任务必须返回进度事件progress notification。这样客户端能看到任务还在推进就不会误以为网关无响应。5.4 问题速查表现象可能原因解决方案P95 延迟高但 CPU 不高下游 MCP Server 慢优先排查下游不是网关问题P95 延迟高且 CPU 高上下文压缩过于频繁调大压缩触发阈值考虑自适应策略偶发会话不存在Peta 缓存与 ContextForge 状态不一致增加心跳或调用后刷新机制多个工具调用互相阻塞缺少并行调度显式指定并行度用令牌桶限流大量请求被目标端限流网关层并发未控制每个目标 Server 单独配置并发上限内存持续增长会话上下文未回收设置空闲超时强制回收长时间无活跃的会话偶尔出现响应超时超时参数设置不合理区分 read_timeout 和 write_timeout6. 持续性调优的经验沉淀最后一个章节聊聊我在这个项目里沉淀下来的调优方法论算是我个人比较受用的三条经验。第一条经验是性能和简洁性的权衡本质是控制复杂度的边界。任何一个功能模块如果既要有状态又要实时响应这个模块迟早会成为整个链路的瓶颈。所以我现在设计组件时首先问三个问题它需要记忆吗它需要实时吗它能接受失败吗如果三个问题的答案分别是不需要需要不能那它就该走最轻量的路径只要有一个答案是需要它就该被拆出来单独部署。第二条经验是给网关加功能很容易但要时刻记录你加这个功能的成本和收益。我在 ContextForge 里加过会话导出、统计上报、schema 校验每一个功能在单测里都很完美但叠加起来之后延迟翻倍了。后来我给自己定了个规矩每次添加新功能前先跑一遍基准压测加完之后再跑一遍延迟增幅超过 15% 的功能必须重新设计。第三条经验是压测数据不要只看均值一定要重点盯 P99 和错误率的联动关系。因为均值很容易被大量快请求稀释真实体验往往是 P99 决定的。我在调优过程中发现一个关联性很强的现象当 P99 超过 3 秒时错误率会非线性飙升。原因可能是客户端侧超时机制被触发用户主动重试导致流量翻倍形成雪崩效应。所以我的压测目标很明确P99 保持在 3 秒以内错误率控制在 1% 以下这两个指标同时达标才算优化有效。ContextForge 和 Peta 的组合实验做到现在我最大的感受是一个好的 MCP 网关不是功能最全的网关而是功能边界最清晰的网关。重活儿和轻活儿分离、状态和服务分离、性能和简洁性在一个明确的预算框架下取平衡这项工作没有终点但每做一轮调整你对自己系统的理解就更深一层。