ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

解决 Spring AI MCP Server SSE 连接并发问题:TaoToken 统一 Key 通道下的配置与验证

解决 Spring AI MCP Server SSE 连接并发问题:TaoToken 统一 Key 通道下的配置与验证 1. 从一次 20 并发就崩的 MCP Server 说起Spring AI MCP Server 是 Spring AI 生态里用来把本地工具、资源、提示词暴露成标准 MCP 协议服务的组件客户端通过 SSE 长连接订阅消息。它适合谁适合正在用 Spring Boot 写 AI Agent 后端、需要把内部能力以 MCP 形式接给 Claude Code、Cursor 这类客户端的开发者。问题也恰恰出在 SSE 这个长连接上当并发连接数爬到 20 左右日志里开始刷BufferOverflowException紧接着newPosition limit: (7894 300)连接成片断开客户端重连又堆上来最后整个服务像被堵住的水管。我最初以为是消息体太大把 Tomcat 的socket.appWriteBufSize、outputBufferSize一路从 64KB 加到 512KB结果错误照旧。后来才想明白Servlet 的阻塞 I/O 模型下每个 SSE 连接都要占一个线程和一份独立缓冲区消息分块和 flush 时机由容器控制长连接场景下缓冲区位置越界几乎是必然。真正的解法不是继续调参而是换掉底层传输模型同时把上游模型的调用通道收敛成统一 Key避免多 Key 轮换带来的额外连接抖动。这篇就把配置骨架、并发压测和连接复用验证一次讲清。2. TaoToken 统一 Key 通道的前置准备在动手改 MCP Server 之前先把模型调用这一层理顺。MCP Server 本身不产生模型能力它转发的是工具调用和消息但很多场景下服务端还要回调模型做摘要、路由或补全。如果每个实例、每个环境各配一把 Key压测时很容易把并发问题误判成鉴权问题。TaoToken 在这里的作用是提供统一的 API 通道一个 Key 覆盖多模型调用减少配置分叉。你需要先拿到一把可用的 Key。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 API Key入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。Key 的管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 建议按环境建不同 Key方便压测时单独观察。接口基地址统一用 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为base_url写进配置即可。如果你用的是 Claude Code 这类客户端Anthropic 兼容入口的说明在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的接入示例。想先验证 Key 是否可用可以直接在模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 发一条消息确认返回正常再往下走。注意Key 只放在服务端环境变量或配置中心不要写进前端或提交到仓库。压测脚本里用环境变量读取避免泄露。3. 可复制的 config.toml 与 settings.json 骨架下面这套配置分成两块一块是 MCP Server 自身的运行参数用config.toml管理一块是客户端侧连接 MCP Server 的声明用settings.json。两者配合才能复现并发场景。先看config.toml重点是切到 WebFlux 后不再需要 Tomcat 那堆缓冲区参数但连接生命周期和线程模型仍要显式声明# config.toml —— MCP Server 运行配置 [server] port 8080 # SSE 长连接读超时设长一些避免压测中途被掐断 read-timeout 300s write-timeout 300s idle-timeout 600s [transport] # 使用 WebFlux 响应式传输替代 webmvc type webflux # SSE 心跳间隔保持连接活跃防止中间层回收 sse-heartbeat-interval 15s # 单条消息最大字节数配合背压使用 max-message-size 1048576 [model] # TaoToken 统一通道 base-url https://taotoken.net/api api-key ${TAOTOKEN_API_KEY} # 连接池上限压测时观察是否成为瓶颈 max-connections 500 connect-timeout 10s [concurrency] # 事件循环线程数Netty 默认是 CPU 核数压测可适当上调 event-loop-threads 8 # 背压队列容量超出后触发流控而非缓冲区溢出 backpressure-queue-size 1024对应的 Maven 依赖要确保只保留 WebFlux 版本两个 starter 同时存在会引发自动配置冲突!-- pom.xml 片段 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webflux/artifactId /dependency !-- 确认没有引入 spring-ai-starter-mcp-server-webmvc --再看客户端侧的settings.json这里声明 MCP Server 地址和连接复用策略{ mcpServers: { spring-ai-mcp: { url: http://127.0.0.1:8080/sse, transport: sse, headers: { Authorization: Bearer ${TAOTOKEN_API_KEY} }, reconnect: { enabled: true, maxAttempts: 5, initialDelayMs: 500, maxDelayMs: 5000 }, keepAlive: { enabled: true, intervalMs: 15000 } } } }reconnect和keepAlive这两段是并发场景下的关键。没有心跳中间层会在空闲时回收连接客户端重连瞬间形成脉冲正好把服务端打到缓冲区边界。有了心跳和退避重连连接数曲线会平滑很多。4. 并发压测与连接复用验证配置改完不能只看日志不报错就收工得用压测把并发拉起来同时观察连接是否被复用。我用的是一段简单的 Java 压测代码模拟 200 个并发 SSE 连接每个连接保持 60 秒并周期性接收心跳// SseConcurrencyTest.java import java.net.http.*; import java.net.URI; import java.time.Duration; import java.util.concurrent.*; import java.util.concurrent.atomic.*; public class SseConcurrencyTest { private static final int CONCURRENCY 200; private static final AtomicInteger success new AtomicInteger(); private static final AtomicInteger failed new AtomicInteger(); public static void main(String[] args) throws Exception { HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .executor(Executors.newFixedThreadPool(64)) .build(); ExecutorService pool Executors.newFixedThreadPool(CONCURRENCY); CountDownLatch latch new CountDownLatch(CONCURRENCY); for (int i 0; i CONCURRENCY; i) { pool.submit(() - { try { HttpRequest req HttpRequest.newBuilder() .uri(URI.create(http://127.0.0.1:8080/sse)) .header(Authorization, Bearer System.getenv(TAOTOKEN_API_KEY)) .header(Accept, text/event-stream) .timeout(Duration.ofSeconds(90)) .GET() .build(); HttpResponseString resp client.send(req, HttpResponse.BodyHandlers.ofString()); if (resp.statusCode() 200) { success.incrementAndGet(); } else { failed.incrementAndGet(); } } catch (Exception e) { failed.incrementAndGet(); } finally { latch.countDown(); } }); } latch.await(120, TimeUnit.SECONDS); System.out.println(success success.get() , failed failed.get()); pool.shutdown(); } }跑之前先确认服务端已启动然后执行export TAOTOKEN_API_KEY你的Key mvn -q compile exec:java -Dexec.mainClassSseConcurrencyTest实测下来切到 WebFlux 之前这个脚本跑到 20 左右就开始出现failed增长服务端日志同步刷BufferOverflowException。切到 WebFlux 并加上心跳配置后200 并发下success200, failed0连接建立后稳定保持没有出现重连风暴。连接复用怎么验证在服务端开一个连接计数端点或者直接看 Netty 的活跃连接指标。简单做法是在WebFilter里打点// McpSseWebFluxConfig.java Bean public WebFilter connectionMetricsFilter() { AtomicInteger active new AtomicInteger(); return (exchange, chain) - { String path exchange.getRequest().getPath().value(); if (path.endsWith(/sse)) { int now active.incrementAndGet(); log.info(SSE active connections: {}, now); return chain.filter(exchange) .doFinally(sig - log.info(SSE closed, active: {}, active.decrementAndGet())); } return chain.filter(exchange); }; }压测时观察这个计数如果它稳定在 200 附近而不是反复上下跳说明连接被复用、没有频繁重建。如果计数持续高于并发数说明旧连接没释放需要检查客户端keepAlive和服务端idle-timeout是否匹配。5. 本篇常见错排查错误一BufferOverflowException在压测中仍偶发。先确认pom.xml里没有残留 webmvc starter两个 starter 共存时自动配置可能仍走 Servlet 路径。用mvn dependency:tree | grep mcp-server检查只应出现 webflux 版本。错误二newPosition limit出现在启动阶段而非压测阶段。这通常是max-message-size设得比实际消息小或者心跳消息和业务消息共用缓冲区。把max-message-size调到 1MB 以上并确认心跳走独立通道。错误三并发上不去连接数卡在某个值。检查event-loop-threads和操作系统文件描述符上限。Netty 事件循环线程数默认等于 CPU 核数压测机核数少时会成为瓶颈可临时上调到 8 或 16。同时用ulimit -n确认 fd 上限200 并发至少需要 1024。错误四客户端反复重连服务端连接数脉冲式上涨。这是心跳间隔和中间层空闲超时不匹配。把sse-heartbeat-interval设得比中间层 idle 超时小一半比如中间层 60 秒回收心跳就设 15 到 20 秒。错误五鉴权失败被误判成并发问题。压测时如果大量 401先单独用 curl 验证 Keycurl -N -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Accept: text/event-stream \ http://127.0.0.1:8080/sse能正常收到事件流再跑压测。Key 相关的问题可以在 API Keys 页 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 核对状态和额度。6. 把通道和传输一起收敛回头看这次问题的本质是两层叠加传输层用阻塞模型扛长连接调用层用多 Key 分散配置导致排障时变量太多。把 MCP Server 切到 WebFlux 解决的是传输层的背压和缓冲区问题把模型调用收敛到 TaoToken 统一 Key 通道解决的是配置层的分叉问题。两者都收敛之后压测数据才干净200 并发稳定不掉线。如果你还在调 Tomcat 缓冲区参数建议直接停手那些参数在 WebFlux 下根本不会生效。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 WebFlux 场景下的完整示例。长期跑编码类 Agent、需要稳定长连接的可以看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 把通道和额度一起规划好省得压测时还要分心查 Key。
返回列表