导读系列九讲完6层安全MCP Server已能挡人但双11预热凌晨两点一次真实事故告诉我能挡人不代表能扛事——单实例被流量打满、下游慢调用拖垮全场。本文基于那次脱敏复盘加本机Docker实测讲清无状态MCP Server如何做到挂一个不影响整体状态全外置才能扩容Nginx加权轮询加健康检查摘死实例下游挂了靠Resilience4j熔断兜底、工具分级降级再kill实例与Redis断网演练验证。做企业MCP生产部署、大促扩容、第三方接口熔断或防Redis与DB单点的团队这套分层打法可作为同类场景的参考。性能数字只来自那次事故本机mock仅验证机制不验证量级。一、那个凌晨两点的告警双11预热第一天。我们的MCP Server上线两周一切正常。凌晨2:17告警群炸了。门店查询接口P99延迟从200ms飙到8s。客服群里店长开始吐槽说查个库存转半天圈。我远程登上去一看CPU打满QPS从平时的8涨到120。问题不在代码。代码没变。问题在于我们只部署了一个实例。大促期间总部营销中心推了一条活动到全部门店。店长们一拥而上查库存、查价格、查活动配置。单实例Spring Boot的线程池被打满后面的请求排队越排越慢越慢超时越多。我做的第一件事是重启。重启后好了3分钟又打满了。第二件事是加机器。但加机器不是复制一个jar包那么简单。你得解决三个问题新实例怎么加入、旧实例挂了怎么自动切、下游依赖挂了怎么办。这篇就讲这三件事。不是概念是我凌晨两点踩完坑之后、在我们这个场景里的标准答案。不过本文有两套证据来源先划清楚免得你拿数字去较真来源覆盖哪些数字怎么复核生产复盘当时监控已脱敏QPS 8→120、P99 200ms→8s、Nginx 8秒摘除、熔断3秒触发、约5%请求502无法复现是那一次事故的个案本机实测Docker mock 数据这次写作时跑的跨实例读写上下文、熔断按样本数触发、哨兵7秒切换、Host头400、连接池对照实验git checkout v10即可复现脚本与原始输出在仓库10-store-ha/verify/实测记录.md本机这套是单机容器加mock数据没有真实流量和真实数据库——机制可以照搬性能数字别照抄。二、全文导览多实例部署无状态之后为什么可以直接加机器Nginx负载均衡三种策略怎么选健康检查挂了的实例怎么自动摘下游熔断第三方服务挂了不能拖垮整个MCP优雅降级工具超时了返回什么故障演练手动kill一个实例验证你以为vs实际五个常见误解决策树你的场景需要做到哪一层完整可运行代码在文末国内直接访问https://gitee.com/ethanliang2016/mcp-in-actiontag:v10三、多实例部署无状态是前提系列三我们把MCP Server改成了无状态。这件事的真正价值今天才体现出来。有状态的MCP Server不能随便加机器。用户A的会话存在实例1的内存里你把请求打到实例2它不认识用户A直接报错。无状态之后每个请求都自带身份和上下文。实例1处理完不需要记住你下一个请求来实例3处理也完全OK。这意味着什么意味着你可以水平扩容。流量来了加机器流量走了减机器。业务代码不用改但负载均衡配置得同步更新或接入Consul / K8s这类服务发现加机器不是零成本的。但有一个前提你可能忽略了。无状态不是把session关掉就完了。你得确认所有工具调用都不依赖本地状态。我逐个检查了工具里有没有本地状态。配套示例里的6个工具是下面这张表生产环境当时还有一个调LLM的工具第7节会提到工具是否依赖本地状态风险查门店销售get_store_sales否查库 Redis缓存无查商品库存get_inventory否查库无查设备状态get_device_status否调用第三方接口无生成补货建议suggest_restock是本地缓存了一份规则表有保存上下文save_context是写在本实例内存有读取上下文load_context是从本实例内存读有两类状态都得挪走。第一类规则表。suggest_restock依赖本地缓存的一份安全线倍数。单实例时没问题多实例之后实例A和B可能算出不同的补货建议。搬到Redis之后大家读同一个key同一家门店在任何实例上算出来的都一样。第二类会话上下文。这条最容易漏——你已经把session关掉了但只要工具还往本实例内存里写东西多实例照样翻车写在实例A下一次请求落到实例B读出来是空。同样搬到Redis。配套示例里我把这两类都挪走之后实测验证是这样的k0 → 上一轮由 app-1 写入这轮请求落到 app-2读到了k0v0 k5 → 上一轮由 app-2 写入这轮请求落到 app-3读到了k5v5写和读落在不同实例上数据照样读得到。这段输入输出就是状态真的外置了最硬的证据你自己改完也建议这么验一遍。这个问题不致命但你不知道它存在上线后就会变成线上事故。还有一件连带的多实例加上客户端重试同一个请求可能被处理两次。读操作天然幂等写操作不是。做法是给写操作带幂等键request_id或业务唯一标识服务端用Redis或数据库唯一约束去重——配套示例没覆盖这块生产上得自己补。四、Nginx负载均衡三种策略的取舍多实例跑起来之后前面加Nginx做分发。upstream mcp_backend { server 10.0.0.11:8088 weight3; server 10.0.0.12:8088 weight2; server 10.0.0.13:8088 weight1; }三种策略我实测后的选择轮询默认。每个请求按顺序分。适合所有实例配置一样的场景。我们10.0.0.11是8核16G12和13是4核8G直接轮询会让小机器先挂。加权轮询。按机器性能分配权重。我们三台按性能给权重3:2:18核机3两台4核机分别2和1大机器扛最多流量。这是我们现在用的。IP哈希。同一个客户端固定打到同一个实例。听起来不错但我们是无状态MCP不需要会话粘滞。而且IP哈希有个坑某个实例挂了这个IP的所有请求重新分配可能造成请求抖动。我的建议在无状态、无需会话粘滞的前提下MCP直接用加权轮询不要用IP哈希。还有一点反直觉。很多人觉得负载均衡就是把请求分出去。但真正的问题是分出去之后你怎么知道哪个实例还活着。这就是健康检查。五、健康检查挂了要自动摘Spring Boot Actuator暴露健康端点management: endpoints: web: exposure: include: health,info endpoint: health: show-details: always访问/actuator/health返回的是这个形态配套示例里开了show-details: always{components:{ diskSpace:{status:UP}, livenessState:{status:UP}, ping:{status:UP}, readinessState:{status:UP}, redis:{details:{version:7.4.11},status:UP}, ssl:{status:UP}}, groups:[liveness,readiness],status:UP}重点看最后那个redis。健康检查不只是汇报我活着它把关键依赖一起带出来了——Redis一旦连不上这里会变DOWN负载均衡就能据此把这个实例摘掉。所以别让health端点只返回一个{status:UP}那等于把依赖问题藏起来。不过详情开出来是有安全代价的show-details: always会把组件细节全吐出去——Redis精确版本号、磁盘余量、SSL证书状态、连接池情况。拿到精确版本号能直接对CVE看出有哪些依赖就能画攻击面。**所以详情只给内网别给公网。**三道防线里至少做两道默认show-details: never对外只返回{status:UP}用health group单独开一个内部端点给Nginx和监控用include只放你真要看的那几个组件网络层隔离actuator只监听内网地址或防火墙只放行Nginxmanagement:endpoint:health:show-details:never# 对外精简group:internal:# 内部专用/actuator/health/internalshow-details:alwaysinclude:-redis-db-ping配套示例是本机Docker演示直接开的always——你上生产前记得收回去。Nginx这边要把权重和健康检查写在同一个upstream再补上代理头——这才是能用的终态配置别拆成两个片段用upstream mcp_backend { server 10.0.0.11:8088 weight3 max_fails2 fail_timeout10s; server 10.0.0.12:8088 weight2 max_fails2 fail_timeout10s; server 10.0.0.13:8088 weight1 max_fails2 fail_timeout10s; } server { listen 80; location / { proxy_pass http://mcp_backend; proxy_http_version 1.1; proxy_set_header Connection ; proxy_set_header Host $host; # 这条不能省原因见下 proxy_set_header X-Real-IP $remote_addr; } }max_fails2是连续2次转发失败就把这个实例摘下来fail_timeout10s是10秒后再试活了加回来。注意这里的失败是真实请求失败不是Nginx主动ping出来的——下面那条边界说明专门讲这点。**proxy_set_header Host $host这句最不起眼漏了整站400。**Nginx不配这条时转发给后端的Host默认是upstream的名字也就是mcp_backend——里面带下划线Tomcat直接判非法主机名。同一套服务的实测Host: localhost:8080 → 200 Host: mcp_backend → 400 Bad RequestTomcat这个坑阴在日志干净Nginx不报错后端也不报错只有客户端拿到400。当时的记录是这样的kill掉实例12的Java进程Nginx在大约8秒后把它从轮询列表移除。期间正在处理的请求会收到502但新请求自动分到11和13。8秒。这个数字你要知道。不是秒切是有一个窗口期的。在这个窗口期里部分用户会看到错误。如果你要求零错误需要在客户端加重试。边界开源Nginx这套max_fails / fail_timeout是被动健康检查——靠真实业务请求失败来计数不会主动ping后端。如果某实例长时间没流量它挂了Nginx也发现不了。这个盲区有个很阴的失效模式低峰潜伏高峰爆发。凌晨实例挂了因为没流量Nginx毫无察觉早高峰流量一来一波请求全撞在那个死实例上等max_fails攒够又要几秒。运维刚上班就接到报警还以为是流量问题。补主动探活有四条路Nginx Plus最省事但商业付费、nginx_upstream_check_module要重新编译Nginx、接入服务发现Consul / Nacos实例自注册自注销、以及脚本加定时任务定时curl各实例的health端点连续失败就在upstream里标down再reload。大多数团队用不起Plus也不想重编译Nginx脚本方案最实际——不优雅但成本低见效快。四种方案的成本对比和一个能直接用的脚本单独写一篇见文末延伸阅读。另一半主动下线要优雅停机上面讲的都是实例意外挂了怎么摘。但生产里绝大多数实例下线是你自己发起的——发版重启。这部分没做每次发布都会甩给用户一批502。做法是开server.shutdown: graceful再配一个timeout-per-shutdown-phase让Tomcat停止接收新连接、把在途请求处理完再退出Nginx侧配合走「摘流量 → 等排空 → 停进程 → 部署 → 健康检查 → 加回」多实例滚动着来一次只动一个。这里有个约束容易踩停机等待必须大于最长的工具超时否则等待到期直接强杀优雅停机等于没开。完整配置、发布步骤和我踩的坑单独写了一篇见文末延伸阅读这里不展开。六、下游熔断第三方挂了不能拖垮你MCP Server自己活着不够。它调用的第三方服务库存系统、设备状态接口可能挂。我凌晨遇到的第二个问题设备状态接口超时了。我们的查设备状态工具调用这个接口最初超时设了30秒。问题就出在这——下游是慢不是挂延迟涨到5秒但不报错这30秒里请求一直占着Tomcat线程不放。50个这样的请求打进来线程池直接满所有工具都不能用了。这就是典型的级联故障。一个下游挂了拖垮整个系统。Resilience4j加熔断CircuitBreaker(name deviceStatus, fallbackMethod fallback) public DeviceStatus queryDevice(String deviceId) { return deviceApiClient.getStatus(deviceId); } public DeviceStatus fallback(String deviceId, Throwable t) { return DeviceStatus.unknown(设备状态暂不可用); }熔断的逻辑失败率或慢调用率超过50%熔断器打开。接下来10秒请求直接走fallback不再调下游10秒后半开放一个请求试探好了就关没好继续开。注意一个坑——下游是慢不是挂默认只有抛异常才算失败慢调用不会被计入。所以我们额外配了slowCallDurationThreshold1s单次调用超过1秒就算慢调用5秒延迟就被统计进去熔断照样能触发。还有一个参数决定了多久打开这个问题其实没有固定答案minimumNumberOfCalls。Resilience4j默认要攒够5次调用才开始算失败率——按样本数触发不按时间触发。所以几秒打开取决于你的QPS并发高时5个请求眨眼就到请求稀的时候可能攒很久才开门。所以在压测环境看到的数字换个环境就对不上。而且它是每个实例各自计数的。加了负载均衡之后请求按权重摊到多个实例每个实例都要各自攒够样本。我在配套示例里单客户端串行调用时就是这个效果慢调用照旧发生但一直返回正常结果跑到第12次才出现降级——不是熔断没生效是分摊之后每个实例都没攒够样本。打开配套代码可能会一愣上面给的是注解写法仓库里却是Resilience4j纯库API手动构建的。原因是注解写法要走Spring Boot的自动配置而这套示例不想依赖自动配置也让它在Boot 4下能直接跑起来所以用了纯库写法——两者语义完全等价。语义注解写法纯库写法配套示例给方法接上熔断CircuitBreaker(namedeviceStatus, fallbackMethodfallback)CircuitBreaker.decorateSupplier(deviceCb, supplier)降级逻辑fallback(...)方法catchCallNotPermittedException后返回降级结果失败率阈值failureRateThreshold50.failureRateThreshold(50)慢调用判定slowCallDurationThreshold1s.slowCallDurationThreshold(Duration.ofSeconds(1))慢调用率阈值slowCallRateThreshold50.slowCallRateThreshold(50)打开后等待waitDurationInOpenState10s.waitDurationInOpenState(Duration.ofSeconds(10))统计窗口slidingWindowSize10.slidingWindowSize(10)最小样本数minimumNumberOfCalls5.minimumNumberOfCalls(5)参数一个对一个。也就是说从配套示例迁到你自己的项目里或者反过来阈值一行都不用改只是换写法。熔断管的是别被下游拖垮反方向还有别被上游冲垮——这就是限流我在系列九里用Bucket4j做过本篇不重复。关键是两者的配合限流是第一道闸熔断是第二道闸限流没生效时熔断会开得特别频繁因为你塞给下游的量本来就超了。看到熔断反复开先查限流。半开状态有三个细节容易踩半开的试探请求本身很慢算失败吗算。半开请求的结果照样统计慢调用也计入失败率——下游正在恢复、响应5秒时试探请求会直接把熔断器推回打开状态。**半开失败后等待时间重置吗**重置。放进去一个请求失败了会从头再等一个waitDurationInOpenState10秒不是立刻再试。**半开成功后流量就立刻全放吗**默认是。成功即关闭熔断器、全量放行——这正是刚恢复就被流量打挂的经典成因。Resilience4j的permittedNumberOfCallsInHalfOpenState只能决定半开期放几个请求本文配的是1它不是渐进放量。真要渐进恢复得在网关或限流层自己做先放10%稳住再放30%逐步到全量。最后一句运维经验这些阈值上线后多半要调。熔断阈值、限流阈值、超时时间最好接配置中心Nacos / Apollo之类支持热更新不然每调一次参就要重启一轮实例——对一篇讲高可用的文章来说自己把重启当儿戏就有点讽刺了。当时压测的结果设备接口延迟从200ms涨到5s后熔断在3秒内触发。触发后MCP Server的P99延迟从5s降回200ms。代价是查设备状态返回暂不可用但其他4个工具完全不受影响。超时是一套体系不是一个数字本文到这儿出现了好几个超时慢调用阈值1秒、设备接口超时30秒、熔断打开后等10秒、优雅停机等30秒。它们是互相约束的——总超时要大于各下游超时之和、越往下层越短、慢调用阈值必须小于工具超时、停机等待必须大于最长工具超时。单看哪个都合理凑一起就可能打架。拿本文这套数举例慢调用1秒小于接口超时这条没问题但优雅停机等30秒和接口超时30秒刚好相等就不行——真有请求卡满30秒时停机等待同时到期照样强杀。要么把接口超时压下去要么把停机等待加长两者不能相等。五条约束的完整说明、层级关系和重试策略单独写了一篇见文末延伸阅读这里只留结论。熔断兜底其余四个工具照常跑——这其实就是优雅降级不是所有工具都必须100% 可用一个挂了不能拖垮全部。七、优雅降级超时了返回什么降级要分程度差别在返回什么降级级别触发条件返回什么用户体验软降级工具超时但主流程可继续返回缓存数据 标注数据延迟能接受中降级下游熔断打开返回暂不可用 建议稍后重试可接受硬降级MCP Server整体过载返回503 友好提示需重试我们的做法查门店销售这种读多写少的加Redis缓存。下游挂了直接返回缓存数据标注数据可能延迟5分钟。店长能接受。顺带提一句缓存的三个经典问题——穿透查不存在的数据每次打到DB、击穿热点key过期的瞬间并发全涌进来、雪崩大批key同时过期或Redis整个挂掉——成因和解法完全不同别混着用。本文演练的是第三种里Redis整体挂了这一支。但现实里击穿更常见——很多团队Redis好好的却因为一个热点key过期把DB打挂了。三者的区分和各自解法单独写了一篇见文末延伸阅读。查设备状态这种实时性要求高的不能用旧数据。熔断后返回设备状态查询暂时不可用。营销文案生成这类LLM调用生产环境才有配套示例里没有这个工具加超时30秒。超时了返回空结果让Agent知道这个工具现在不能用它会自己决定要不要换个方式回答。另外HTTP层的502/503/429是一回事MCP协议层还有一层工具调用失败要按JSON-RPC错误对象返回负的code加一句人能读的message别把异常栈直接塞回去。客户端拿到错误后的行为由它自己决定——大多数Agent会自行重试或换别的工具所以message写现在不可用稍后再试比写内部错误有用得多。八、故障演练别等大促才发现理论说完了。真正验证高可用的方法只有一个主动制造故障。我做了三次演练演练1kill掉实例12。Nginx 8秒摘除其他实例接管。期间约5% 请求收到502。演练2设备接口断网。熔断3秒触发其他工具正常。用户看到设备状态暂不可用。演练3Redis挂了。这是我没想到的。缓存没了之后查门店销售全部穿透到DB——我用5次同样的请求量了一下缓存前后的差别缓存正常时只有第1次打到数据库后面几次全命中缓存数据库压力基本为零缓存一挂5次请求全部直连数据库数据库承接的查询量立刻放大好几倍。但这里有个前提我后来才想明白而且它非常容易被说反Redis挂了并不自动等于连接池打满。真正决定的是并发数 × 单次查询耗时有没有超过连接池的容量。我拿同样的池上限8跑了两组对照12个并发客户端抢连接单次查询耗时12并发打上进8连接的池结果200ms级正常全部正常返回12个全成功没有发生排队——连接用完就还了退化到4秒8个成功剩下4个直接拿不到连接Too many connections所以准确的说法是缓存是个节流阀。阀一没DB承受的压力立刻按倍数放大。至于会不会打满连接池要看那时候查询本身有没有变慢。而现实里这两件事常常一起发生——热点key集体过期或者DB本来就在报警。于是体感上就成了Redis挂 → DB跟着挂的连锁反应容易把因果记反。配套示例是全mock数据、不带真实数据库上面这两组对照是我在本地另外起的一套MySQL Redis上跑出来的我把触发条件、实验脚本和原始输出都留在仓库的10-store-ha/verify/实测记录.md里了你可以照着自己复现一遍。Redis挂了这个case教会我一件事你的MCP Server依赖的每一个外部组件DB、Redis、下游API都是单点。你得知道每个单点挂了会发生什么。缓存这边我用哨兵解决了自动切换数据库是另一个单点主从复制、读写分离或直接用云RDS都行。DB要是单点前面这些高可用做再多也白搭。后来我给Redis加了哨兵Sentinel三个节点形成多数派主节点挂了自动切换。实测一遍大约7秒完成15:59:47 # sdown master mymaster redis-master 6379 15:59:48 # odown master mymaster redis-master 6379 #quorum 3/2 15:59:49 # switch-master mymaster redis-master 6379 172.19.0.5 6379切换完成后读写恢复正常切换前已经写进去的数据也没丢——新主库是从副本提上来的数据早就同步过去了。原主库回来之后会被哨兵降级成副本不会形成双主。**但别理解成切换期间零失败。**主从切换有一个窗口期我这次实测约7秒这段时间主库不可写撞上去的写请求是会失败的。我的验证是在切换完成之后才发起的所以看到的是一切正常窗口期内那几个请求报的错我这边没采到。要兜住这段靠的是客户端重试Lettuce会自己重连重要写操作建议业务层再补一次重试不是靠哨兵。另外哨兵只解决一件事主节点故障自动切换。它不解决水平扩展读压力大要做读写分离也不解决数据分片数据量上去了要Redis Cluster。选型时别把它当万能药。这里有个坑值得单独拎出来因为按直觉操作会得出哨兵没用的错误结论演练主库故障要用pause不要用stop。容器一stopDocker网络里的DNS别名就注销了而哨兵配置里开着resolve-hostnames解析失败会让它进入TILT模式——一种自我保护状态期间不发起failover。我第一次就是这么试的主库明明已经不可用哨兵却迟迟不动15:55:12 # Failed to resolve hostname redis-master 15:55:12 # tilt ← 自我保护不发起 failover 15:55:42 # sdown master mymaster ← 本该 5 秒就标记 15:55:45 # tilt换成pause冻结进程但别名和IP都保留之后一切正常5秒sdown、7秒完成switch-master。日常监护没有监控的高可用是盲飞演练是定期体检但平时你得知道系统现在什么状态。这里只讲跟高可用直接相关的那几个指标——全链路追踪和可观测性体系放在系列十四不在这儿展开。至少要看这6个各实例的QPS / 错误率 / P99健康实例数Nginx认为还活着几个熔断状态哪些下游的熔断器是打开的、开了多久降级次数三个级别各触发了多少次线程池 / 连接池使用率依赖可用性DB、Redis、下游接口告警阈值我用的经验值错误率5%、P991s、实例不健康、熔断打开超过30秒、线程池使用率80%这条是预警不是告警。池子该设多大是另一个话题——线程池不是越大越好、数据库连接数有公式、拍脑袋定的数基本都不准得靠实际使用率回调这些单独写了一篇见文末延伸阅读。这里只强调一个结论容量不是静态数字它是并发 × 单次耗时的函数。你按今天的耗时配的容量明天下游慢一倍就不够用了——上面那组对照实验已经证明过这一点。熔断这里有个原则值得单独说**fallback必须极简不能再依赖外部服务。**否则下游挂了、fallback也跟着挂那就真没有退路了——降级是为了收窄影响面不是换个地方继续失败。另外熔断打开这件事本身要留一条审计日志什么时候开的、开了多久、失败多少次不然事后复盘只能靠猜到底是下游真的挂了还是我阈值配得太敏感。九、你以为vs实际你以为实际加机器就能扛住流量加机器前先确认无状态否则会话错乱Nginx负载均衡自动搞定一切Nginx不会主动发现实例挂了要配健康检查下游挂了等它自己恢复不熔断的话一个下游挂拖垮整个MCP降级就是返回错误页降级是分级别有的返回缓存有的返回友好提示高可用 多活高可用是一个挂了系统还能用不是所有都活着加了健康检查就零故障了健康检查有窗口期窗口期内照样有错误要零错误得客户端重试熔断越快越好太快误伤、太慢保护不住而且它是按样本数触发的不是按时间多实例就等于高可用多实例却共用一个DB或Redis那依赖还是单点十、决策树你需要做到哪一层不是所有人都需要上K8s。按你的场景选你的MCP Server有多少QPS ├─ 10 QPS内部用 │ └─ 单实例定时重启就够了别折腾 ├─ 10-100 QPS有真实用户 │ └─ 2实例Nginx加权轮询健康检查下游熔断 ├─ 100-500 QPS大促有峰值 │ └─ 3实例Redis缓存优雅降级故障演练 └─ 500 QPS核心业务 └─ K8s自动扩缩容多机房全链路压测不过QPS只是其中一个维度。真正在这几档之间做选择的通常是另外三件事维度怎么影响选择SLA99.5%和99.99%不是一个量级的投入——后者基本意味着多机房光这一项就够吃掉预算成本2实例加Nginx是几百块一个月的事K8s加多机房是好几倍还得算上运维人力团队能力K8s不是搭起来就完事得有人能扛半夜的Pod异常。没人维护的话3实例加Nginx远比一个没人看懂的K8s集群可靠另外MCP有个容易被忽略的点QPS不是唯一衡量维度同时在线的会话数和工具调用频率往往更早撞到瓶颈——一个会话里Agent可能连着调七八个工具。还有一个前置问题你怎么知道该上几台估峰值QPS历史峰值 × 1.5~3倍增长系数→ 单机压测找到瓶颈 → 按峰值 ÷ 单机容量 × 冗余系数算实例数。冗余至少30%大促给50%~100%压测别只压匀速流量指标看P99不看平均值。至于决策树最后一档的多机房——那是另一个量级的话题本文不覆盖数据同步、流量调度、机房级切换以及成本。它通常需要单独立项。我们现在在第三档。双11期间从2实例扩到3实例扛住了120 QPS的峰值。没有用户投诉。十一、下一篇高可用解决了挂了怎么办。但店长们在飞书群里 机器人查库存这个场景我们还没接。下一篇讲怎么把MCP接到飞书让店长在群里直接 机器人查数据。十二、本次实测核心踩坑与风险环境坑Nginx漏配proxy_set_header Host $host时转发Host默认成upstream名含下划线Tomcat判非法主机名导致整站400且Nginx与后端都不报错、只有客户端拿到400日志干净极难查actuatorshow-details: always会把Redis精确版本、磁盘余量、SSL状态全吐出去等于给攻击者画攻击面只能放内网。安全坑健康详情开公网泄露版本号与依赖即暴露CVE攻击面fallback若再依赖外部服务下游挂时降级也跟着挂等于没退路熔断何时打开、开了多久、失败几次若不记审计日志事后复盘只能靠猜。生产坑被动健康检查靠真实请求失败计数低峰实例挂了Nginx毫无察觉、早高峰才爆发优雅停机等待必须大于最长工具超时否则等待到期强杀、优雅停机形同虚设熔断各实例各自计数负载均衡把请求摊薄后单实例凑不够样本看起来没生效其实是没攒够次数。边界坑本机mock无真实流量与数据库性能数字别照抄Redis哨兵只解决主节点自动切换不解决水平扩展与数据分片多实例若共用单点DB或Redis前面高可用全白做熔断半开成功后默认全量放行刚恢复就被流量打挂是经典成因。延伸阅读这个专题的另外 5 篇讲高可用时我拆出去单独写了 5 个知识点跟本文配套篇标题链接01Nginx开源版没有主动探活四种补救方案和一个能用的脚本待补02发版就掉请求Spring Boot优雅停机的四步待补03超时不是一个数字是一套体系MCP Server的五条约束待补04高可用配完之后该看哪几个指标池子该设多大待补05缓存穿透、击穿、雪崩三个概念三种解法待补本文完整可运行代码tag:v10国内访问 / 点starhttps://gitee.com/ethanliang2016/mcp-in-actionGitHub需代理https://github.com/ethanliang2016/mcp-in-action克隆git clone https://gitee.com/ethanliang2016/mcp-in-action.gitclone下来git checkout v10就能还原本文状态全mock数据不依赖任何真实系统。跑通了的话点个star——这是我继续写下去最直接的反馈。跑不通直接来提issue我看到就回。