MCP Server 实践之旅第 3 站:MCP 协议亲和性的技术内幕

作者:柳下,西流

背景

在分布式架构设计中,请求亲和性是实现有状态服务高可用的核心技术,通过将具备相同会话标识的请求智能路由至固定计算节点,保障会话连续性及缓存有效性。然而在 Serverless 范式下,函数计算服务因其瞬时实例生命周期、自动弹性扩缩容、无状态化运行等特性,在亲和性保障层面面临原生架构冲突。本文将以 MCP Server 在函数计算平台的深度集成为研究载体,解构基于 SSE 长连接通信模型,剖析会话亲和、优雅升级等关键技术,揭示 Serverless 架构在 MCP 场景中的亲和性创新实践。

概念介绍

在系列文章首篇 MCP Server 实践之旅第 1 站:MCP 协议解析与云上适配 我们深入解析了 MCP 以及 SSE 协议,为了方便本文的阅读, 这里再简单介绍下 MCP 以及 SSE 协议。

MCP:作为开放标准协议,为 AI 应用构建了通用化上下文交互框架。可以将 MCP 想象成 AI 应用程序的 USB-C 接口。就像 USB-C 为设备连接各种外设和配件提供了标准化方式一样,MCP 为 AI 模型连接不同的数据源和工具提供了标准化方式。

MCP Server&Client :MCP 通过 Server+Client 在 AI 应用程序和数据之间搭起了一座桥梁,MCP Server 负责打通 data、tools,AI 应用程序通过 MCP Client 连接 MCP Server。其中 Client 与 Server 的通信基于 SSE 协议实现,而 MCP 亲和奥秘也恰恰隐藏在 SSE 协议之中,下文将详细介绍。

SSE协议:作为 HTTP/1.1扩展协议,SSE(Server-Sent Events)定义了结构化流式传输规范,允许服务器实时向客户端推送事件或数据更新。在接收到订阅请求后,服务器会保持连接开启,并通过 HTTP 流向客户端推送事件,其中每个流数据格式如下:

plain 复制代码
event: <event-type>       // 事件类型(endpoint/message/close)
data: <payload>           // 通过数据字段传递实际内容
id: <message-id>          // 用于唯一标识事件
retry: <milliseconds>     // 重连退避策略

函数计算:函数计算是事件驱动的全托管计算服务。使用函数计算,您无需采购与管理服务器等基础设施,只需编写并上传代码或镜像。函数计算为您准备好计算资源,弹性地、可靠地运行任务,并提供日志查询、性能监控和报警等功能。

MCP交互协议解析

在解析 MCP 亲和性实现原理前,需重点解析其基于 SSE(Server-Sent Events)构建的通信框架。该协议通过定义标准化事件类型,实现了客户端-服务端的交互控制及会话保持机制,具体流程如下:

  1. 会话建立阶段:
    • 客户端发起初始 SSE 连接请求;
    • 服务端通过 event:'endpoint' 事件响应,在 data 字段中嵌入唯一会话标识(Session ID);
  2. 请求保持机制
    • 后续所有客户端请求必须携带该 Session ID;
    • 服务端通过该标识验证请求来源的合法性;
    • 实现客户端与服务端实例的绑定关联(Session Affinity);
  3. 实例绑定校验:
    • 当 messages 请求的路由目标实例与SSE连接绑定实例不一致时;
    • 服务端将触发安全校验失败机制,返回 4xx Conflict 错误代码;

该设计通过事件驱动架构确保了会话状态的连续性,同时通过实例绑定校验机制保障了分布式环境下的请求一致性。需要注意的是,Session ID 的有效期与 SSE 连接生命周期严格绑定,连接中断后需重新进行会话协商,一个简单的交互流程示例如下:

  1. Client 端发起一个 GET 请求,建立 SSE 长连接。(Connection1)
  2. Server 端回复event:endpoint类型的事件,将 sessionId 信息放入 data 中返回。(Connection1)
  3. Client 端使用第2步返回的 sessionId 信息发起首个 HTTP POST 请求。(Connection2)
  4. Server 端迅速响应202,但无内容。(Connection2)
  5. Server 端返回第3步请求的实际消息。(Connection1)
  6. Client 端使用第2步返回的 sessionId 发起 HTTP POST 请求initialized作为确认。(Connection3)
  7. Server 端迅速响应202,无内容。(Connection3)
  8. Client 端使用第2步返回的 sessionId 发起 HTTP POST 请求list tools。(Connection4)
  9. Server 端迅速响应202,无内容。(Connection4)
  10. Server 端返回第8步请求的实际消息,即工具列表。(Connection1)
  11. Client 端使用第2步返回的 sessionId 发起 HTTP POST 请求call tool。(Connection5)
  12. Server 端迅速响应202,无内容。(Connection5)
  13. Server 端返回第11步请求的实际消息,即工具调用结果。(Connection1)

