最新版 Model Context Protocol(MCP)规范移除了协议级会话。这意味着远程 MCP 服务器不再要求负载均衡器把同一客户端持续路由到同一个实例,也不必为协议会话维护专门的共享存储。
这项变化直接简化了 AWS 上的多副本部署:ALB、NLB 或 Kubernetes Service 可以把每个请求发送给任意健康实例。不过,“协议无状态”并不等于“系统没有状态”。应用数据、重试、幂等性和可观测性仍然需要由其他层明确承担。
部署模型发生了什么变化
依赖会话的服务通常需要保存客户端上下文。请求一旦被路由到另一台服务器,新实例可能无法继续处理,因此部署时往往要选择以下方案之一:
- 在负载均衡器上启用 sticky session;
- 将协议会话存入 Redis、数据库等共享存储;
- 在实例扩缩容和滚动发布期间迁移或保留会话;
- 接受实例失效后会话中断。
协议级会话被移除后,每个 MCP 请求都可以独立路由:
MCP Client
|
v
AWS Load Balancer
|------> MCP Server A
|------> MCP Server B
`------> MCP Server C
|
v
External state and tools
服务器实例因此更接近可替换的计算单元。实例重启、扩容或滚动更新时,不再需要恢复 MCP 协议会话。对于 ECS、EKS 或基于 EC2 Auto Scaling Group 的部署,这会减少扩缩容过程中对连接归属的依赖。
需要注意,无状态化解决的是协议路由问题,不会自动解决业务状态。例如,一个工具调用创建了工单、启动了训练任务或触发了付款,这些结果仍然必须存储在数据库、队列或外部服务中。
在 EKS 上取消会话亲和性
下面是一个可以改造的 Kubernetes 部署示例。运行前需要把镜像地址替换为自己的 MCP Server 镜像,并确认应用提供 /healthz 健康检查端点。
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
spec:
replicas: 3
selector:
matchLabels:
app: mcp-server
template:
metadata:
labels:
app: mcp-server
spec:
containers:
- name: mcp-server
image: 123456789012.dkr.ecr.us-east-1.amazonaws.com/mcp-server:1.0.0
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: mcp-server
spec:
type: LoadBalancer
sessionAffinity: None
selector:
app: mcp-server
ports:
- name: http
port: 80
targetPort: http
保存为 mcp-server.yaml 后执行:
kubectl apply -f mcp-server.yaml
kubectl rollout status deployment/mcp-server
kubectl get pods -l app=mcp-server
kubectl get service mcp-server
这里最关键的不是 replicas: 3,而是任意副本都能独立处理请求。应用不应假设“上一次请求一定到达当前进程”,也不应只在进程内存中保存后续调用必需的数据。
如果使用 AWS Load Balancer Controller,还应检查 Ingress 或 Target Group 配置,确保没有因为历史部署而遗留粘性 Cookie。是否保留长连接则是另一个问题:连接生命周期和协议会话不是同一个概念,不能仅通过关闭粘性路由来推断长连接行为。
状态消失了吗?只是换了归属
协议无状态之后,职责边界需要重新划分。
应用状态
业务结果应保存在具有明确生命周期的系统中,例如 DynamoDB、Aurora、S3、ElastiCache 或任务队列。进程内缓存可以用于提速,但不能成为正确性的唯一来源。
长时间运行的操作可以返回业务任务 ID,客户端随后使用该 ID 查询状态。这个 ID 属于应用模型,而不是为了维持 MCP 路由而创建的协议会话。
重试与幂等性
负载均衡器、SDK 或客户端可能重试失败请求。查询类工具通常容易重试,但带副作用的工具可能造成重复写入。
下面的请求仅用于演示外围 HTTP 层如何传递幂等键;具体 URL、认证方式和工具参数需要按实际 MCP SDK 与服务实现调整:
curl --request POST 'https://mcp.example.com/mcp' \
--header 'Authorization: Bearer REPLACE_ME' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: ticket-2025-00042' \
--header 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
--data '{
"jsonrpc": "2.0",
"id": "req-42",
"method": "tools/call",
"params": {
"name": "create_ticket",
"arguments": {
"title": "Database connection timeout"
}
}
}'
Idempotency-Key 并不是“加上请求头就自动生效”。服务端需要把它与调用者、工具名称和参数摘要关联,并持久化执行状态与结果。重复请求到达另一个副本时,该副本才能返回已有结果,而不是再次执行副作用。
还要为幂等记录设置过期时间,并明确处理以下情况:
- 同一个键携带了不同参数;
- 第一次执行超时,但后端操作实际上已成功;
- 工具执行到一半失败;
- 两个副本同时收到相同的键。
可观测性
取消会话后,不能再依赖某个实例的本地日志拼接完整调用过程。建议每个请求都携带请求 ID 或分布式追踪上下文,并在负载均衡器、MCP Server、工具适配器和下游服务中继续传播。
至少应记录:
- 请求 ID、追踪 ID和调用者身份;
- MCP 方法及工具名称;
- 目标实例、处理时间和结果状态;
- 重试次数与幂等命中情况;
- 下游依赖的错误类型,但不要记录令牌或敏感参数。
上线前的检查清单
采用无状态 MCP 部署时,可以逐项确认:
- 任意健康副本是否都能处理下一次请求;
- 实例重启后,业务正确性是否仍然成立;
- 负载均衡器是否关闭了不再需要的会话亲和性;
- 带副作用的工具是否定义了幂等和重试策略;
- 长任务是否使用外部任务状态,而不是进程内对象;
- 身份认证和授权信息是否能随每个请求被验证;
- 日志、指标和追踪是否能跨实例关联;
- 扩容测试是否覆盖并发写入、重复请求和实例中断。
无状态 MCP 的核心收益不是少配一个负载均衡参数,而是让计算实例真正可替换。与此同时,原先被协议会话隐式承担的责任会显性地落到应用、数据层和运维体系中。把这些边界设计清楚,横向扩容才会从“能够增加副本”变成“增加副本后仍然正确”。