TellWorldAI 世界档案
问档案English

首页›实践›实践

实践

MCP 服务器的扩展与高可用

MCP 服务器扩展有四条路:水平加实例、纵向调单机、资源优化和分布式节点。这里讲清每条怎么做,Redis 注册和心跳怎么工作,以及示例里要先改的地方。

这篇适合谁、读完能做到什么

适合已经能跑通单个 MCP 服务器,开始担心并发量和稳定性的开发者,尤其是要把 MCP 部署到企业环境里的团队。读完你能分清水平扩展、纵向扩展和分布式节点各自解决什么问题,知道示例里的 Redis 注册和心跳是怎么工作的,也知道哪些代码需要改了才能跑。

扩展 MCP 服务器有哪几条路

企业部署的 MCP 服务常常要在低延迟下处理大量请求。常见的办法有四类:水平扩展、纵向扩展、资源优化和分布式架构[1]。

水平扩展是部署多个 MCP 服务实例,放在负载均衡器后面分摊请求。它能同时处理更多请求,单个实例出问题时还有其他实例顶上,因此也带来容错[1]。

纵向扩展是把单个实例调到更能扛请求:增加 CPU 和内存,同时精细调整配置,例如线程池、请求超时和内存上限[1]。

资源优化关注的是少做无用功,包括用更高效的算法、缓存和异步处理,减少资源消耗、缩短响应时间[1]。

分布式架构则让多个节点一起分担负载,并通过协调保证冗余[1]。这四条路往往要组合使用:先把单实例调好,再横向加实例,最后才考虑节点之间的协调。顺序也有讲究。状态没有外置就直接加实例,会出现会话错乱;没有心跳就做负载分配,等于盲分。

水平扩展:多实例加负载均衡,状态放到 Redis

水平扩展的第一个难点是会话状态。示例的 C# 代码用 AddStackExchangeRedisCache 配置了 Redis 分布式缓存,从名为 RedisConnection 的连接字符串读取地址,实例名前缀设为 MCP_[1]。这样,同一个用户的状态不必绑死在某个实例上。如果状态只存在某个实例的内存里,负载均衡把下一个请求转到别的实例,状态就丢了。把状态放进 Redis,任何实例都能读到,这正是水平扩展能成立的前提。

服务选项里打开了分布式缓存,EnableDistributedCaching 设为 true,CacheExpirationMinutes 设为 60[1]。工具也通过 AddMcpTool<HighPerformanceTool>() 注册,目的是让它能在多个 MCP 实例上使用[1]。完整的配置如下:

// ASP.NET Core MCP load balancing configuration
public class McpLoadBalancedStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Configure distributed cache for session state
        services.AddStackExchangeRedisCache(options =>
        {
            options.Configuration = Configuration.GetConnectionString("RedisConnection");
            options.InstanceName = "MCP_";
        });
        
        // Configure MCP with distributed caching
        services.AddMcpServer(options =>
        {
            options.ServerName = "Scalable MCP Server";
            options.ServerVersion = "1.0.0";
            options.EnableDistributedCaching = true;
            options.CacheExpirationMinutes = 60;
        });
        
        // Register tools
        services.AddMcpTool<HighPerformanceTool>();
    }
}

这段代码里的 API 名称,例如 AddMcpServer、EnableDistributedCaching,是示例中的写法。接入前要对照你所用 SDK 的文档确认它们确实存在,并确认参数含义一致。

纵向扩展:线程池、请求上限和超时

纵向扩展的典型做法是调线程池。Java 示例把核心线程数设为可用处理器数,最大线程数设为它的两倍,注释说明这是 I/O 密集型任务的常见经验值[1]:

        // Configure thread pool for optimal performance
        int processors = Runtime.getRuntime().availableProcessors();
        int optimalThreads = processors * 2; // Common heuristic for I/O-bound tasks
        
        ExecutorService executorService = new ThreadPoolExecutor(
            processors,       // Core pool size
            optimalThreads,   // Maximum pool size 
            60L,              // Keep-alive time
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(1000), // Request queue size
            new ThreadPoolExecutor.CallerRunsPolicy() // Backpressure strategy
        );

这段代码用的是有界队列(容量 1000)。队列满了以后,策略是 CallerRunsPolicy:由提交任务的线程自己执行,相当于给上游一个背压信号[1]。这是处理过载的一种方式,避免请求无限堆积。调线程池时,建议先在压测中观察排队长度和超时比例,再决定核心线程数和队列容量;示例里的 1000 只是示例值。

同一个示例还设置了三个上限:单个请求最大 1MB,同时处理的请求最多 100 个,请求超时 5000 毫秒[1]。这些数字是示例的取值,不是推荐值。你的请求体大小、并发量和超时要按自己的业务数据测出来再定。

