Spring AI MCP Server分布式翻车现场:Streamable协议的甜蜜与危险,以及无状态救赎

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

手打不易,如果转摘,请注明出处!

本文链接:

https://zhangxiaofan.blog.csdn.net/article/details/161204324


目录

问题现象

MCP协议简介

环境

Maven配置

问题分析

初步猜测

网络调用链

问题解决

方法一:ALB开启会话粘滞(会话保持)

方法二:实现基于Redis的分布式Session共享

[方法三:换 MCP 协议为 STATELESS 无状态协议](#方法三:换 MCP 协议为 STATELESS 无状态协议)

问题总结

结论

参考资料


问题现象

Spring AI项目,开发了mcp接口(http://api.test.com/mcp/timetool),协议配置的是:streamable。

本地和测试环境调用都正常,但是一旦部署到生产后,通过postman请求MCP接口,http://api.test.com/mcp/timetool,**偶尔成功,偶尔失败**,失败信息如下:

复制代码
{
  "cause": null,
  "jsonRpcError": null,
  "message": "Session not found: bdc41b7a-9e25-4481-8f1b-fdcd939e3e17",
  "suppressed": [],
  "localizedMessage": "Session not found: bdc41b7a-9e25-4481-8f1b-fdcd939e3e17"
}

MCP协议简介

问题分析前,我们有必要先了解下MCP,MCP(Model Context Protocol) 是Anthropic提出的标准化协议,旨在为AI模型建立统一的上下文交互接口,使其能够安全、可控地连接外部工具、数据源及服务。Spring AI 对该协议提供了完整的服务端实现,并针对不同场景抽象出三种传输协议,以适应多样化的部署和交互需求。

特性 SSE STREAMABLE STATELESS
通信模式 单向推送(服务端→客户端) 客户端发起请求 + 服务端流式响应 请求-响应(双向一次性)
连接类型 持久长连接 短期长连接(请求生命周期内) 短连接
会话状态 需要 Session 需要 Session 无 Session
客户端请求方式 GET(建立连接)+ 额外 POST POST(带请求体,流式接收) POST(同步返回)
实时推送能力 ✅ 支持 ✅ 支持 ❌ 不支持
部署复杂度 需保持会话粘滞 需保持会话粘滞 无状态,易于水平扩展
适合的调用模式 持续监听、事件驱动 流式交互、AI 流式生成 一次性工具调用、查询

配置示例:

复制代码
# SSE协议(传统)
spring.ai.mcp.server.protocol=SSE

# Streamable HTTP协议(推荐)
spring.ai.mcp.server.protocol=STREAMABLE

# Stateless协议(云原生首选)
spring.ai.mcp.server.protocol=STATELESS

环境

JDK:21、SpringBoot 3.4

服务部署:微服务、多实例分布式部署

网络:双层ALB架构

请求先通过第一层ALB负载均衡,轮询到2个后端集群,每个后端集群又有一层ALB,轮询请求到5个pod,合计10个pod。

Maven配置

MCP服务器maven配置:

复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.4.6</version>
</parent>

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    <version>1.1.2</version>
</dependency>

application.yaml配置:

复制代码
spring:
  ai:
    mcp:
      server:
        name: streamable-mcp-server      # MCP 服务器名称
        version: 1.0.0                   # 服务器版本
        type: ASYNC                      # SYNC:客户端发起请求后,线程会阻塞等待工具执行完成,ASYNC:不会阻塞当前线程,执行完成后异步响应
        request-timeout: 20s             # 请求超时,默认 20s
        protocol: STREAMABLE             # 协议类型:简单理解就是 STREAMABLE 是 SSE 的升级版,可选:SSE, STREAMABLE,STATELESS
        streamable-http:
          mcp-endpoint: /mcp/timetool    # MCP 端点路径
          keep-alive-interval: 30s       # 保持连接间隔
          disallow-delete: false         # 是否禁用 DELETE 方法

问题分析

初步猜测

现象有个特点:偶现,那我们可以大概猜测问题的原因在于三个因素:

首先,STREAMABLE协议是有状态的,需要服务端维护Session;

其次,两层ALB采用轮询策略,缺乏会话保持机制;

最后,Spring AI MCP Server默认将Session存储在Pod本地内存中。当同一Session的后续请求被路由到其他Pod时,目标Pod既无Session元数据,也无对应的SSE连接,导致'Session not found'错误。

网络调用链

整个网络调用链路如下:

  • 客户端请求先抵达第一层 ALB ,通过轮询分发至集群 A / 集群 B的第二层 ALB;
  • 第二层 ALB 再次通过轮询,将请求分发至集群内 5 个 Pod 中的任意一个;
  • 正常情况:同一会话的所有请求命中同一个 Pod,Pod 本地存在 Session,请求成功;
  • 异常情况:同一会话的请求被轮询到其他 Pod,目标 Pod 无该 Session,直接抛错。

最终通过本地启动2个MCP Server实例(端口8080、8081)模拟,证实是该原因导致。

问题解决

方法一:ALB开启会话粘滞(会话保持)

强制同一个客户端的所有请求始终路由到同一个 Pod,保证 Session 不跨节点。

关闭两层的ALB的轮询策略,开启基于Cookie或其他字段的会话保持。

阿里云、华为云、腾讯云等配置大同小异,大概步骤如下:

  • 粘滞方式:基于 Cookie 粘滞(优先)/ 源 IP 粘滞;
  • 粘滞超时时间:≥MCP 的keep-alive-interval(你配置的 30s,建议设为 60s);
  • 禁用 ALB 的连接复用 / 强制轮询,保证会话绑定。

该方法无需改动代码,仅配置ALB即可。

方法二:实现基于Redis的分布式Session共享

将会话信息从 Pod 内存移至 Redis ,让所有 Pod 共享会话,任意 Pod 都能处理同一 Session 的请求。考虑到需要一定开发量,而且 STREAMABLE 协议下 SSE 连接与底层流绑定,仅仅存储会话元数据可能不够,还需要处理流的多播。即使Session元数据共享了,后续请求到达其他Pod时,那个Pod可能没有对应的SSE连接来推送结果。因此除了共享Session之外,还需要实现SSE消息的多播机制(如通过Redis Pub/Sub)替代SSE以支持双向通信。

该方案对代码改造较大,大家可参考官方MCP文档实现:

https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html

方法三:换 MCP 协议为 STATELESS 无状态协议

作者写的测试工具,TimeTool工具是无会话状态依赖(纯无状态接口),直接将协议改为STATELESS就可以,抛弃 Session 。

复制代码
spring:
  ai:
    mcp:
      server:
        protocol: STATELESS   # 不再产生服务端会话,每个请求独立处理

作者最终采用的方法三 ,因为demo中开发的MCP工具仅为一次性请求-响应,根本不需要保持长连接和流式推送。

问题总结

初期想法是用Java开发一个MCP工具,心想MCP工具要支持流式,理所应当的认为应该设置spring.ai.mcp.server.protocol = STREAMABLE ,实际上,我把MCP工具本身 和 server 支持流式弄混淆了,这是两个不同维度的对象,它们其实是2个概念。一个是MCP工具本身,一个是服务server支持的MCP协议。

当大模型决定调用一个 MCP 工具时,过程通常是:

  • 模型暂停文本生成,发出一个 tool_use 指令。

  • 客户端(或服务端)执行该工具,等待完整的工具结果。

  • 将完整结果作为一条 tool_result 消息塞回对话上下文。

对工具这一层而言,结果是一次性的,针对当前MCP工具本身来说,即使MCP工具支持流式,对AI来说,据了解,目前主流框架也是要等待完整结果

那么MCP 协议支持SSE、STREAMABLE 意义是什么?

主要有如下作用:

  • 非阻塞响应

    如果使用简单的 HTTP 一问一答,客户端在发起工具调用后必须一直阻塞等待执行完毕,期间不能做任何其他事(除非使用回调或轮询)。而有了 SSE 通道,客户端发出调用请求后可立即释放连接,继续处理其他任务(比如同时触发多个工具),等结果到来时通过 SSE 事件自然获得通知。这极大地提高了并发和资源利用率。

  • 服务端主动推送其他事件

    工具并不是 MCP 的全部。MCP 还涉及资源(resources)、提示模板(prompts)等的动态更新。通过 SSE,服务端可以随时向客户端推送资源变更、工具状态变化等通知,客户端无需轮询。这对于构建动态工具生态至关重要。

  • 长时运行工具的进度反馈(未来可能支持)

    虽然当前 MCP 规范并未强制要求工具结果必须以流式分块返回,但 SSE 通道天然支持服务端在执行工具时不断推送"中间日志"、"进度百分比"等事件(只要 MCP 社区后续扩展协议支持)。这正是之前代码中 Flux 想达到的效果,但需要协议层面约定好。一旦支持,你的流式时间输出就能真正逐条推送给客户端,而客户端可以缓存这些中间数据,待完成后再组装完整结果交给模型。这种能力完全依赖于 SSE 这类持久化连接

  • 跨网络防火墙与长连接保持

    SSE 基于 HTTP,易于通过防火墙和负载均衡器,同时可以自动重连,适合云端部署。相较于需要双向长连接的 WebSocket,SSE 更轻量,且足以满足 MCP 非对称通信的需求

结论

经过分析Spring AI MCP Server在分布式环境下的Session丢失问题,明确问题根因是有状态协议与无会话保持负载均衡的组合冲突。针对不同业务场景,一般有三种解决方案:

  1. ALB会话粘滞:适合快速解决,无需代码修改

  2. Redis分布式Session:适合需要Session持久化的场景

  3. STATELESS协议:适合无状态工具,是最简洁的方案

对于本文案例的TimeTool工具,工具本身无需支持有状态,那么采用STATELESS协议是最优解,仅需修改一行配置即可彻底解决问题。

建议开发者在设计MCP工具时,优先评估工具的状态依赖特性,选择合适的协议类型,避免在分布式部署时遇到类似问题。


参考资料

  1. Anthropic MCP Specification

  2. Spring AI MCP Server Documentation

  3. Server-Sent Events (SSE) Specification

相关推荐
Elastic 中国社区官方博客2 小时前
用于调试 LLM 延迟、成本和 GPU 饱和度的 ES|QL 查询
大数据·人工智能·elasticsearch·搜索引擎·ai·云原生·serverless
夕除2 小时前
spring boot 11
java·spring boot·后端
踏着七彩祥云的小丑2 小时前
AI——LangChain 三大核心概念
人工智能·ai·langchain
TechPioneer_lp2 小时前
就业指导|中九非科班毕业,华为 OD 做 Java 后端想转 C++,能找到深度学习挂钩的岗工作吗?
java·c++·华为od·华为·就业指导·校招指导
枕星而眠2 小时前
C++ String类精讲:从基础用法到进阶底层原理
开发语言·c++·后端·学习方法
Dicky-_-zhang2 小时前
分布式ID生成方案详解与实战
java·jvm
m0_474606782 小时前
JAVA - 使用Apache POI 自定义报表字段手写导出(支持-合并单元格)
java·开发语言·apache
念何架构之路2 小时前
Go pprof性能剖析
开发语言·后端·golang
zhz52142 小时前
Spring Boot 接入国密实战:传输加密(TLCP)+ 密码加密(SM4)
java·spring boot·后端·国密·sm4