Skip to content

Daily Study

更新: 8/8/2025 字数: 0 字 时长: 0 分钟

Daily Plan

#todo

  • [ ]

针对MCP中SSE协议的缺陷与解决办法

SSE (Server-Sent Events): 是一种基于 HTTP 的轻量级单向推送技术。客户端向服务器发起一个 GET 请求,服务器保持此连接开启,并可以随时通过这个连接向客户端发送事件数据。它比 WebSocket 更简单,非常适合这种“服务器到客户端”的配置推送场景。

在 MCP over SSE 的实现中,通常流程是:

  1. Client 发起一个 HTTP GET 请求到 Server 的某个端点。
  2. Server 接受连接,创建一个会话(Session),并为这个会话生成一个唯一的 sessionId
  3. Server 将 sessionId 返回给 Client,并保持 HTTP 连接不关闭。
  4. 之后,Server 会通过这个连接持续推送配置更新。Client 在后续的某些交互中(如 ACK 确认)可能会带上这个 sessionId 来表明自己的身份。

一个 单实例 的服务器环境中,这个模型工作得很好,因为从始至终都只有一个 Server 实例在处理所有事情。当引入多个 Server 实例并使用负载均衡器(Load Balancer)时,问题就暴露了。

环境:

  • 一个客户端 (MCP Client SDK)
  • 一个负载均衡器 (如 Nginx, HAProxy, ALB 等),采用轮询 (Round-Robin) 策略
  • 两个无状态的服务器实例 (Server A, Server B)

失败流程:

  1. [连接建立]

    • 客户端发起连接请求 GET /mcp-stream

    • 负载均衡器将这个请求转发给了 Server A。

    • Server A 在其 本机内存 中创建了一个会话,生成 sessionId: "xyz123",并将这个 session 信息 { sessionId: "xyz123", clientInfo: ... } 存储在自己的一个 map 或缓存中。

    • Server A 通过 HTTP 连接将 sessionId: "xyz123" 返回给客户端,连接保持。

  2. [后续交互/心跳/ACK]

    • 一段时间后,客户端可能需要发送一个 ACK 确认包,或者一个用于保持连接的心跳请求。这个请求中包含了它的身份标识:sessionId: "xyz123"

    • 客户端发起 POST /mcp-ack 请求,并在请求体或头中携带 Authorization: Bearer xyz123X-Session-ID: xyz123

    • 负载均衡器按照轮询策略,这次将请求转发给了 Server B。

  3. [请求失败]

    • Server B 收到了这个带有 sessionId: "xyz123" 的请求。

    • Server B 在自己的 本机内存 中查找这个 sessionId。

    • 它找不到! 因为这个 session 是由 Server A 创建并只存在于 Server A 的内存中的。

    • 由于找不到会话信息,Server B 认为这是一个非法或过期的请求,因此拒绝处理,返回一个 401 Unauthorized404 Not Found 的错误。

    • 最终,客户端收到了一个失败的响应。如果这是一个关键的 ACK,可能会导致 Server A 认为客户端已失联,从而断开 SSE 连接,造成整个配置推送的中断。

缺陷根源总结: 将会话状态(Session State)存储在了单个、易失的服务器实例内存中,破坏了服务的 无状态性 (Statelessness)。这使得服务无法进行水平扩展,因为实例之间无法共享状态。

解决方案:集中式会话存储 (Centralized Session Store)

  • 做法: 引入一个独立的、高可用的中间件作为“会话存储中心”。所有服务器实例都去这个中心读取和写入会话信息。

    常用的会话存储中心:

    1. Redis (首选): 一个基于内存的键值数据库。它读写速度极快,非常适合存储生命周期不长的会P话数据。SETex sessionId 'sessionData' ttl 这样的命令可以完美地创建带过期时间的会话。

    2. Memcached: 另一个高性能的内存对象缓存系统,也可以用于会话存储。

    3. 数据库 (如 PostgreSQL, MySQL): 也可以将会话信息存入数据库,但相对于 Redis,数据库的读写延迟更高,通常不作为首选,除非有特殊的数据持久化需求。

  • 改造后的流程:

    1. [连接建立]

      • 客户端请求到达 Server A。

      • Server A 生成 sessionId: "abc456"

      • Server A 不再将 session 存入本机内存,而是连接到 外部的 Redis,执行命令 SET abc456 '{"clientInfo":...}',将 session 数据存入 Redis。

      • Server A 返回 sessionId 给客户端。

    2. [后续交互]

      • 客户端携带 sessionId: "abc456" 的 ACK 请求被负载均衡器转发到了 Server B。

      • Server B 收到请求后,拿着 sessionId: "abc456" 去 查询 Redis。

      • Redis 中存在这个 key,并返回了对应的 session 数据。

      • Server B 成功获取了会话信息,验证通过,处理请求成功。

  • 优点:

    • 真正的无状态服务: 后端服务器实例不存储任何会话数据,可以随意扩缩容、重启、下线,而不会影响用户的会话。

    • 高可用性: 只要集中式存储(如高可用的 Redis 集群)是稳定的,整个会话服务就是稳定的。

    • 提升了系统的健壮性和可扩展性。

  • 缺点:

    • 架构复杂性增加: 需要引入并维护一个新的中间件(如 Redis 集群)。

    • 性能开销: 每次会话验证都需要进行一次网络调用(访问 Redis),虽然 Redis 很快,但这仍然比访问本机内存要慢。但在分布式环境中,这种开销是为了换取可扩展性和可靠性而必须接受的。

菜就多练

本站访客数 人次 本站总访问量