分布式节点:用 Redis 注册和心跳

分布式节点的生命周期可以归结为三个动作:启动时注册,运行中定期心跳,关闭时注销。示例代码正好覆盖这三步,可以当作骨架来读。

分布式部分的示例是 Python 写的。每个节点启动时,先把自己的 ID 加入 Redis 集合 mcp:nodes,再在 mcp:node:{id} 这个哈希里写入状态。连接地址是 redis://redis-master:6379[1]。启动完成后状态改为 running,节点就算加入了集群。

工具注册分两部分。所有节点都注册公共工具;专用工具则按节点 ID 最后一位十六进制数字对 3 取余来分配,三种专用工具大致轮流分到各个节点上[1]:

    def register_tools(self):
        # Register common tools across all nodes
        self.server.register_tool(CommonTool1())
        self.server.register_tool(CommonTool2())
        
        # Register specialized tools for this node (could be based on node_id or config)
        if int(self.node_id[-1], 16) % 3 == 0:  # Simple way to distribute specialized tools
            self.server.register_tool(SpecializedTool1())
        elif int(self.node_id[-1], 16) % 3 == 1:
            self.server.register_tool(SpecializedTool2())
        else:
            self.server.register_tool(SpecializedTool3())

心跳每 5 秒写一次 lastHeartbeat、load 和 maxLoad,也就是最近一次心跳的时间、当前活跃请求数和上限。写入失败时等 1 秒再继续[1]:

    async def _heartbeat(self):
        """Periodic heartbeat to indicate node health"""
        while True:
            try:
                await self.redis.hset(
                    f"mcp:node:{self.node_id}", 
                    mapping={
                        "lastHeartbeat": int(time.time()),
                        "load": len(self.server.active_requests),
                        "maxLoad": self.server.max_concurrent_requests
                    }
                )
                await asyncio.sleep(5)  # Heartbeat every 5 seconds
            except Exception as e:
                print(f"Heartbeat error: {e}")
                await asyncio.sleep(1)

这里有两个地方要注意。第一,心跳代码调用了 time.time(),但示例的导入只有 asyncio、aioredis、uuid 和 AsyncMcpServer,没有导入 time,直接运行会报错[1]。第二,示例把心跳字段写进了 Redis,但示例代码里没有任何地方读取它们。负载信息要真正起作用,还需要一个读取并据此分配请求的组件。

节点 ID 是默认随机生成的 UUID,所以专用工具的分配在统计上大致均匀。但它不保证每种专用工具一定有节点注册成功,部署时应当检查三种专用工具是否都在至少一个节点上注册。

关闭时,节点先把状态改为 stopping,停止服务,再从 mcp:nodes 中移除自己,删除状态哈希,最后关闭 Redis 连接[1]。这一步保证集群里不会留下已经下线的节点记录。注意,它只在正常关闭时执行。

常见问题

  • 节点异常退出:异常退出不会执行 shutdown,节点记录会一直留在 mcp:nodes 里。示例只在正常关闭时清理,所以还需要超时机制,把心跳过期的节点视为离线。
  • 节点之间负载不均:示例的心跳只记录负载,不做分配。要实现负载感知的分配,需要在请求入口读取各节点的心跳,再决定转发给谁。
  • 上线后看什么:每个节点的 load 与 maxLoad 之比、最后一次心跳的时间,以及请求超时数。示例写入的正是前两项的数据,但没有汇总和告警,需要自己补上。
  • Redis 成为单点:示例只配置了一个 Redis 地址,没有涉及 Redis 的高可用方案。生产环境要自己规划主从或集群。
  • 超时和线程池不匹配:请求超时设得很长,而线程池队列又很大时,排队的请求会先超时,资源白白占着。调参时应当一起看,而不是只改其中一个数字。
  • 代码直接运行失败:Python 示例缺少 time 的导入。另外,C# 和 Python 示例里的 SDK 接口名称都要对照你的版本确认。改完之后,在本地连一次真实的 Redis,看 mcp:nodes 和 mcp:node:{id} 里是否出现对应的记录,心跳字段是否每 5 秒更新,这是最快的验收办法。

下一步

下一个值得读的是安全。扩展之后,节点数量和入口都增加了,鉴权与访问控制会变得更重要[1]。与扩展一起规划,比事后补救省力。

本文改编自 Microsoft 开源课程《MCP for Beginners》中关于 MCP 扩展与高性能的高级主题[1],原文以 MIT 许可证发布。

来源

  1. 1
    Scalability and High-Performance MCPMicrosoft mcp-for-beginners (MIT)

引用本页: MCP 服务器的扩展与高可用. TellWorld, https://www.tellworld.net/zh/practice/practice-mcp-scaling. 更新于 2026-11-14

相关动态