ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

高并发系统面试指南:从QPS压测到限流降级的实战拆解

高并发系统面试指南:从QPS压测到限流降级的实战拆解 “你简历上写着‘精通高并发’那咱们聊聊你负责的系统核心接口的QPS峰值是多少这个数字你怎么得出的如果流量再翻一倍你最先动哪里”这是我面试时特别爱问的开场白。十个候选人里有五六个会被这一串问题问住。他们能背出“高并发三件套”——缓存、异步、削峰——但问到具体数字怎么来的、瓶颈卡在哪个环节、扩容为什么有的系统加了机器反而更慢就开始含糊其辞。这种状态行内人一眼就能认出简历上的“高并发经验”实际是“高并发围观经验”。这篇文章我想借一场虚拟面试把高并发系统的核心挑战拆开揉碎。不是给你背八股文答案而是从面试官的角度告诉你一个问题为什么要这么问、背后考察的是哪种能力、真正做过的人会怎么回答。不管你是准备面试、刚接手高流量系统还是单纯想把“高并发”这三个字理解透这篇文章应该都能给你一些不一样的启发。1. 第一层拷问压测数据背后到底在测什么面试官第一个问题往往是“你的系统能扛多少并发”这题的陷阱不在于数字大小而在于——你说出来的那个数到底怎么来的。1.1 先搞清楚QPS、RT和吞吐量的关系很多人张口就是“我们接口QPS 5000”问RT多少答不上来问压测用的什么工具、压了多久、样本量多少也说不清楚。这种回答暴露了一个问题他对QPS这个指标本身就没理解透。高并发场景下三个指标必须一起看QPS每秒请求数衡量系统处理请求的速度RT响应时间单个请求从发出到返回的耗时吞吐量系统在单位时间内实际完成处理的请求总量它们之间有个很基础的规律——Littles Law系统内的并发请求数 QPS × 平均RT。举个例子一个接口RT是200msQPS是1000那么系统任意时刻“在途”的请求数就是1000 × 0.2 200。这200个请求需要200个连接资源去支撑。你反过来算就知道为什么RT升高会吃掉连接池——RT从200ms涨到2s同样的QPS下在途请求数变成10倍连接池不炸才怪。这个公式看着简单但真到排查问题时就特别管用。我曾经处理过一次线上故障接口RT从50ms飙升到5秒QPS没变结果数据库连接数在几分钟内被打满。原因就是慢查询拖住了RTRT拉高后每个请求占用连接的时间变长连接池被耗尽后续请求全部排队。这就是典型的“RT劣化引发雪崩”路径。1.2 面试官真正想听的压测方法面试官如果追问你“QPS怎么压出来的”他想听到的是这样一套完整动作先定环境压测环境要和生产环境配置对等至少CPU核数和内存不能差太多数据库数据量要接近生产规模。拿一个4核8G的机器压出来的数据去推算128核生产集群没有意义。再定目标RT比如业务要求P99响应时间在200ms以内那压测就看200ms这个水位线能扛到多少QPS。逐步加压找拐点用wrk、JMeter或者Locust从低并发开始逐步增大并发数观察RT曲线。你会看到一个明显的拐点在拐点之前QPS线性增长、RT平稳过了拐点QPS不再增长甚至下降RT指数级飙升。这个拐点就是系统的真实上限。过程中盯监控压测不是只看结果数字CPU、内存、GC、数据库连接数、磁盘IO要同时看。拐点出现在哪个资源上哪个就是瓶颈所在。拿wrk举例命令很简单wrk -t4 -c200 -d30s --latency http://your-api.com/v1/query-t是线程数-c是并发连接数-d是持续时长。跑完看两个数Requests/sec就是QPSLatency分位数能看到P50、P99。我个人的习惯是根据压测数据算出一个“安全水位线”通常是拐点值的70%到80%。比如单机压测拐点是1000 QPS那线上就把单机流量控制在700到800。留出余量才能应对流量突刺不然一个大促活动就把系统顶到拐点后面用户体验直接崩盘。面试官问压测核心想确认三件事你会不会科学地给系统定指标、能不能通过实验找到系统的极限位置、知不知道怎么把极限值转变成线上的容量规划依据。这三点全做到才算真正有实战经验。2. 第二层拷问加机器就完事了吗无状态设计的真相“系统扛不住怎么办”“加机器啊。”这个回答本身没错但只答对了一半。面试官紧接着就会问“加机器之前你要确认什么”这一问能把水货和做过的人立刻分开。2.1 为什么有的系统加机器没用水平扩容有个大前提你的应用层必须是无状态的。如果每个请求都依赖本机内存里的Session、本地磁盘上的临时文件、或者单机进程里的缓存那么当你从1台扩到10台时流量被负载均衡分发到不同机器上用户上一次请求落在A机器、下一次落在C机器A机器上存的Session对C机器来说就是不可见的。结果就是用户反复被踢下线行为数据丢失体验雪崩。这就是很多人踩过的坑——系统扛不住加机器加了之后问题不但没解决反而出了新故障。原因就是扩容前没有检查状态依赖。有状态和无状态的判断方法很简单把一台机器从集群里摘掉如果系统功能不受影响那就是无状态如果出现数据丢失、需要人工迁移数据才能恢复那就是有状态。2.2 无状态改造的实操路径改造的核心思路只有一个把状态从进程内搬到进程外。会话信息把Session从Tomcat内存搬到Redis或者直接用JWT之类的无状态令牌把会话信息放到客户端服务端不存任何状态。本地缓存单机进程内缓存比如Caffeine、本地Map在高并发下命中率再高扩容后也会因为数据分散而命中率骤降。大流量场景要上分布式缓存Redis或者至少在本地缓存命中失败后做一次分布式缓存的回源避免每次都打到DB。临时文件上传文件、生成报表这类功能文件不能落在本地磁盘要放到对象存储或者分布式文件系统否则请求被路由到其他机器就找不到文件了。我在实际项目中改造过一个老系统最麻烦的其实是隐藏状态。原业务代码里有个全局静态Map跑着跑着往里塞数据看起来没什么问题等到要做多副本部署才发现这个Map里积累的数据在每台机器上都不一样只能先把这块逻辑全部改成Redis存储折腾了两周才完成无状态化。这个教训让我后来在做技术方案评审时看到“static”关键字就条件反射去问这个东西能跨进程共享吗2.3 数据和连接扩容的隐形天花板应用层无状态只是第一步。流量上来了数据库怎么办比如你的系统从10台扩到20台QPS翻倍但数据库还是那一套。应用层的QPS上限提高了DB的连接数和查询压力成了新瓶颈。这时候就需要分库分表、读写分离或者引入缓存挡在DB前面。分库分表这块水货最爱说“我们用了ShardingSphere”但一问分片键怎么选的就卡壳了。分片键的选择和业务强相关目标是让数据分布均匀、且大多数查询能路由到尽可能少的片上。选的不好比如用UUID做分片键插入时数据分布确实均匀了但按用户维度查询时要跨所有分片聚合性能反而还不如不分片。扩容时还会遇到数据迁移的问题原来10个库要扩到20个不是简单加库就行得考虑数据怎么重新分布。用一致性哈希能最小化迁移量但复杂度也高这里面的取舍要结合业务规模来谈。面试官考察扩容设计其实想看你会不会做“容量规划前的体检”应用层有没有状态、数据库连接够不够、热点数据有没有倾斜、文件存储能不能水平扩展。能把这几个问题主动列出来基本就是有实战经验的人。3. 第三层拷问缓存是救星还是雷区高并发读场景下缓存几乎是必选项。但缓存用不好就不是“帮系统扛压力”而是“给系统埋雷”。面试官在这里的经典追问是缓存穿透、击穿、雪崩你分别怎么处理3.1 三个“刺客”的原理和应对穿透查询一个根本不存在的数据缓存里没有数据库里也没有每个请求都直接打到DB。如果黑客用一堆不存在的ID刷接口DB分分钟被打垮。应对方案有两种一是布隆过滤器启动时把存在的ID加载到布隆过滤器里查不到直接返回二是缓存空值把不存在的key也缓存起来设置短过期时间比如60秒减少DB落库查询。布隆过滤器有误判率但宁可误判放进来一次也不能让恶意流量全打到DB。击穿某个key是热点大家都在查它结果缓存刚好过期瞬间所有请求全部穿透到DB。单点压力瞬间拉满。处理方法是互斥锁查到缓存过期时不是所有请求都去DB查而是让第一个请求去查DB并重建缓存其他请求短暂等待后重新读缓存。实现上可以用Redis的SETNX做分布式锁也可以用进程内锁配合单机版重建。// 伪代码示例缓存击穿互斥锁 public String getData(String key) { String value redis.get(key); if (value ! null) { return value; } String lockKey lock: key; if (redis.setIfAbsent(lockKey, 1, 3, TimeUnit.SECONDS)) { // 第一个请求重建缓存 String dbValue queryFromDB(key); redis.set(key, dbValue, 600, TimeUnit.SECONDS); redis.delete(lockKey); return dbValue; } else { // 其他请求短暂sleep后重试 Thread.sleep(50); return getData(key); } }雪崩大量key在同一时间段集中过期导致这些key的请求全部打到DB。它和击穿的区别在于击穿是单个热点雪崩是大面积。应对方案是在设置过期时间时加随机偏移量比如“基础过期时间 随机0到300秒”避免同一秒集体过期。还可以做多级缓存本地缓存挡一道Redis挡一道最后才打到DB。3.2 缓存一致性才是真正的难点穿透击穿雪崩都是入门题更难的是缓存和数据库的一致性问题。面试官会问“你们怎么保证缓存和DB的数据一致”这个问题的标准操作叫Cache Aside模式读的时候先读缓存没命中再读DB然后回填缓存写的时候先更新DB再删缓存。但删缓存这个动作有个并发隐患。举个例子一个线程更新DB成功还没删缓存另一个线程正好读到旧缓存回填了DB里的新值此时第一个线程删缓存把新回填的缓存也删掉了。下一次请求再查缓存只能重新读DB这时候数据是对的但缓存已经被误删了。更麻烦的一种情况是删缓存失败那就只能等过期期间一直返回旧数据。实际项目中常用的补救方案叫“延迟双删”先删缓存再更新DB更新完成后sleep几百毫秒再删一次缓存。这个方案不完美延迟时间内可能有脏数据但对大多数业务来说可接受。要是业务强一致要求更高那就需要引入更复杂的机制比如订阅DB binlog异步同步缓存靠消息保证最终一致。我在自己负责的系统里做过一个教训深刻的优化为了省事更新数据时直接更新缓存而不是删缓存结果两个并发线程交替更新缓存造成缓存里存了旧数据且长期不失效。排查了好几天才发现是这一行代码的问题。从那以后我严格遵循“先更新DB后删缓存”的原则宁可多几次冷启动回源也不碰“更新缓存”这条线。面试官问缓存其实不是考你背了多少方案而是看你能不能根据业务场景选方案。读多写少的场景、写多读少的场景、强一致和最终一致的场景方案都不一样。能把这些差异说清楚才是真懂缓存。4. 第四层拷问消息队列是削峰神器但你知道它隐藏的成本吗高并发写流量场景秒杀、下单、抢购核心矛盾是流量在极短时间内冲过来而系统无法瞬间消化。面试官这题的进阶版是“为什么用了MQ系统就能扛住MQ本身不会被打挂吗”4.1 削峰填谷的本质消息队列的核心价值在于将同步请求变成异步处理拉平流量曲线。用户请求进来后我们快速地把“订单创建”这个消息写入MQ然后立刻返回“下单成功”真正的库存扣减、订单入库、通知发货全部放到下游消费者里慢慢处理。这样系统处理的QPS上限取决于消费者的处理能力而不是流量的峰值。这里有个容易误解的点MQ本身是有容量上限的。如果瞬时流量超过了MQ写入的能力或者消息堆积速度远超消费速度MQ的磁盘、内存也会被打爆。所以用了MQ不等于万事大吉你依然要做流量预估确保峰值流量下的写入速度在MQ承受范围内。4.2 消息队列带来的三大可靠性挑战面试官这时通常会抛出一连串问题消息丢了怎么办重复消费怎么办顺序怎么保证这三个问题分别对应消息生命周期的三个环节发送端可靠性生产者发送消息时要用确认机制确保消息到达Broker。Kafka可以设置acksallRabbitMQ可以开publisher confirm机制。没有确认的消息要重发或者落本地表下次补偿发送。消费端可靠性消费者处理完成后要手动ACK不能自动ACK。很多初学者用Spring的自动ACK模式消费者收到消息就确认了但业务逻辑还没执行完崩溃后消息就丢了。重复消费消费者处理完消息后还没来得及ACK就宕机Broker会重新投递导致同一消息被处理两次。解决重复消费唯一的办法是幂等消费端根据业务唯一键去重比如订单ID在消费记录表里先查后写或者数据库里加唯一约束重复插入直接报错返回成功。我自己踩过的一个真实案例消息通知服务消费端逻辑很简单给用户发短信没有做幂等。结果某次下游短信服务超时消费端处理失败触发了重试一条“订单发货”短信给用户连发了六遍。后来在消费逻辑里加了一个基于订单ID通知类型的去重表这个问题才彻底解决。4.3 MQ选型不是越火越好在MQ选型上大多数面试者只能答出“Kafka吞吐高、RabbitMQ功能全”这种级别。更深入的考察点是你这个业务场景的数据量到底需要哪种MQ对比项KafkaRabbitMQRocketMQ吞吐量极高百万级/秒中等万级/秒高十万级/秒消息可靠性高需正确配置高机制完善高事务消息支持好延迟毫秒级但吞吐优先时延迟会变高微秒级延迟毫秒级社区活跃度极高高国内高适用阶段海量日志、大数据管道企业级应用、复杂路由电商平台、金融级业务对于大多数业务系统日请求量在百万到千万级别其实RabbitMQ或RocketMQ已经足够了非要上Kafka带来的运维成本和复杂度反而是一种负担。Kafka在机器配置、分区数调整、消费者Rebalance机制上都有不少讲究用得不好性能还不如RabbitMQ。我见过不止一个团队业务量不大却非要用Kafka结果topic和分区规划不合理消费者频繁Rebalance导致消费延迟抖动调试起来特别折腾。面试官问MQ考察的是你能不能把“异步解耦”这四个字落到具体的可靠性和一致性问题上。能主动说出消息丢失的三个环节和幂等方案这道题基本就拿下了。5. 第五层拷问流量超过预估值时系统靠什么活下来前面聊的是容量规划但再完备的规划也会遇到意外活动效果超预期、爬虫攻击、联调方异常重试。面试官这时候会问“假设系统容量就是不够了你有哪些止损手段”5.1 限流把流量挡在系统能承受的范围内限流的本质是保护系统不被打垮。常见算法有四种每种都有适用场景。固定窗口计数1分钟内最多允许1万请求。实现简单但有边界问题——第59秒和第61秒之间如果各来1万请求就会出现2万请求瞬间涌入。滑动窗口解决固定窗口的边界问题把时间分成小块统计前N个小块的请求数。精度高了但需要更多的内存记录。漏桶算法请求像水一样流进桶里桶以固定速率滴水。不管多猛烈的突发流量出去的速率恒定适合保护下游依赖能力弱的系统。缺点是即使系统有能力也不能瞬间处理积压的大量请求。令牌桶算法以固定速率向桶里放令牌请求来了要拿到令牌才能通过。桶里最多存N个令牌意味着允许一定程度的突发流量。这是用得最多的方案Google的Guava RateLimiter和Sentinel都支持。应用层用代码限流很简单// Guava RateLimiter每秒发放100个令牌 RateLimiter rateLimiter RateLimiter.create(100.0); if (rateLimiter.tryAcquire()) { // 放行执行业务逻辑 } else { // 被限流返回提示或走降级逻辑 }但我建议你在真正的生产环境里优先选择成熟组件比如Sentinel或网关层的限流而不是自己手写。原因很简单限流阈值不是固定的需要根据压测结果和线上监控动态调整一个完整的限流平台应该支持规则动态下发、实时监控和告警自己造轮子维护成本很高。我在项目中用Sentinel做接口维度的限流规则存在Nacos里发现某个接口RT异常时直接调整限流阈值不需要发版就能生效这个体验比手写限流好太多。限流阈值怎么定原则是“以压测数据为准”。我在第1章讲过压测找拐点限流阈值就设在安全水位线上。比如拐点QPS是1000单机限流就设在800。另外不同接口要不同处理核心接口下单、支付限流阈值低一点优先保成功率非核心接口查询历史订单可以把阈值放宽失败了允许用户重试。5.2 熔断别让一个故障拖垮整个链路微服务架构里一个服务挂了如果调用方不做保护所有请求都会堆积在挂掉的服务上调用方线程被占满然后调用方也挂掉故障顺着调用链一路向上传递——这就是所谓的“雪崩效应”。熔断器的思路是当依赖服务的错误率达到阈值比如5秒内错误率超过50%熔断器从关闭状态切换到打开状态后续请求直接短路不再调用下游服务快速返回一个错误。过一段时间后进入半开状态放一小部分试探请求过去如果成功了说明服务恢复了熔断器关闭如果失败继续打开。这个状态机和电路里的保险丝工作原理一模一样。Sentinel和Hystrix都实现了完整的状态机。我在实际使用中更偏好Sentinel它对Java生态的适配性更好而且支持实时监控面板能直观看到每个接口的被熔断流量。5.3 降级用非核心的牺牲保核心的可用降级和熔断经常一起出现但本质不同。熔断是依赖故障时的被动保护降级是主动决策——预期流量会超过容量提前把某些功能关掉把资源让给核心链路。最典型的例子是大促期间关闭商品详情页的“猜你喜欢”推荐位用户点进去看不到个性化推荐但商品信息、库存、下单这些核心功能完全不受影响。另一个常见的降级策略是返回默认值或兜底数据。比如用户签到功能挂了直接返回“签到成功”活动期间的积分后续再补发。这比让用户看到报错页面好得多。面试官考察限流熔断降级最看重的其实是你对“优先级”的判断。不同业务场景下哪些功能是核心、哪些可以被牺牲、降级后用户的体验会打几分折扣这需要业务sense和技术敏感度的结合。能说出“我们把XX功能降级了因为它的ROI最低用户感知也最弱”这比背十遍Sentinel源码更有说服力。6. 最后一问从“知道”到“做过”的距离聊到这里面试题能背的基本都过了一遍。但面试官不会轻易结束。最后一个问题往往是“你说你处理过高并发能不能讲一个你印象最深的线上故障”这个问题是想把你之前所有漂亮的回答拉回地面。因为“知道”和“做过”之间隔着一道最宽的河。6.1 真实故障排查链路的正确打开方式一个合格的线上故障描述应该是这样的去年双11大促当天下午两点开始核心下单接口的RT从80ms逐渐涨到1.2s错误率一度到5%。我们第一时间看了监控大盘发现应用CPU正常、内存正常但数据库的活跃连接数从平时的30涨到了300多几乎打满。进一步查慢SQL定位到一条“按用户查询最近订单”的SQL执行计划没有走索引在数据量翻倍后触发了全表扫秒。临时处理是先用SQL加索引止血然后对该接口加了本地缓存绕过DB查询。事后复盘问题根因是数据链路团队在大促前扩容了订单表的归档策略把近3个月订单全部保留在主库导致订单表数据量翻了三倍执行计划优化器判断全表扫描的代价更小。后续做了三件事一是上线SQL review拦截机制新SQL必须走索引二是对该接口做基于APM的RT告警阈值P99超过200ms自动通知三是大促前的容量测试加入历史数据膨胀模拟场景。这个描述为什么好因为它的每一步都有因果关系指标异常是谁发现的→异常指向哪个层面→如何缩小排查范围→临时止血怎么做→根因是什么→事后怎么避免。面试官从里面能看到你的排查方法论、工具使用习惯和复盘意识。这些恰恰是用人方最想知道的东西。6.2 没做过大型高并发项目的人还有救吗很多人会担心“我所在的业务没这么大的流量简历上怎么写高并发经验”我的看法是高并发的核心技能——压测、缓存、扩容、熔断降级——完全可以在小项目里自己练出来。你完全可以用wrk压一个本地Demo系统看它超过多少QPS后RT开始劣化也可以在业务系统里尝试接入Sentinel给一个普通的列表接口配置限流和降级规则观察流量被拦截时的系统行为还可以在自己负责的服务里加上Prometheus监控把RT、QPS、错误率这些指标面板搭起来。这些东西用到的全部原理和工具和大型系统没有本质区别。区别只在于规模和数据量。面试时你完全可以坦诚“我们业务峰值只有2000 QPS但我在这个规模下完整做过压测、容量评估和缓存改造。这是当时的数据和压测报告。”一个能把2000 QPS写出完整实践链路的人比一个把10万 QPS挂在嘴边但说不出子丑寅卯的人更让人放心。6.3 我面试别人时的真实偏好这些年我面过不少人也总结了一套自己的判断方法我不要求候选人每个高并发组件都精通但他必须有一个真正深入过的领域能把一个技术点的来龙去脉、适用边界、踩坑经历讲清楚。比如你说你懂缓存那我会追着问缓存命中率掉到多少你会报警缓存重建导致DB压力翻倍你怎么办缓存和DB不一致的容忍时长你的业务是多少这些问题没有标准答案但我能从回答里听出你有没有真实的“体感”——一种对系统在极限状态下的行为的直觉判断。这种体感只有和线上故障正面对抗过的人才会有。高并发系统的技术挑战技术细节能学会、工具能熟练、方案能堆叠但把这一切串起来的那个“为什么”的思考方式必须靠实战去磨。从一开始“遇到问题——拆解——定位——修复——复盘”的完整循环里走一遍你才能真正跨过从知道到做过的那条河。最后分享一个我自己的小习惯每接触一个新的技术组件我都会问自己三个问题——它解决什么问题、它引入什么新问题、在什么条件下它不值得用。把这三个问题想明白比把官方文档翻十遍都管用。希望你面完试之后也能拿这三个问题重新审视一遍简历上的每一行字。
返回列表