亲和性机制解析

宏观分类与核心价值

系统亲和性主要分为两大维度:

  1. 节点亲和性:面向资源调度场景,确保工作负载优先部署至符合标签规则的节点。
  2. 会话亲和性:面向请求路由场景,保障客户端流量持续定向到特定后端实例。

两类机制均通过属性一致性调度实现核心价值:

  • 提升局部资源复用率(如缓存命中)
  • 保障有状态业务连续性
  • 满足合规性数据路由要求

会话亲和性实现范式

而常见的会话亲和性主要有以下几类:

  1. Cookie 植入模式:首请求时 LB、网关类服务注入含后端标识的 Set-Cookie 头,后续请求基于 Cookie 值进行会话绑定。适用 HTTP 无状态协议场景,且客户端缺乏显式标识信息。
  2. 源 IP 哈希模式:基于 ClientIP 哈希值映射到后端特定节点,满足 TCP/UDP 四层流量及需要客户端级会话保持的场景。
  3. Header 字段路由模式:预定义 Header 字段值提取(如 X-Session-ID),并哈希计算生成目标映射,支持多客户端标识共存场景,满足细粒度路由策略。

MCP SSE 亲和性架构特性

MCP SSE 采用双阶段协商机制:

  1. 会话建立阶段:MCP Server 生成全局唯一 SessionID 并同步至客户端
  2. 请求路由阶段:网关通过专有协议实时获取 Session-Node 映射关系

该模式需网关层与 MCP Server 间实现会话信息同步。相较于传统会话亲和方案,在获得精确路由控制能力的同时,需权衡协议交互带来的系统复杂度提升。

MCP ON FC 亲和调度设计

函数计算支持一键托管 MCP Server,并通过深度适配 MCP SSE 协议,提供了一种即开即用的 Serverless 亲和调度能力,帮助您实现 MCP 服务的 Serverless 托管能力,下面将详细介绍函数计算的亲和策略机制。

亲和策略

函数计算作为集调度、计算托管、免运维等特性于一身的 Serverless 服务,可将函数计算核心组件抽象为三部分:

  1. Gateway:网关层,用户流量入口,负责接收用户请求、鉴权、流控等功能。
  2. Scheduler:调度引擎层,负责将用户的请求调度到合适的节点和实例。
  3. VMS:资源层,函数执行环境 。

当客户通过函数计算托管 MCP 服务并通过 MCP Client 发起请求时,可将用户请求分为两类:SSE 管控链路和 Message 数据链路。
SSE 管控链路(会话初始化)

  1. Client 发起首个 SSE 请求路由到一台函数计算网关节点 Gateway1,网关节点权限校验通过后转发至调度模块 Scheduler。
  2. 调度模块根据特定标识识别出请求类型为 SSE 时,将调度到一台可用实例。
  3. 当请求和实例绑定时,实例将启动用户代码。
  4. 用户代码启动完成后,会通过event:endpoint事件将 sessionId 放入 data 中,返回第一个数据包。
  5. 在 response 返回经过 Gateway 网关层时,网关层将拦截 SSE 请求的首个回包,解析 SessionID 信息,并将 SessionID 和实例的映射关系持久化到DB。

Message 数据链路(请求处理)

  1. Client 完成SSE请求后,将发起多个 Message 请求,由于函数计算网关节点无状态,Message 请求将打散到多个网关节点。
  2. 当 Gateway 收到 Message 请求,将检查网关节点 cache 中是否存在 Message 请求携带的 SessionID 亲和信息,如果 cache 中无记录,将回源到 DB 获取相关数据。
  3. Gateway 通过 cache 或 DB 拿到 SessionID 和实例的绑定关系时,将携带相关信息转发至调度模块。
  4. 调度模块根据特定标识识别出请求类型为 Message 时,解析携带的实例信息,将请求定向调度到特定实例。
  5. 当请求和实例绑定时,MCP Server 校验请求通过,将返回202通知 Client 请求接收成功,实际数据将通过 SSE 请求建立的连接返回。

