高并发下接口耗时狂飙?这3个高可用设计让QPS从500冲到5000
TOC上回书说完这次来点更刺激的Day5-1咱们聊了怎么用自定义线程池 连接池调优把接口从3秒拉到200毫秒单机QPS稳在500。我记得那天晚上11点改完上线信心满满地去睡觉结果第二天凌晨3点……电话还是响了。运维小哥声音都劈叉了“哥订单接口又炸了现在QPS没到1000CPU直接飙到90%响应时间5秒起步……”我一脸懵赶紧爬起来看监控。好家伙真是单机能扛500超过500就开始排队线程池满了直接拒绝请求。老板第二天黑着脸问我“这系统能撑住促销吗”我说……能。但心里直打鼓。后来我花了三个周末硬是把QPS从500干到了5000。用的就是今天要讲的3个高可用设计多活架构多台机器一起扛故障转移Redis主从自动切换降级策略关键时刻保核心弃车保帅这篇你读完直接拿配置去用QPS至少翻10倍还能睡个安稳觉。一、多活架构一台机器不行就加三台1. 原始场景还原先复现一下那个凌晨的惨案。咱们有个订单查询接口核心逻辑是查MySQL顺带查用户标签服务内部RPC代码大概长这样RestController public class OrderController { Autowired private OrderService orderService; GetMapping(/order/{id}) public Order getOrder(PathVariable Long id) { return orderService.queryOrder(id); } }Day5-1已经调过自定义线程池了Tomcat也配了# application.yml server: tomcat: threads: max: 400 min-spare: 100本地用JMeter压500QPS稳稳的。但一到1000并发错误率直接上去RT响应时间暴涨。问题出在哪单机天花板。CPU就2核内存就4G400线程全满了新的请求排队排到超时。这就是典型的“单活”问题——流量全压在单个节点上。2. 多活架构的落地所谓的“多活”听起来高大上说白了就是多台机器同时提供服务。咱们搞三台ECS每个上面部署同一个Spring Boot应用然后用Nginx做负载均衡。架构图大概这样用户请求 - Nginx - 应用节点1 (192.168.1.10) - 应用节点2 (192.168.1.11) - 应用节点3 (192.168.1.12)Nginx配置超级简单upstream order-backend { server 192.168.1.10:8080 weight1; server 192.168.1.11:8080 weight1; server 192.168.1.12:8080 weight1; } server { listen 80; location /order/ { proxy_pass http://order-backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }人话解释Nginx把请求平均分给三台机器每台机器的负载从1000QPS降到了300多理论上整体能抗的QPS变成原来的3倍。3. 实践中的血泪教训你可能觉得这就完了我当时也是这么以为的配完Nginx直接压测结果QPS只翻了一倍多离5000还差得远。排查半天发现两个坑Session粘滞问题如果接口用了Spring Session或JWTNginx默认轮询不需要粘滞但如果业务依赖了服务器本地内存比如用了ConcurrentHashMap存用户状态就会导致请求发到不同节点找不到数据。血的教训做多活前先干掉本地内存状态转为Redis或分布式缓存。数据库连接数暴增三台节点同时连MySQL连接数从20直接飙到60如果数据库没优化好反而成为瓶颈。所以Day5-1的连接池调优得多机器一起考虑每台机器的最大连接数 数据库总连接数 / 节点数提前规划好。二、故障转移别让Redis宕机拖垮整个集群1. 缓存雪崩的恐怖多活搞完后QPS上到了3000但离5000还有距离。我一拍脑袋加Redis把订单数据缓存起来数据库压力瞬间下降。Service public class OrderService { Autowired private StringRedisTemplate redisTemplate; public Order queryOrder(Long id) { String cacheKey order: id; String json redisTemplate.opsForValue().get(cacheKey); if (json ! null) { return JSON.parseObject(json, Order.class); } // 查数据库再写缓存 Order order orderMapper.selectById(id); redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(order), 30, TimeUnit.MINUTES); return order; } }压测一看QPS直接冲到4500RT降到20ms心里美滋滋。然后又一次深夜告警来了。Redis实例因为内存满了挂了重启后所有请求穿透到数据库数据库瞬间被打崩集群的QPS跌到0。这就是典型的缓存雪崩 单点故障Redis只有一台主库挂了就全完。咱们需要故障转移机制。2. Redis哨兵模式落地Redis Sentinel哨兵能自动监控主从节点主库挂了会选举新的主库应用层几乎无感。配置步骤核心在3台机器上分别部署一个Redis实例一主两从。给每个Redis配上哨兵监控。哨兵配置sentinel.confsentinel monitor mymaster 192.168.1.10 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 15000Spring Boot集成哨兵YAML配置spring: redis: sentinel: master: mymaster nodes: - 192.168.1.10:26379 - 192.168.1.11:26379 - 192.168.1.12:26379 lettuce: pool: max-active: 50 min-idle: 10这样主库挂了哨兵自动把从库提升为主库你的Spring Boot应用通过哨兵拿到新地址整个集群继续服务。我那次凌晨3点就是没配哨兵手动登录机器改配置足足搞了40分钟。3. 故障转移带来的QPS提升加了哨兵后即使Redis出问题故障转移在15秒内完成期间因为熔断降级下面会讲接口不至于全挂数据库也能顶住。单节点缓存可靠性从“看天吃饭”变成了自动修复整体集群的可用性直接拉满。数据说话加上哨兵后集群抗住了一波5000QPS压测RT稳定在30ms以内0错误。三、降级策略关键时刻保命要紧1. 不是所有功能都值得死扛上了多活和故障转移按理说可以躺平了。但有一天运营搞了个“新品推荐”活动用户点进订单页时后台会调用一个推荐服务返回你可能喜欢的商品。这个推荐服务偶尔会慢到2秒导致整个订单接口被拖慢。我又被老板叫去“为什么订单接口这么慢推荐不显示也行啊先把订单展示出来”对啊非核心功能关键时刻可以降级。2. Resilience4j 实现降级Spring Boot 3.x 原生支持Resilience4j咱们用它来实现熔断降级。添加依赖dependency groupIdio.github.resilience4j/groupId artifactIdresilience4j-spring-boot3/artifactId /dependency编写推荐服务调用加上降级逻辑Service public class RecommendService { CircuitBreaker(name recommendBackend, fallbackMethod fallbackReco) public ListProduct getRecommendProducts(Long userId) { // 远程调用推荐服务可能很慢 return restTemplate.postForObject(http://reco-service/api/reco, userId, List.class); } // 降级方法返回空列表或默认推荐 public ListProduct fallbackReco(Long userId, Throwable t) { log.warn(推荐服务降级返回空列表。原因{}, t.getMessage()); return Collections.emptyList(); } }YAML配置熔断规则resilience4j: circuitbreaker: instances: recommendBackend: sliding-window-size: 10 # 窗口大小 failure-rate-threshold: 50 # 失败率超过50%就熔断 wait-duration-in-open-state: 30s # 熔断30秒后尝试半开 permitted-number-of-calls-in-half-open-state: 3 slow-call-rate-threshold: 100 # 慢调用阈值 slow-call-duration-threshold: 1s # 超过1秒视为慢调用人话解释当推荐服务最近10次调用中失败或超时比例超过50%时熔断器打开接下来30秒内所有调用直接走降级方法返回空列表避免拖垮整个订单接口。30秒后尝试少量请求如果恢复正常就关闭熔断器。3. 降级策略带来的压测效果我把这个配置上线后再压5000QPS即使故意把推荐服务延迟调到5秒订单接口的RT一直稳定在40ms以内。因为一旦触发熔断推荐调用直接短路主流程丝毫不受影响。对比数据| 方案 | QPS能力 | 平均RT(ms) | 99线(ms) | 错误率 | |------|---------|-----------|----------|--------| | 单机无缓存 | 500 | 200 | 1200 | 5% | | 三节点集群缓存 | 4500 | 20 | 150 | 0.1% | | 集群缓存降级 | 5000 | 18 | 100 | 0% |看到了吧降级策略不光是可用性的保障还能在异常场景下稳住性能。四、避坑指南这些烂坑我全踩过⚠️ 坑1多活≠随便加机器我早期觉得加机器就能线性提升QPS结果加到5台时QPS不升反降。排查发现数据库连接池满了每台机器默认连20个5台就是100个MySQL配置的max_connections只有100。解决统一调配连接池大小用HikariCP的maximumPoolSize限制总共别超过数据库上限。血的教训先评估数据库和中间件的承载能力再决定加多少节点。⚠️ 坑2降级方法不能有远程调用降级方法的目的是快速返回别在里面调数据库或远程服务否则雪上加霜。我见过有人在降级方法里查缓存结果缓存也挂了死循环。降级方法尽量只返回静态默认数据或本地计算。⚠️ 坑3缓存与数据库数据不一致加了缓存后偶尔会出现A机器改了数据库B机器缓存还是旧的。咱们采用先更新数据库再删缓存的策略并且可以用Canal监听binlog来同步。这个坑我掉进去过搞出了一个“幽灵订单”缓存里显示已支付数据库却是未支付。五、高级进阶你以为5000就到头了今天讲的这些其实只是企业级高可用的基础款。如果你还想继续往上冲比如QPS到5万、50万那就要考虑异地多活同城三节点如果整个机房挂了怎么办数据同步、延迟处理是另一个大课题。服务网格Service Mesh用Istio等把降级、负载均衡做到基础设施层业务代码可以更干净。读写分离CQRS数据库读写分离查询走从库写走主库复杂查询走ES等。这些内容我在专栏后面会展开讲带着你一步步搭建能在双十一扛住百万并发的系统。写在最后今天从凌晨救火开始咱们用多活架构打破了单机瓶颈用Redis哨兵解决了缓存单点故障最后用Resilience4j降级保住了核心接口的性能。这套组合拳打下来QPS从500干到5000其实只是开胃菜。说实话这3个设计思路能直接帮你解决80%的高并发接口性能问题而且配置我全都贴出来了直接拿到项目里改改就能用。但如果你想应对更变态的场景比如大促秒杀、红包雨那得深入学习分布式事务、最终一致性那些不得不啃的硬骨头。这个专栏30天能带你从CRUD工程师蜕变到能扛住亿级流量的架构师。下一篇我要跟你聊聊“接口再怎么优化都扛不住该上消息队列了”带你看Spring Boot 3.x整合RocketMQ的真实案例还是老规矩代码可运行数据全透明。觉得有用的点个赞收藏想系统学整套的关注专栏咱们下一篇见。