作者:柳下,西流
背景
在分布式架构设计中,请求亲和性是实现有状态服务高可用的核心技术,通过将具备相同会话标识的请求智能路由至固定计算节点,保障会话连续性及缓存有效性。然而在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流向客户端推送事件,其中每个流数据格式如下:
event: <event-type> // 事件类型(endpoint/message/close)
data: <payload> // 通过数据字段传递实际内容
id: <message-id> // 用于唯一标识事件
retry: <milliseconds> // 重连退避策略
函数计算:函数计算是事件驱动的全托管计算服务。使用函数计算,您无需采购与管理服务器等基础设施,只需编写并上传代码或镜像。函数计算为您准备好计算资源,弹性地、可靠地运行任务,并提供日志查询、性能监控和报警等功能。
MCP交互协议解析
在解析MCP亲和性实现原理前,需重点解析其基于SSE(Server-Sent Events)构建的通信框架。该协议通过定义标准化事件类型,实现了客户端-服务端的交互控制及会话保持机制,具体流程如下:
会话建立阶段:
- 客户端发起初始SSE连接请求;
- 服务端通过event:'endpoint'事件响应,在data字段中嵌入唯一会话标识(Session ID);
请求保持机制
- 后续所有客户端请求必须携带该Session ID;
- 服务端通过该标识验证请求来源的合法性;
- 实现客户端与服务端实例的绑定关联(Session Affinity);
实例绑定校验:
- 当messages请求的路由目标实例与SSE连接绑定实例不一致时;
- 服务端将触发安全校验失败机制,返回4xx Conflict错误代码;
该设计通过事件驱动架构确保了会话状态的连续性,同时通过实例绑定校验机制保障了分布式环境下的请求一致性。需要注意的是,Session ID的有效期与SSE连接生命周期严格绑定,连接中断后需重新进行会话协商,一个简单的交互流程示例如下:
- Client端发起一个 GET 请求,建立SSE长连接。(Connection1)
- Server端回复
event:endpoint
类型的事件,将sessionId信息放入data 中返回。(Connection1) - Client端使用第2步返回的sessionId信息发起首个HTTP POST 请求。(Connection2)
- Server端迅速响应202,但无内容。(Connection2)
- Server端返回第3步请求的实际消息。(Connection1)
- Client端使用第2步返回的sessionId发起HTTP POST请求
initialized
作为确认。(Connection3) - Server端迅速响应202,无内容。(Connection3)
- Client端使用第2步返回的sessionId发起HTTP POST请求
list tools
。(Connection4) - Server端迅速响应202,无内容。(Connection4)
- Server端返回第8步请求的实际消息,即工具列表。(Connection1)
- Client端使用第2步返回的sessionId发起HTTP POST请求
call tool
。(Connection5) - Server端迅速响应202,无内容。(Connection5)
- Server端返回第11步请求的实际消息,即工具调用结果。(Connection1)
亲和性机制解析
宏观分类与核心价值
系统亲和性主要分为两大维度:
- 节点亲和性:面向资源调度场景,确保工作负载优先部署至符合标签规则的节点。
- 会话亲和性:面向请求路由场景,保障客户端流量持续定向到特定后端实例。
两类机制均通过属性一致性调度实现核心价值:
- 提升局部资源复用率(如缓存命中)
- 保障有状态业务连续性
- 满足合规性数据路由要求
会话亲和性实现范式
而常见的会话亲和性主要有以下几类:
- Cookie植入模式:首请求时LB、网关类服务注入含后端标识的Set-Cookie头,后续请求基于Cookie值进行会话绑定。适用HTTP无状态协议场景,且客户端缺乏显式标识信息。
- 源IP哈希模式:基于ClientIP哈希值映射到后端特定节点,满足TCP/UDP四层流量及需要客户端级会话保持的场景。
- Header字段路由模式:预定义Header字段值提取(如X-Session-ID),并哈希计算生成目标映射,支持多客户端标识共存场景,满足细粒度路由策略。
MCP SSE亲和性架构特性
MCP SSE采用双阶段协商机制:
- 会话建立阶段:MCP Server生成全局唯一SessionID并同步至客户端
- 请求路由阶段:网关通过专有协议实时获取Session-Node映射关系
该模式需网关层与MCP Server间实现会话信息同步。相较于传统会话亲和方案,在获得精确路由控制能力的同时,需权衡协议交互带来的系统复杂度提升。
MCP ON FC 亲和调度设计
函数计算支持一键托管MCP Server,并通过深度适配MCP SSE协议,提供了一种即开即用的Serverless亲和调度能力,帮助您实现MCP服务的Serverless托管能力,下面将详细介绍函数计算的亲和策略机制。
亲和策略
函数计算作为集调度、计算托管、免运维等特性于一身的Serverless服务,可将函数计算核心组件抽象为三部分:
- Gateway:网关层,用户流量入口,负责接收用户请求、鉴权、流控等功能。
- Scheduler:调度引擎层,负责将用户的请求调度到合适的节点和实例。
- VMS:资源层,函数执行环境 。
当客户通过函数计算托管 MCP 服务并通过 MCP Client 发起请求时,可将用户请求分为两类:SSE管控链路和Message数据链路。
SSE管控链路(会话初始化)
- Client 发起首个 SSE 请求路由到一台函数计算网关节点Gateway1,网关节点权限校验通过后转发至调度模块Scheduler。
- 调度模块根据特定标识识别出请求类型为 SSE时,将调度到一台可用实例。
- 当请求和实例绑定时,实例将启动用户代码。
- 用户代码启动完成后,会通过
event:endpoint
事件将sessionId放入data中,返回第一个数据包。 - 在 response 返回经过 Gateway 网关层时,网关层将拦截 SSE 请求的首个回包,解析SessionID信息,并将 SessionID 和实例的映射关系持久化到DB。
Message数据链路(请求处理)
- Client 完成SSE请求后,将发起多个Message请求,由于函数计算网关节点无状态,Message请求将打散到多个网关节点。
- 当Gateway收到Message请求,将检查网关节点cache中是否存在Message请求携带的SessionID亲和信息,如果cache中无记录,将回源到DB获取相关数据。
- Gateway通过cache或DB拿到SessionID和实例的绑定关系时,将携带相关信息转发至调度模块。
- 调度模块根据特定标识识别出请求类型为Message时,解析携带的实例信息,将请求定向调度到特定实例。
- 当请求和实例绑定时,MCP Server 校验请求通过,将返回202通知Client请求接收成功,实际数据将通过 SSE请求建立的连接返回。
函数计算通过无状态网关层与智能调度层的协同设计,在Serverless架构下创新实现了MCP SSE会话亲和性保障。SSE管控链路借助首包拦截实现SessionID与实例的动态绑定,Message数据链路则通过多级缓存与持久化存储确保请求精准路由。该架构既保留了函数计算的弹性优势,又攻克了无状态服务处理有状态请求的难题,为MCP场景提供了高可靠、低延迟的Serverless化解决方案,同时通过冷启动优化与智能扩缩容机制,实现资源效率与性能的最佳平衡。
MCP场景会话配额控制体系
配额冲突建模分析
在MCP会话资源需求模型中,单个Session生命周期内存在两类并发需求:
而这种并发需求在传统配额分配中存在严重缺陷,需要引入一种动态预留的配额分配策略:
动态配额分配策略
为避免上述问题,函数计算引入Session Quota策略,即结合函数实例的并发度配置,限制每个实例最多绑定Round(函数单实例多并发配置 / 10) 个Session。如下流程所示:
- 当函数配置了20并发时,可服务 20/10=2 个 Session 请求。
- Client1发起SSE请求时,分配了VM1实例,并占用1 Session Quota。
- Client2发起SSE请求时,Scheduler计算VM1 仍有1 Session Quota,成功和VM1再次完成绑定。
- 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 服务托管到函数计算作为压测对象, 函数代码如下:
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 如下:
# 并发 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 的结果是符合预期的
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 的行为都符合预期无报错。
我们使用如下脚本模拟持续压测:
...
# 压测(分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的极致弹性与有状态服务的强一致性实现了深度融合。
更多内容关注 Serverless 微信公众号(ID:serverlessdevs),汇集 Serverless 技术最全内容,定期举办 Serverless 活动、直播,用户最佳实践。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。