消息落错了副本:Higress MCP 网关的 SSE 会话路由与长连接保活机制
2026/9/18 3:36:13 网站建设 项目流程

消息落错了副本:Higress MCP 网关的 SSE 会话路由与长连接保活机制

【免费下载链接】higress🤖 AI Gateway | AI Native API Gateway项目地址: https://gitcode.com/GitHub_Trending/hi/higress

Higress 的 MCP 网关能力里,SSE 传输依赖一条不断开的 HTTP 长连接,而网关副本是无状态的,且通常不止一台。本文拆解 Higress 如何用 Redis 发布订阅把会话路由拆开,让任意副本都能处理 MCP 消息。

一次 tools/call 为什么可能落到没有会话的副本

客户端先发起 GET /sse,负载均衡把这条长连接分给了副本 A,会话上下文只存在于 A 的进程内存里。几秒后,客户端发起 POST /message?sessionId=xxx 携带工具调用请求,这一次负载均衡按常规策略挑选上游,很可能落到副本 B。B 能正常执行工具,但按 MCP 的 SSE 传输规范,结果必须从 A 上那条打开的 SSE 流推给客户端。

这正是它和普通 HTTP API 代理的本质区别:普通请求是自包含的,任何副本都能独立完成;MCP SSE 模式把一次逻辑交互拆成两条互不相关的 HTTP 请求——建连一条、发消息一条——两条请求必须跨副本互相找到。B 手里的"会话在哪"这个问题,不借助进程外的设施无解。

为什么不让负载均衡记住这条连接

可选项有三条。粘性会话(按 Cookie 或源 IP 哈希)把会话生命周期绑死在某个进程上,副本扩缩容、滚动发布都会打断存量连接,也和 Envoy 集群负载均衡的默认行为相抵触。把会话状态写成 KV 记录则不对症:SSE 的"状态"本质是一条消息通道而不是一份可读写的数据,存取开销和延迟都高于直接推送。第三条路是共享消息总线:每个会话对应一个频道名,持有连接的副本订阅频道,执行了工具的副本把结果发到频道,进程彻底无状态化,副本可随意增删替换——代价是引入一个 Redis 依赖和多一跳发布开销。

Higress 选了第三条。取舍的边界在代码里写得很直白:没有配置 Redis 时,SSE 端点直接返回 "Redis is not enabled, SSE connection is not supported",而 streamable HTTP 模式不需要会话状态,承担无 Redis 场景的降级路径。

会话建立:每个 SSE 连接独享一个频道

触发条件:客户端 GET /sse(路径后缀由 sse_path_suffix 指定,缺失时配置解析直接报错)。

执行过程:mcp-session 过滤器在头阶段生成一个 uuid 作为 sessionID,并为此请求新建一个 SSEServer 实例。源码注释解释了为什么不能复用全局实例:MCPServer 本身线程安全可以共享,但 SSEServer 持有请求特有的 messageEndpoint,跨连接复用会串路。随后它按mcp-server-sse:<sessionID>拼出频道名,在 Redis 上订阅该频道,并通过 Envoy 的 InjectData 接口把首个 endpoint 事件推给客户端——事件里携带的就是拼好 sessionId 查询参数的消息端点 URL。此时过滤器返回 api.Running 表示响应流未结束,SSE 连接保持打开;另有一个协程每 5 秒向频道发布一条 ping 的 JSON-RPC 请求,防止中间层超时掐断空闲连接。

channel := GetSSEChannelName(sessionID) // "mcp-server-sse:<sessionID>" initialEvent := fmt.Sprintf("event: endpoint\ndata: %s\n\n", messageEndpoint)

配置入口:mcp-session 过滤器配置的 redis 字段(address、username、password、db、secret)与 sse_path_suffix。secret 仅用于 AES 加密写入 Redis 的存储值,与发布订阅路径无关。这段逻辑见 SSE 会话与频道订阅。

这里埋着整个设计的唯一"亲和信息":sessionId 被内嵌进下发给客户端的 URL,此后所有消息请求都携带它,于是任何副本都能从查询参数反推出频道名。进程内存里不存任何"哪个会话在哪台机器"的映射。

消息投递:POST /message 在任意副本上执行

触发条件:客户端 POST /message?sessionId=xxx。这里 sessionId 设计成查询参数而非请求头,是为了让消息端点可以直接写进 endpoint 事件、被客户端原样复用。

