AI 技术
#MCP#心跳检测#超时判定#SSE#长任务

MCP Ping 心跳与超时判定:长任务执行中连接为何被误判为断开

远程 MCP Server 执行长任务时,客户端常因 ping 超时误判连接断开并重复重连。本文围绕双向存活探测、请求超时与进度通知的区别,分析事件循环阻塞、SSE 流空闲和代理层超时如何制造假故障,并给出心跳间隔与可观测指标的配置思路。

一个被误判的断开

设想一个远程 MCP Server,它暴露了一个 build_report 工具。客户端调用后,服务端要读取几十个数据源、跑一次聚合,再生成报告,整个过程持续两分钟。客户端在调用发出后开始计时,同时按固定间隔发送 ping 请求。第一分钟一切正常,第二分钟开始,ping 的响应变慢,随后连续几次没有回音。客户端判定连接失效,关闭会话,重新初始化,再次调用工具。服务端那边第一次任务其实还在跑,于是同一份报告被生成两遍,资源被占了两份。

这类误判的根源在于:ping 探测的是“对端是否还能回应消息”,而长任务占用的可能是“对端是否还能及时处理消息”。两者在正常情况下重合,在事件循环被阻塞、SSE 流长时间空闲、代理层提前掐断连接时就会分叉。MCP 规范把 ping 定义为可选机制,并明确收到 ping 的一方必须及时返回空响应;如果发送方在合理超时内没收到响应,可以认为连接陈旧、终止连接或尝试重连。规范同时提醒,ping 频率应当可配置,超时应当匹配网络环境,并避免过度 ping。这些要求留出了工程判断空间,也留下了误判空间。

ping 在协议里到底承诺了什么

MCP 的 ping 是一个标准 JSON-RPC 请求,方法名为 ping,没有参数。接收方必须尽快返回一个空结果对象。客户端和服务端都可以发起 ping,也就是说存活探测是双向的:客户端可以确认服务端还在,服务端也可以确认客户端还在。

这个设计解决的是“连接是否还活着”,而不是“某个请求是否还在正常推进”。规范在生命周期一节里进一步区分了两类超时。第一类是每个已发送请求的超时:发送方应当为所有请求设置超时,防止挂死连接和资源耗尽;超时后应当为该请求发出取消通知,并停止等待响应。第二类是 ping 的超时:它判定的是连接健康,触发的是重连流程。

关键在于,规范允许实现者在收到与某请求对应的进度通知时重置该请求的超时时钟,因为进度通知意味着工作确实在推进。但它同时要求,无论收到多少进度通知,都应当始终执行一个最大超时,用来限制行为异常的对端造成的影响。这句话把“活跃”和“健康”分开了:进度通知证明任务在跑,ping 响应证明连接能通,两者不能互相替代。

一次长任务调用中消息如何流动

下面用一个贯穿全文的场景说明。客户端通过 Streamable HTTP 连接远程 MCP Server,调用 build_report,服务端选择用 SSE 流返回结果。

flowchart TD
    A[客户端发送 tools/call] --> B[服务端接受 POST 并建立 SSE 流]
    B --> C[服务端开始执行 build_report]
    C --> D{事件循环是否空闲}
    D -->|空闲| E[及时处理 ping 并返回空结果]
    D -->|阻塞| F[ping 排队等待]
    C --> G[周期性发送进度通知]
    G --> H[客户端重置请求超时]
    F --> I[客户端 ping 超时]
    I --> J[客户端判定连接陈旧]
    J --> K[关闭会话并重新初始化]
    K --> L[再次调用 build_report]
    E --> M[SSE 流最终返回结果]
    H --> M

图中的分叉点在第 D 步。服务端如果在跑同步计算、做阻塞式数据库查询,或者在一个单线程事件循环里执行 CPU 密集的解析,那么 ping 请求会排在任务后面。客户端看到的不是“服务端死了”,而是“服务端暂时没空回我”。

阻塞、空闲与代理层的三重误判

事件循环阻塞是最直接的一类。以 Node.js 或 Python asyncio 这类运行时为例,如果工具实现里有一段同步循环或一次阻塞调用,整个事件循环会被占住,期间到达的 ping 无法被调度处理。任务耗时越长,ping 排队越久,越容易超过客户端设定的阈值。

第二类是 SSE 流空闲。Streamable HTTP 允许服务端用 SSE 流式发送多条消息,也允许流在发送完所有响应后关闭。规范说明,除非会话到期,服务端不应在发完针对每个已接收 JSON-RPC 请求的响应之前关闭 SSE 流。但一个长任务在两次进度通知之间可能安静很久。如果客户端或中间层把“一段时间没有收到任何 SSE 事件”当作连接失效,就会在任务仍在推进时主动断开。规范还特别指出,断开连接不应被解释为客户端取消请求;要取消请求,客户端应当显式发送取消通知。这条规则说明,断流和取消在语义上是两件事,但很多实现把前者当成了后者。

第三类是代理层超时。反向代理、负载均衡器和网关通常有自己的空闲超时和读写超时。参考实现里可以看到,HTTP 传输的读超时和写超时默认是 30 秒,SSE 心跳默认也是 30 秒。这些默认值对短请求够用,对两分钟的长任务就可能偏紧。当代理在客户端和服务端之间先一步掐断连接,双方都收不到对方的 ping,于是各自判定对端失联。

