这篇适合谁、读完能做到什么
适合已经能跑通单个 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 许可证发布。
来源
- 1Scalability and High-Performance MCPMicrosoft mcp-for-beginners (MIT)
引用本页: MCP 服务器的扩展与高可用. TellWorld, https://www.tellworld.net/zh/practice/practice-mcp-scaling. 更新于 2026-11-14