MCP Server 并发上不去,根因几乎都在”会话状态”:只要服务端把能力、版本、客户端身份存在连接上,就没法横向扩容,只能靠粘性会话硬撑。 2026-07-28 版规范已经把 MCP 改成无状态优先,正确的做法是把状态外置成显式句柄,再叠加缓存、批处理和背压。
为什么有状态的 MCP 天生不好扩容
SEP-2575(Make MCP Stateless,已为 Final 状态)把这个问题讲得很清楚,可以归纳成三点。
- 负载均衡做不了:客户端会话和持有其状态的那个实例绑死了。普通的 L4/L7 轮询用不上,因为轮询会把请求打到没有会话状态的后端。运维只能上粘性会话,结果是负载不均、扩容复杂。
- 容错差:持有会话的实例挂了,状态就丢了。客户端得自己发现断连、重连、再走一遍完整初始化握手。
- 实现复杂度高:服务端要管理会话的创建、回收,是内存泄漏的常见来源;客户端要写重连和状态重同步逻辑。
规范已经改了什么
2026-07-28 版本的变更里,和性能直接相关的这几条最值得注意。
| 变更 | 对并发的影响 |
|---|---|
移除协议层会话与 Mcp-Session-Id 头 | 请求可落到任意实例,标准负载均衡可用 |
移除 initialize / notifications/initialized 握手 | 省掉三次往返,首请求延迟显著下降 |
每个请求的 _meta 自带协议版本与能力 | 服务端无需记忆,天然支持多任务并发 |
新增 server/discover 且响应可缓存 | 发现流程不必每次请求都跑 |
列表结果带 ttlMs 与 cacheScope | 客户端可缓存,减少重复轮询 |
tools/list 返回顺序应确定 | 提高客户端缓存命中与提示词缓存命中率 |
规范原文还明确要求:服务端应当准备好处理来自多个任务、线程或会话的请求,不应要求客户端复用同一条连接或同一个进程;跨请求共享的状态必须用显式标识符引用,由客户端在每个请求里带上。
四件事:把并发做上去
一、状态外置成显式句柄
原来存在连接上的东西,改成服务端铸造一个句柄,作为普通工具参数在每次请求里传回来。规范里对应的是 SEP-2567 的做法:
// 不推荐:状态挂在连接上
{ "method": "tools/call", "params": { "name": "next_page" } }
// 推荐:状态显式传递
{ "method": "tools/call",
"params": { "name": "next_page",
"arguments": { "cursor": "eyJvZmZzZXQiOjEwMH0" } } }
句柄的落点建议放在 Redis 这类外部存储,而不是进程内存。这样任何实例都能处理任何请求,扩容就是加机器。
二、能缓存的都标上 TTL
新规范要求 tools/list、prompts/list、resources/list、resources/read、resources/templates/list 的返回都带 ttlMs(新鲜度提示,毫秒)和 cacheScope。这不只是给客户端看的——服务端自己也该按同样的 TTL 做内部缓存。
三类内容的缓存建议
工具清单 ttlMs 建议 300000(5 分钟),改动时主动失效
资源内容 ttlMs 按数据源更新频率定,静态资源可到小时级
提示词模板 ttlMs 建议 600000,版本化后几乎不变
注意 tools/list 的返回顺序要稳定。顺序乱变会让提示词前缀每次都不一样,直接打掉大模型侧的提示词缓存。
三、批处理合并小请求
客户端一次性拉十个资源,发十个 resources/read 是最浪费的写法。要么在工具设计层面提供批量入参,要么在传输层合并。工具设计上更推荐前者,因为批量入参对模型来说也更好理解——一次调用拿到全部结果,比十次调用少九轮往返。
四、背压:并发不是越高越好
工具后端通常是数据库或第三方 API,它们的并发上限往往远低于 MCP 层。不设闸的结果是把下游打挂,然后所有请求一起超时。
- 信号量限流:每个下游设独立并发上限,超出排队而不是拒绝。
- 队列有界:队列满了要快速失败并返回明确错误,不要让请求无限堆积。
- 超时分层:连接超时 < 请求超时 < 客户端超时,避免两边各等各的。
stdio 传输下的特殊处理
stdio 传输里,一个子进程就是一条”连接”。规范明确提醒:客户端不应把单个任务、线程或会话当作 stdio 进程的生命周期边界,服务端也不应把进程身份当成会话连续性的代理。
换句话说,stdio 进程应该是长期复用的,多个不相关的请求可以在同一条传输上交错进行。如果每个任务起一个进程,冷启动成本会把吞吐压得很低——尤其是 Java 、.NET 这类启动慢的运行时。
具体取舍在stdio 与 HTTP 传输的对比里有展开,配置层面则和不同作用域的配置共享相关。
断流之后怎么办
新规范移除了 SSE 的流可恢复性和消息重投递(Last-Event-ID 与事件 ID)。这意味着一条响应流断了,那个在途请求就丢了,客户端必须用新的请求 ID 重新发起。
对服务端的直接启示是:工具必须朝幂等方向设计。同一个请求重发一次,不能产生两次副作用。做法是把请求 ID 或客户端生成的幂等键作为工具参数的一部分,服务端用它去重。错误码的返回规范参考协议错误与工具执行错误的区分,长任务则建议走进度通知与 Tasks 轮询,而不是死等一条长连接。
无状态之后,版本协商怎么做?
_meta 里带 io.modelcontextprotocol/protocolVersion 和客户端能力,版本不匹配时服务端返回 UnsupportedProtocolVersionError。客户端也可以先调一次 server/discover 做前置版本选择。订阅类通知还能用吗?
resources/subscribe 已被 subscriptions/listen 取代:客户端开启一条长驻流并声明想要的通知类型,服务端在这条流上推送。与请求绑定的通知(进度、日志)仍走原请求的响应流。老的有状态实现要不要立刻改?
并发上去了,成本会不会失控?
ttlMs 的引入正是为了让客户端少轮询;工具清单返回顺序稳定则能提高提示词缓存命中率,这两项对 token 成本的削减通常比想象中大。参考来源:modelcontextprotocol.io(2026-07-28 规范与变更清单、架构总览、 SEP-2575 Make MCP Stateless)。本文基于上述公开材料重新组织,并补充服务端工程实践经验。










评论 (0)