高并发下接口耗时狂飙?这3个高可用设计让QPS从500冲到5000

@TOC


上回书说完,这次来点更刺激的

Day5-1咱们聊了怎么用自定义线程池 + 连接池调优把接口从3秒拉到200毫秒,单机QPS稳在500。我记得那天晚上11点改完上线,信心满满地去睡觉,结果第二天凌晨3点……电话还是响了。

运维小哥声音都劈叉了:“哥!订单接口又炸了!现在QPS没到1000,CPU直接飙到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核,内存就4G,400线程全满了,新的请求排队,排到超时。这就是典型的“单活”问题——流量全压在单个节点上。

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 weight=1;
    server 192.168.1.11:8080 weight=1;
    server 192.168.1.12:8080 weight=1;
}

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或JWT,Nginx默认轮询,不需要粘滞,但如果业务依赖了服务器本地内存(比如用了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直接冲到4500,RT降到20ms,心里美滋滋。

然后,又一次深夜,告警来了。Redis实例因为内存满了挂了,重启后所有请求穿透到数据库,数据库瞬间被打崩,集群的QPS跌到0。

这就是典型的缓存雪崩 + 单点故障:Redis只有一台主库,挂了就全完。咱们需要故障转移机制。

2. Redis哨兵模式落地

Redis Sentinel(哨兵)能自动监控主从节点,主库挂了会选举新的主库,应用层几乎无感。

配置步骤(核心):

  • 在3台机器上分别部署一个Redis实例,一主两从。
  • 给每个Redis配上哨兵监控。哨兵配置 sentinel.conf
    sentinel monitor mymaster 192.168.1.10 6379 2
    sentinel down-after-milliseconds mymaster 5000
    sentinel failover-timeout mymaster 15000
    
  • Spring 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>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
</dependency>

编写推荐服务调用,加上降级逻辑:

@Service
public class RecommendService {
    
    @CircuitBreaker(name = "recommendBackend", fallbackMethod = "fallbackReco")
    public List<Product> getRecommendProducts(Long userId) {
        // 远程调用推荐服务,可能很慢
        return restTemplate.postForObject("http://reco-service/api/reco", userId, List.class);
    }

    // 降级方法:返回空列表或默认推荐
    public List<Product> 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的真实案例,还是老规矩,代码可运行,数据全透明。

觉得有用的,点个赞收藏,想系统学整套的,关注专栏,咱们下一篇见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值