函数计算通过无状态网关层与智能调度层的协同设计,在 Serverless 架构下创新实现了 MCP SSE 会话亲和性保障。SSE 管控链路借助首包拦截实现 SessionID 与实例的动态绑定,Message 数据链路则通过多级缓存与持久化存储确保请求精准路由。该架构既保留了函数计算的弹性优势,又攻克了无状态服务处理有状态请求的难题,为 MCP 场景提供了高可靠、低延迟的 Serverless 化解决方案,同时通过冷启动优化与智能扩缩容机制,实现资源效率与性能的最佳平衡。

MCP 场景会话配额控制体系

配额冲突建模分析

在 MCP 会话资源需求模型中,单个 Session 生命周期内存在两类并发需求:

而这种并发需求在传统配额分配中存在严重缺陷,需要引入一种动态预留的配额分配策略:

动态配额分配策略

为避免上述问题,函数计算引入 Session Quota 策略,即结合函数实例的并发度配置,限制每个实例最多绑定 Round (函数单实例多并发配置 / 10) 个 Session。如下流程所示:

  1. 当函数配置了20并发时,可服务 20/10=2 个 Session 请求。
  2. Client1 发起 SSE 请求时,分配了VM1实例,并占用1 Session Quota。
  3. Client2 发起 SSE 请求时,Scheduler 计算 VM1 仍有1 Session Quota,成功和 VM1 再次完成绑定。
  4. Client3 发起 SSE 请求时,Scheduler 计算 VM1 2个并发 Session Quota 已被2个 SSE 请求占用,无法再次绑定,则调度到新实例 VM2,完成实例绑定。

MCP 会话场景灰度优雅升级方案

函数计算支持 UpdateFunction 操作更新函数配置,在用户更新函数后,新的请求将路由到新配置拉起的实例,旧实例不再接收新请求,在处理完存量请求后后台自动销毁。如下图在 UpdateFunction 前,请求1-n路由到 VM1,UpdateFunction 后新请求路由到 VM2,VM1 在处理完存量请求后自动销毁。

在 MCP 场景下,数据请求从请求级无状态变为会话级绑定,在 UpdateFunction后,如果存量 Session 关联的请求路由到新实例,则新增无法识别到 SessionID 信息,返回错误。为解决这类问题,函数计算优雅更新能力从升级至有状态 Session 级别,在用户更新函数后,存量 Session 关联的请求仍路由到旧实例,新建 Session 请求路由至新实例,优雅实现 MCP 亲和场景下的升级需求。

压测

压测准备

我们以一个简单的 MCP Server 服务托管到函数计算作为压测对象, 函数代码如下:

python 复制代码
import random
import asyncio
from mcp.server.fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.routing import Mount

mcp = FastMCP("My App")

@mcp.tool()
async def add(a: int, b: int) -> int:
    """计算两个整数的和(含150-1000ms随机延迟)"""
    delay = random.uniform(0.15, 1.0)
    await asyncio.sleep(delay) 

    print(f"add工具被调用,延迟 {delay:.3f}s")
    return a + b

app = Starlette(
    routes=[
        Mount("/", app=mcp.sse_app()),
    ]
)

压测脚本 load_test.py 如下:

python 复制代码
# 并发 100 个 mcp client
# 每次 client 建立一条 SSE 连接, 执行 call tool
# 并且校验 call tool 的结果是符合预期的

import asyncio
from mcp.client.sse import sse_client
from mcp import ClientSession
import logging
import time
import random
import traceback

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("LoadTestClient")

class ErrorCounter:
    def __init__(self):
        self.count = 0
        self._lock = asyncio.Lock()

    async def increment(self):
        async with self._lock:
            self.count += 1

err_counter = ErrorCounter()

