无状态 MCP 如何让 AWS 服务摆脱粘性会话

2026-09-25 30 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

最新的 Model Context Protocol(MCP)规范取消了协议层会话。对远程 MCP 服务来说,这意味着负载均衡器不再需要把同一客户端持续绑定到同一个实例,服务端也不必为了协议本身维护会话存储。

这项变化直接简化了 AWS 上的横向扩缩容:ALB、ECS、EKS 或其他计算平台可以把每个请求路由到任意健康实例。不过,“无状态”并不等于系统没有状态。业务上下文、重试、幂等性和可观测性仍然存在,只是需要由更合适的应用层与基础设施层负责。

去掉会话亲和性后,部署模型发生了什么

传统的有状态远程服务通常依赖这样的请求链路:

Client -> Load Balancer -> 固定实例 A -> 实例内会话状态

一旦实例 A 重启、扩容或被替换,客户端上下文就可能丢失。为了避免这种情况,团队往往启用 sticky session,并额外处理会话复制、过期和恢复。

无状态 MCP 允许链路变成:

Request 1 -> Load Balancer -> Instance A
Request 2 -> Load Balancer -> Instance C
Request 3 -> Load Balancer -> Instance B

每个实例都能独立处理请求,不依赖前一个请求落在哪台机器上。这会带来几个直接收益:

  • 实例可以随时启动、停止或替换,滚动发布更简单。
  • 扩容后,新实例无需接管协议会话即可接收流量。
  • 负载均衡器不再需要维护客户端与实例之间的绑定关系。
  • 服务端无需为协议会话设计本地缓存或共享会话数据库。
  • 单个实例故障不会因为会话绑定而持续影响同一批客户端。

但必须注意:协议无状态只说明 MCP 请求不依赖协议层会话,并不保证工具调用本身没有副作用。创建工单、发送邮件、提交付款等操作仍然需要业务级幂等控制。

状态没有消失,而是需要重新归位

一个实用的划分方式是把状态分成四类:

状态类型 推荐位置 例子
请求上下文 当前请求 参数、认证信息、租户 ID、追踪 ID
持久业务状态 外部数据库或对象存储 工单、任务进度、用户配置
短期协调状态 Redis、DynamoDB 等外部系统 幂等键、分布式锁、限流计数器
诊断信息 日志、指标和追踪平台 请求耗时、错误码、工具名称

不应继续放在实例内存中的内容包括“当前客户端进行到第几步”、尚未持久化的任务结果,以及决定重复请求是否应该再次执行的记录。实例内缓存可以存在,但它应该是可丢弃、可重建的性能优化,而不能成为正确性的前提。

重试策略也需要明确分层:客户端或网关可以重试网络错误,服务端则应通过幂等键判断同一个有副作用的操作是否已经执行。JSON-RPC 请求 ID、追踪 ID 和幂等键用途不同,不应混为一谈:

  • 请求 ID 用于匹配请求与响应。
  • 追踪 ID 用于串联日志和分布式调用。
  • 幂等键用于识别同一次业务操作的重复提交。

一个最小的无状态请求处理示例

下面的 FastAPI 示例不是完整的 MCP SDK 实现,而是一个可运行的“无状态 MCP 风格边界”示例,用来展示关键约束:每个请求携带完整参数,进程不保存客户端会话,并为日志生成关联 ID。接入生产环境时,应替换为符合目标 MCP 规范版本的 SDK 和传输实现。

创建 requirements.txt:

fastapi==0.115.12
uvicorn[standard]==0.34.2

创建 app.py:

import logging
import uuid
from typing import Any

from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel, Field

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)

app = FastAPI()


class RpcRequest(BaseModel):
    jsonrpc: str = "2.0"
    id: str | int
    method: str
    params: dict[str, Any] = Field(default_factory=dict)


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


