MCP 规范 2025-03-26 用 Streamable HTTP 取代 SSE 传输层。本文从代码、性能数据和踩坑记录三个维度,拆解 Go 服务迁移过程:SSE 在 1000 并发下延迟从 18ms 飙到 1.5s,新方案单端点架构只需几十个连接。
2025 年 3 月 26 日,Model Context Protocol 规范修订版发布,PR #206 做了一件事:用流式方案全面取代原来的 SSE 传输层。PR #228 同期加入了 JSON-RPC 批处理。到 2026 年年中,几乎所有活跃的 Go 版 SDK——包括 8300+ stars 的社区主力库和官方实现——都已将新传输模式作为推荐选项。关于 MCP 在企业落地的完整决策框架,可参考我们之前的MCP 协议 2026 企业落地指南。如果你还在用事件流模式跑服务,生产环境出问题是迟早的事。
事件流(SSE)是 HTML5 时代的产物,设计初衷是浏览器内的单向推送——服务端往客户端推数据,客户端不往回发。MCP 早期把它搬到服务端场景,架构变成两条独立通道:一条 POST 发请求,一条 /sse 端点收响应流。这个设计在低并发下能跑,但压测数据暴露了三个不可修复的问题。
每个 SSE 客户端需要一个持久 TCP 连接。1000 个并发客户端 = 1000+ 个 socket。Linux 默认单进程 fd 上限 1024,超了就报 too many open files。实际压测中,并发超过上限后新请求大规模失败——而流式方案在同等条件下维持接近 100% 的成功率[1]。
旧方案的延迟劣化是非线性的。并发从 100 增至 1000 时,平均响应时间从 18 毫秒飙到 1.5 秒。根因:长连接对 OS 资源的过度占用,加上 HTTP/1.1 连接无法复用导致的队头阻塞。AI 工具调用的 p99 到了 1.5 秒,用户体验从"即时响应"直接跌到"明显卡顿"。
企业网络中的防火墙和反向代理会主动断开长时间空闲的连接。旧方案默认心跳不足以维持连接存活,频繁意外断开。更致命的是,连接断开后无法传递上下文状态——重连等于从头开始,所有工具调用结果丢失。
这三块硬伤不是调参能解决的。事件流是浏览器导向的机制,硬套到服务端 RPC 场景上本身就是架构错配。
这套方案的核心思路:端点统一 + 按需流式 + 会话抽象。它不需要新协议,而是对现有语义的创造性重构。
旧方案有两类端点:POST 发请求 + /sse 收流。新方案全部收敛到一个端点(通常 /mcp)。客户端发一个普通 POST,后台根据响应内容类型动态决定返回完整 JSON 还是升级为流式传输:
# 小结果 → 200 OK + application/json
# 大结果 → 102 Processing + text/event-stream
# 连接不需要预先建立,用完即释放
102 Processing 状态码是关键——后台用它告知「在处理,准备收流」,透明地把连接升级为事件流。和旧方案的最大区别:连接不需要预先建立,流式传输发生在同一个请求-响应周期内,用完即释放。
新方案用 Mcp-Session-Id 请求头维护上下文。客户端初始化握手后拿到一个标识符,后续请求带上这个头,后台就能在无状态架构之上重建对话上下文。这带来两个关键能力:
Last-Event-ID 头,流式传输中断后从断点恢复,而不是从头开始。下面以 Go 社区最主流的 mcp-go 库(8300+ stars,兼容规范 2025-11-25)做对比。三种传输模式(Stdio、SSE、新方案)的完整工程取舍分析,见用 Go 写 MCP 工具端的工程决策。
// SSE 模式:需要 /sse + /message 两个端点
s := srv.NewMCPServer("file-ops", "1.0.0")
s.AddTool(searchTool, searchHandler)
sse := srv.NewSSEServer(s)
sse.Start(":8080")
事件流模式的部署负担不只在代码。反向代理不能缓冲事件流、负载均衡要 sticky session、防火墙要允许长连接保持——这些配置在生产环境很容易漏掉或改错。
// 新方案:单一 /mcp 端点,挂在标准库上
s := srv.NewMCPServer("file-ops", "1.0.0")
s.AddTool(searchTool, searchHandler)
stream := srv.NewStreamableHTTPServer(s)
http.Handle("/mcp", stream)
http.ListenAndServe(":8080", nil)
改动面:NewSSEServer → NewStreamableHTTPServer,双端点变单端点,直接挂在 net/http 上。不需要特殊 Nginx 配置、不需要 sticky session——会话头自带追踪能力。Kubernetes 上一个普通的 ClusterIP Service + Ingress 就能搞定。
基于社区公开的压测数据[1],以及我们内部在 K8s 集群上对 mcp-go v0.24 的复现测试(Go 1.23,4C8G 节点,wrk 压测):
| 指标 | SSE(旧) | 单端点模式(新) |
|---|---|---|
| 1000 并发成功率 | ~45% | ~99.5% |
| p50 延迟(1000 并发) | ~320ms | ~22ms |
| p99 延迟(1000 并发) | ~1500ms | ~48ms |
| 内存占用(1000 并发) | ~2.1 GB | ~180 MB |
| 企业防火墙兼容 | ❌ | ✅ |
| 函数计算部署 | ❌ | ✅ |
| 客户端复杂度 | 双通道 + 重连 | 单客户端 |
延迟差距不是 ulimit -n 调大能解决的。旧方案高并发下的响应时间劣化是 H1.1 队头阻塞 + 长连接资源竞争叠加的结果,除非换 H2 或 H3 否则无解——而新方案在 H1.1 上就能达到接近 H2 的效果。
新传输模式下,客户端在 initialize 响应中拿到会话 ID 后必须缓存,后续每个请求都带上 Mcp-Session-Id。忘了带的结果是后台返回 400 或把你当成新会话重新初始化。我们踩过一次——客户端库的旧模式不需要显式传这个头(连接本身就是会话),迁移后如果不改客户端代码,所有请求全部失败。
修法:确认客户端库支持流式传输。Go 侧用官方 SDK 的 NewClient() 会自动处理会话管理;社区版确认版本 ≥ v0.18。
新方案的流式响应通过 102 Processing + text/event-stream 实现。Nginx 默认缓冲后端响应,导致流式数据被囤积到缓冲区满才一次性发出,看起来像"卡住不动"。
修法:
location /mcp {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
}
PR #228 同期引入的 JSON-RPC 批处理允许一个 POST 发多个请求。社区版从 v0.22 支持批处理,但需要显式开启。默认情况下批处理请求会被拆成单个处理——如果依赖批处理的原子性(如"列出工具 + 调用工具"必须在同一事务中),务必检查 SDK 版本和开关。
技术上可以。同一套 tool handler 分别挂到两种传输实例上,监听不同端口。但官方规范已明确流式方案为推荐传输模式,事件流模式保留仅为向后兼容。新项目直接用新的,旧项目尽快迁移。
取决于你的客户端。Claude Desktop、VS Code Copilot 这类通用 Host 已经在 2025 下半年支持了新方案。自研客户端需要确认底层库是否支持 Mcp-Session-Id 和 text/event-stream 响应解析。
一个明显的取舍:旧方案的"主动推送"在新方案下需要通过 notifications 机制实现,即客户端发起 GET 长轮询来接收推送。但对绝大多数工具调用场景(请求-响应模式),这个取舍完全值得。
到 2026 年年中两个都成熟。官方 SDK(github.com/modelcontextprotocol/go-sdk)设计更"Go 味"——泛型、强类型、接口简洁,生态插件相对少。社区版 8300+ stars,middleware、hook、连接池等周边更丰富。两个 SDK 的详细安装和部署流程见Go 语言 MCP Server 实战:官方 SDK 从零到高性能部署。偏好"跟着官方走"选前者,看重现成轮子选后者。