async def robust_client_instance(instance_id: int):
    try:
        async with sse_client(
            "https://xxx-yyy-zzz.cn-hangzhou.fcapp.run/sse",
            headers={
                "Authorization": "Bearer YOUR_API_KEY",
                "X-Instance-ID": str(instance_id),
            },
            timeout=10
        ) as streams:
            if streams is None:
                raise ConnectionError("SSE连接失败")

            async with ClientSession(
                read_stream=streams[0],
                write_stream=streams[1],
            ) as session:
                # 初始化及调用逻辑
                start = time.time()
                await asyncio.wait_for(session.initialize(), timeout=10.0)
                logger.info(f"实例{instance_id}; initialize 耗时: {time.time() - start}")

                try:
                    # 设置call_tool超时
                    start = time.time()
                    random_number = random.randrange(1, 51)
                    result = await asyncio.wait_for(
                        session.call_tool(
                            "add", {"a": instance_id, "b": random_number}
                        ),
                        timeout=10.0,
                    )
                    logger.info(
                        f"实例{instance_id};  call_tool 耗时: {time.time() - start}"
                    )

                    if result.isError:
                        logger.error(
                            f"实例{instance_id}, call_tool 调用失败: {result.content}"
                        )
                        raise Exception(
                            f"实例{instance_id}, call_tool 调用失败: {result.content}"
                        )
                    else:
                        logger.info(
                            f"实例{instance_id}, call_tool 调用成功: {result.content}"
                        )
                        assert (
                            int(result.content[0].text)
                            == instance_id + random_number
                        )
                except asyncio.TimeoutError:
                    logger.error(f"实例{instance_id} 操作超时")


    except asyncio.TimeoutError:
        await err_counter.increment()
        logger.error(f"实例{instance_id} 操作超时")
    except Exception as e:
        await err_counter.increment()
        logger.error(f"实例{instance_id} 失败: {str(e)}")
        logger.debug(traceback.format_exc())

async def main():
    BATCH_SIZE = 100
    tasks = [robust_client_instance(j) for j in range(0, BATCH_SIZE)]
    await asyncio.gather(*tasks, return_exceptions=True)

    print(f"总错误数: {err_counter.count}")

if __name__ == "__main__":
    asyncio.run(main())

压测结果:

执行压测命令, 并行启动 3 个压测进程, 每次压测进程并发启动 100 mcp client, 每次 client 建立一条 SSE 连接, 执行 call tool, 并且校验 call tool 的结果是符合预期的

bash 复制代码
python load_test.py & python load_test.py & python load_test.py & wait

3 个压测进程, 每个进程中的 100 个并发的 mcp client 全部成功执行

表示 SSE 长连接和后续配合该会话的 HTTP 请求(call tool)在同一个函数实例,实现亲和行为

MCP Server 函数实例从 0 毫秒级扩容到 15 个实例,实现有状态实例水平横向扩展

根据官方文档 MCP SSE亲和性调度 描述,该函数设置的单实例并发度为 200,那么单个实例支持的 session 数目(即 SSE 长连接个数为 20个), 300/20 = 15 实例

测试更新函数, MCP 会话灰度优雅升级

我们尝试在持续压测过程中,中途更新函数, 所有的 mcp client 的行为都符合预期无报错。

我们使用如下脚本模拟持续压测:

python 复制代码
...
# 压测(分50批)
BATCH_SIZE = 100
for i in range(0, 5000, BATCH_SIZE):
    tasks = [robust_client_instance(j) for j in range(0, BATCH_SIZE)]
    await asyncio.gather(*tasks, return_exceptions=True)
    await asyncio.sleep(1)  # 批次间间隔

总结

通过系列技术方案的精妙设计,函数计算为 MCP 场景构筑了 Serverless 化的完整技术基座。在 Serverless 服务范式与有状态业务需求之间架起智能桥梁,通过会话亲和调度引擎、优雅会话升级、动态配额熔断等机制,让 Serverless 的极致弹性与有状态服务的强一致性实现了深度融合。

相关推荐
国际云,接待13 小时前
甲骨文云服务器适合做网站吗
服务器·云原生·架构·云计算·量子计算
浮桥13 小时前
uniapp实现大视频文件上传-- 阿里云oss直传方式 - app、H5、微信小程序
阿里云·微信小程序·uni-app
是垚不是土13 小时前
云原生微服务的前世今生
微服务·云原生·架构
阿里云云原生15 小时前
阿里巴巴 MCP 分布式落地实践:快速转换 HSF 到 MCP server
分布式·阿里云·云原生·higress
掘金-我是哪吒1 天前
分布式微服务系统架构第138集:打包发布全流程(iOS + Android)
android·微服务·云原生·架构
汤姆丁11111 天前
阿里云web端直播(前端部分)
阿里云·云计算
chunmiao30321 天前
理解阿里云的MQTT
阿里云·云计算
Akamai中国1 天前
GPU加速Kubernetes集群助力音视频转码与AI工作负载扩展
人工智能·云原生·容器·kubernetes·云计算·音视频
XINVRY-FPGA1 天前
Xilinx XCAU10P-2FFVB676I 赛灵思 Artix UltraScale+ FPGA
嵌入式硬件·安全·阿里云·ai·fpga开发·云计算·fpga