@app.post("/mcp")
def handle_request(
    request: RpcRequest,
    x_request_id: str | None = Header(default=None),
) -> dict[str, Any]:
    correlation_id = x_request_id or str(uuid.uuid4())

    logging.info(
        "request_id=%s rpc_id=%s method=%s",
        correlation_id,
        request.id,
        request.method,
    )

    if request.method == "tools/call":
        tool_name = request.params.get("name")
        arguments = request.params.get("arguments", {})

        if tool_name == "add":
            try:
                result = float(arguments["a"]) + float(arguments["b"])
            except (KeyError, TypeError, ValueError) as exc:
                raise HTTPException(
                    status_code=400,
                    detail="add requires numeric arguments a and b",
                ) from exc

            return {
                "jsonrpc": "2.0",
                "id": request.id,
                "result": {
                    "content": [{"type": "text", "text": str(result)}],
                    "requestId": correlation_id,
                },
            }

    return {
        "jsonrpc": "2.0",
        "id": request.id,
        "error": {"code": -32601, "message": "Method or tool not found"},
    }

启动服务并发送请求:

python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
uvicorn app:app --host 0.0.0.0 --port 8000

在另一个终端执行:

curl -s http://localhost:8000/mcp \
  -H 'Content-Type: application/json' \
  -H 'X-Request-ID: demo-request-001' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "add",
      "arguments": {"a": 12, "b": 30}
    }
  }'

这个进程可以被任意副本替代,因为处理结果只取决于当前请求。若工具需要读取业务数据,应从数据库读取;若任务耗时较长,可以把任务写入队列并返回任务标识,而不是把进度留在某个 Web 实例的内存中。

在 EKS 或 ALB 上明确关闭粘性路由

在 Kubernetes 中,可以显式声明 Service 不使用会话亲和性。运行前需要把镜像地址改成自己的 MCP 服务镜像:

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:
            - containerPort: 8000
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 3
            periodSeconds: 5
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
  name: mcp-server
spec:
  sessionAffinity: None
  selector:
    app: mcp-server
  ports:
    - port: 80
      targetPort: 8000

应用并检查副本:

kubectl apply -f mcp-server.yaml
kubectl rollout status deployment/mcp-server
kubectl get pods -l app=mcp-server -o wide
kubectl get service mcp-server

如果使用 AWS Application Load Balancer,也应检查目标组是否启用了 stickiness。可以先导出目标组 ARN,再关闭粘性:

export TARGET_GROUP_ARN='arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/mcp-server/replace-me'

aws elbv2 modify-target-group-attributes \
  --target-group-arn "$TARGET_GROUP_ARN" \
  --attributes Key=stickiness.enabled,Value=false

关闭粘性前要先确认应用确实不依赖实例内状态。否则,这个配置变化只会更快暴露原有的状态耦合问题。

上线前需要补齐的工程边界

无状态协议降低了基础设施复杂度,却把一些隐含问题变成了显式设计任务。上线前可以按下面的清单检查:

  • 认证完整性:每个请求都能独立完成身份验证与授权,不依赖前一次请求写入的内存状态。
  • 幂等性:有副作用的工具接受业务幂等键,并在 DynamoDB、Redis 或数据库中记录执行结果。
  • 重试边界:明确哪些错误可以重试,设置退避与最大次数,避免网关和客户端叠加重试形成放大效应。
  • 超时管理:为负载均衡器、应用、下游 API 和模型调用设置协调一致的超时。
  • 可观测性:记录请求 ID、工具名称、延迟、结果状态和错误类别,但不要把令牌或敏感参数直接写入日志。
  • 扩缩容指标:除 CPU 和内存外,关注并发请求、排队时间、下游限流和工具调用延迟。
  • 故障测试:在请求期间主动终止实例,验证后续请求能由其他副本继续处理。

无状态 MCP 最有价值的地方,并不是少维护一张会话表,而是让远程 MCP 服务回到标准的云原生扩缩容模型。采用时应先移除对实例内存的正确性依赖,再关闭粘性路由,最后通过故障注入验证任意请求都能由任意健康实例处理。


相关推荐