执行过程分两种链路。内置 MCP 服务器(golang-filter 形态的 mcp-server)在进程内执行工具,请求体被缓冲完整后交给 HandleMessage,结果直接写回 HTTP 响应,SSE 侧的推送由会话机制兜底,见 mcp-server 过滤器。代理外部 MCP 服务器的场景更依赖跨副本路由:mcp-session 过滤器按 match_list 匹配出上游类型,收到上游响应体后,若 URL 携带 sessionId,就把响应体包成event: message发布到对应频道,持有长连接的那台副本经订阅回调 InjectData 推给客户端;HTTP 响应与 SSE 推送是同一份数据,前者兼容只看 HTTP 响应的客户端,后者才是协议真正的主通道。这条发布逻辑在 mcp-session 过滤器 的响应编码路径里,同一文件还处理第三种上游:原生 SSE 协议的 MCP 服务器,其首个 endpoint 事件会被 rewriteEndpointUrl 改写成网关对外路径,防止客户端绕过网关直连后端。

连接保活与故障恢复

触发条件:网络抖动、副本重启、Redis 断连。

Redis 客户端在后台协程里每 5 秒 Ping 一次,探测到失败就关闭旧连接并按原配置重建,重建期间已有会话的订阅会随新连接恢复;会话侧,请求结束或被中断时 OnDestroy 关闭 stopChan,订阅协程退出并清理 pubsub 资源,频道映射同步删除。所有会触碰 Envoy 回调的协程都包了 recover 与 RecoverPanic,单个 panic 不会击穿 Envoy worker。实现见 Redis 客户端与保活逻辑。

边界要认清:Redis 断连期间,5 秒一次的 ping 发布失败只产生错误日志,SSE 连接本身不掉,但断连窗口内的工具结果会丢失,客户端只能靠 ping 中断感知异常。恢复后旧会话无需重建,这一点和粘性会话方案形成对照。

测试如何覆盖这些路径

单元测试集中在会话解析与流处理的边角上:SSE 事件行结束符的 \r\n 组合按 HTML 规范逐一验证,endpoint 事件在流中被截断、跨缓冲块到达的情况由缓存拼接逻辑覆盖,上游响应非 SSE 类型时的跳过路径也有对应断言。端到端层面,Higress 在 test/e2e 下搭了真实集群的验证框架,conformance 用例按 Gateway API 规范组织,整体结构如下:

运维视角上,5 秒一次的 ping 机制天然兼做连接健康信号:监控里"某会话 ping 停发"可以作为会话失效的判定基准,比依赖连接断开事件更及时。

多副本部署 MCP 网关时的检查项

Redis 可用性排第一:发布订阅不落盘,Redis 故障不影响已返回的 HTTP 响应,但会阻断所有新建会话和工具结果推送,生产环境至少上主从或集群模式,并把"Redis ping 失败"日志纳入告警。

不要给网关前配置粘性会话:前置负载均衡用任意均衡策略即可;若强行按源 IP 亲和,等于把状态性又绑回进程,故障域原样返回。

无 Redis 的场景走 streamable HTTP 降级:代码中 sessionId 为空时 HandleMessage 返回 200 并把结果直接写进 HTTP 响应,适合不需要服务端主动推送的调用方。

启用用户级服务器(enable_user_level_server)时注意两点:/config 端点仅对集群内网 IP 放行;rate_limit 的 limit 与 window 是针对 uid 的唯一限流位,未命中白名单的匿名调用方全部受其约束。

协议类型上,网关支持的 MCP 传输在 MCP 模型定义 里以常量列出,mcp-sse 与 mcp-streamable 之外还覆盖 stdio、dubbo 等形态,选型时先确认上游实际协议再决定是否需要 Redis 依赖。

对需要在网关层托管 MCP 接入的团队,这套机制给出的参考答案是:把长连接的会话性拆成一个可推导的频道名,进程保持无状态,代价交给一个可独立扩容的 Redis。理解了这个拆法,后续无论是排查"结果偶发丢失"还是评估 streamable 迁移,都有了明确的判断坐标。

【免费下载链接】higress🤖 AI Gateway | AI Native API Gateway项目地址: https://gitcode.com/GitHub_Trending/hi/higress

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询