这三类误判的共同结果是重复重连。客户端重连后重新调用工具,服务端如果没收到取消通知,旧任务仍在执行。重连次数越多,重复执行越严重。

心跳间隔与超时该怎么配

配置的核心是让 ping 超时明显大于“最坏情况下服务端处理一个 ping 的延迟”,同时让请求超时由进度通知来续期,而不是由 ping 来续期。

配置项作用对象建议取值思路配错后的表现
ping 间隔连接健康探测小于代理空闲超时,留出余量间隔过大时连接已被代理掐断才发现
ping 超时单次 ping 等待大于服务端最坏 ping 处理延迟设得过小,长任务期间频繁误判
请求超时单个业务请求覆盖任务正常耗时,可被进度通知重置设得过小,任务未完成就被取消
最大请求超时单个业务请求无论多少进度通知都必须触发不设上限时异常任务永久占用资源
SSE 心跳流空闲检测小于代理空闲超时流被中间层当作空闲连接关闭
会话超时整个会话大于最长任务加余量任务执行中会话被回收

这张表里最容易被忽略的是请求超时和 ping 超时的分工。ping 超时触发的是重连,请求超时触发的是取消。如果实现把长任务的存活判断完全交给 ping,那么事件循环一阻塞就会重连;如果完全交给请求超时,那么连接真的断了也不会被发现。

规范建议实现应当周期性发送 ping 来检测连接健康,频率应当可配置,并避免过度 ping 以降低网络开销。它还建议把 ping 失败记录下来用于诊断。这些建议指向同一个工程做法:ping 间隔和超时是两套独立参数,不要用同一个值。

进度通知与取消机制的分工

进度通知和 ping 解决的是不同问题。进度通知由服务端在任务推进时发出,携带与请求关联的信息,客户端收到后可以重置该请求的超时时钟。它证明的是“任务在推进”。ping 由任意一方发出,接收方必须尽快返回空结果,它证明的是“对端还能处理消息”。

取消机制则是第三条路径。当客户端确实想终止任务,应当发送取消通知,服务端据此停止工作并回收资源。规范在生命周期一节里说明,请求超时后发送方应当发出取消通知并停止等待响应。这意味着超时处理的标准动作是“取消加放弃”,而不是“直接重连”。重连只应在确认连接本身失效时使用。

把三者串起来看:进度通知维持请求存活,ping 维持连接存活,取消通知终止任务。任何一条路径缺失,都会导致长任务场景下的行为异常。缺少进度通知,客户端无法区分“任务在跑”和“任务卡死”;缺少 ping,连接断开无法及时发现;缺少取消,重连会带来重复执行。

生产环境该看哪些信号

长任务误判很难靠单一日志定位,需要几个信号交叉验证。

  • ping 往返延迟的分布,而不只是平均值。延迟突然抬升往往先于超时出现。
  • ping 超时次数与重连次数的比例。如果每次超时都伴随重连,说明重连策略过于激进。
  • 进度通知的间隔分布。间隔突然拉长,说明任务阶段变化或事件循环开始阻塞。
  • 同一工具在短时间内被重复调用的次数。这是重复执行的直接证据。
  • 代理层记录的连接关闭原因。区分是客户端主动关闭、服务端关闭还是代理超时关闭。
  • 会话创建与销毁的时间线。会话在任务执行中消失,说明会话超时或代理超时先触发。

定位顺序上,先看代理层是否先于客户端超时关闭连接,再看服务端事件循环是否在任务期间被占住,最后看客户端是否把断流当成了取消。这三步能覆盖大部分误判。

什么时候这套做法会失效

ping 加进度通知的组合并非万能。当服务端工具实现里存在无法让出事件循环的同步计算时,任何基于消息的探测都会延迟,此时只能靠把重活挪到独立进程或线程来缓解。当网络本身不稳定、丢包率较高时,ping 超时和真实断连难以区分,需要靠重连后的幂等处理来兜底,而不是靠调大超时。当代理层不支持 SSE 或会对流做缓冲时,进度通知可能被攒着一起发,客户端看到的间隔失去参考价值。

还有一个边界来自规范本身:ping 是可选机制。对端可能根本不实现 ping,或者只实现单向 ping。在这种情况下,连接健康的判断只能退回到传输层信号,例如 TCP 连接状态或 HTTP 请求是否还能建立。

与替代方案的取舍

把连接健康判断完全交给传输层,实现最简单,但无法发现“连接在、对端不响应”的情况。只用应用层 ping,能发现对端不响应,但会把事件循环阻塞误判为断连。只用进度通知,能反映任务推进,但任务阶段之间可能长时间无通知,仍会触发空闲超时。

工程上通常把三者叠加:传输层负责发现物理断连,ping 负责发现对端无响应,进度通知负责维持长请求的存活。代价是参数变多,需要为 ping 间隔、ping 超时、请求超时、最大请求超时和 SSE 心跳分别设定合理值,并让它们与代理层超时保持一致的量级关系。收益是长任务期间不会因为一次事件循环抖动就重连,也不会因为连接真的断了而无限等待。

尚未解决的问题是:如何在服务端确实卡死和只是暂时繁忙之间做出可靠区分。目前只能靠超时阈值和重试策略近似,没有协议层的确定性判据。

资料来源

  1. Model Context Protocol Specification - Utilities Ping
  2. Model Context Protocol Specification - Lifecycle
  3. 传输方式 | MCP 中文文档
  4. docs/TRANSPORT.md