1. 先别急着加机器崩溃的根子大多在“想”上我在不少团队做过类似“救火”白天还好好的服务到了晚上八点活动一开始监控图当场起飞。DBA在群里刷屏慢SQL前端一直报504运维一边扩容器一边说“资源还有但CPU没上去流量好像没进来”。等大家都折腾完活动已经过去一大半该参加的没参加上该下单的没下单成功复盘的时候才发现真正的问题不是机器不够而是链路里某个环节从设计上就没想清楚流量大了之后会发生什么。这种“流量一上来就崩”的现象几乎每个业务团队都会遇到。很多人第一反应是扩容加机器、加带宽、加连接数但实际效果往往很差。原因很简单如果瓶颈在应用代码的同步阻塞加多少机器都会被同一个共享资源卡住如果瓶颈在数据库的连接池耗尽扩容只会让更多实例抢同一批连接反而把数据库拖得更垮如果瓶颈在某个单点服务比如单实例的定时任务、单节点的Redis、单表的写入锁那无论横向扩多少流量都过不了这个独木桥。所以我想先把话说直白系统崩溃从来不是一个瞬间的事而是设计阶段没把“流量变大之后每个环节会怎么表现”推演一遍的结果。你以为上线了就完事其实真正的工程工作在你按下发布按钮之前就已经决定了系统能不能扛住流量。这里的“想明白”包含三件事想清楚峰值长什么样想清楚流量会走哪些路径想清楚每个路径上的瓶颈在哪里。这篇文章就是围绕这三件事展开的。包括怎么做容量估算、怎么识别链路里的隐形瓶颈、怎么用异步和缓存把关键路径变薄、怎么证明自己压测过了而不是走过场以及流量真来了之后怎么快速定位和止损。适合后端开发、架构师、运维和带技术团队的朋友看也适合那些刚遇到过线上事故、想搞清楚“我到底哪里没想到”的人。2. 提前想清楚容量而不是等流量来教育你2.1 把“峰值”这个词定义准确很多系统崩溃根源是从一开始就把峰值估错了。最常见的做法是拿平均QPS当容量目标比如一天100万请求除以86400秒算出来大约11.6 QPS觉得随便扛。但真实业务不是均匀分布的。一个典型的电商页面白天低谷可能只有几百QPS晚上八点到十点能冲到平均值的几十倍一个抽奖活动开场的瞬间甚至可能是平时峰值的百倍以上。正确的姿势是区分三个数日均QPS、峰值QPS、瞬间冲击QPS。日均QPS用来做资源预算的参考峰值QPS用来定扩容阈值瞬间冲击QPS用来决定限流和降级策略。举个例子假设你日均100万请求按经验晚上高峰期是日均的10倍那峰值大概在115 QPS左右。但活动开场瞬间又有可能再翻5倍那就是接近580 QPS。你的系统必须能在580 QPS下稳定跑5分钟而不是按115 QPS去准备。还有一类容易被忽视的峰值来自重试机制。客户端超时之后自动重试如果配置的是3次重试那系统在故障状态下实际接收的流量可能是正常情况下的3到4倍。你没把重试放大算进去容量永远是算不够的。我自己在复盘时看到过一个数据某接口后端实际收到的QPS是网关转发量的2.8倍就是因为多个客户端层叠重试加上没有全局幂等直接打垮了下游数据库。2.2 画出完整的流量路径不要只看入口容量规划最容易犯的错是只盯着入口网关。你削尖脑袋把Nginx、网关扩容到位了结果流量一进来后面的业务服务、Redis、数据库、消息队列任何一个环节扛不住整体照样崩。所以第二步必须把一条真实请求从进门到响应出门的完整路径画出来。我习惯用一张表来列每一行是一个依赖组件列包括组件类型、部署形态、单实例上限、集群总量、超时时间、是否可降级。比如组件类型单实例上限集群容量超时配置能否降级接入层Nginx10k并发连接5节点3s可降级应用服务Java/Go200 QPS/实例20实例2s核心不可降Redis缓存集群10w QPS3主6从50ms可降级MySQL主从5000 QPS1主2从1s不可降级消息队列Kafka10w msg/s3节点无可降级这个表填完之后你就能一眼看出哪条链路最细。大多数情况下最细的都是数据库写路径。比如一个下单接口前面所有的服务都可以横向扩容但最终所有请求都要写MySQL哪怕你做了分库分表单库的写入能力就摆在那里。十个应用实例打同一个库和一百个实例打同一个库对数据库来说没区别都是那几千TPS的上限。所以画流量路径这个事不是为了画图好看是为了逼着你回答一个问题如果流量翻十倍这条路径上的每一个方框分别是线性扩容、有上限扩容、还是完全不能扩容想不明白这个后面一切优化都是在打地鼠。2.3 给自己留出性能预算别把水位算到极限就算你把每个组件的容量都摸清了也别按100%水位去做预算。任何一个系统在真实运行中都有抖动GC暂停、网络毛刺、磁盘IO写满、其他业务抢占资源都会让实际吞吐低于压测数字。我见过太多团队在线下压测能跑到5000 QPS上线时按4500去配置告警结果一次大促流量刚到3000就出问题。原因就是压测是纯净环境真实环境有大量背景流量和干扰。比较稳妥的做法是给每个关键组件留出40%以上的buffer。假如数据库压测上限是5000 QPS线上日常运行已经在2000 QPS那你最多还能承受某种程度的突发流量一旦预估峰值会到3500以上就必须提前扩容、限流或者降级别硬扛到临界点。说句难听的故障往往就是在你觉得自己“刚好够”的时候来的系统永远比你想象的更脆弱。另外还要注意容量不只是QPS连接数、内存、带宽、文件句柄都可能成为瓶颈。比如高并发下每个请求占用一个连接连接的TIME_WAIT堆积可能把端口耗尽再比如日志量翻倍磁盘IO先打满CPU还没到瓶颈系统先卡死。所以性能预算要有多个维度任何一个维度爆了都会以“系统响应变慢”的形式表现出来。3. 从架构层面把这些“没想到”提前补上3.1 连接池和线程池最容易被手写配置坑死的两个池子流量一上来最先出问题的地方往往不是什么高深架构而是线程池和连接池的配置。它们就像一个房子的门框你其他墙面砌得再厚门框不够宽人照样进不来。线程池的问题我见得最多。很多框架默认的线程池参数是给常规业务用的比如Tomcat默认线程数是200。一旦并发请求超过这个数请求就开始排队队列满了之后新的请求会被拒绝。表面上看起来是服务“没资源了”实际上CPU占用可能只有30%。最可怕的是线程池耗尽后的连锁反应所有请求都在排队等待客户端超时后重试重试又带着新请求进来线程池排队队列越来越长系统吞吐直接归零。正确做法是把线程池参数当成容量设计的一部分。需要算的数是单个请求平均处理时间允许的排队长度最大线程数。公式大概是最大并发 线程数 * (单请求耗时 / 平均响应时间)。如果你希望一个接口在100ms内返回而单请求实际处理要30ms那500线程最多能支撑大约1600 QPS。算完之后再去设置参数而不是用框架默认值。连接池则是另一个重灾区。数据库连接池太小流量一大会导致应用线程都在等待获取连接数据库端连接数却没满整体卡死连接池太大数据库侧资源被连接占满反而拖垮数据库。我在实际项目里见过最离谱的一次应用把数据库连接池从50调到500结果数据库的自身连接数被打满所有查询排队最终QPS比调参之前还低一半。有个经验值可以参考连接池大小不需要追求大按“单连接能处理的QPS×并发需求”来倒推。一个MySQL连接大概能顺序执行每秒几百次简单查询复杂查询可能只有几十次。假设你有20个应用实例每实例需要支撑100 QPS单连接处理能力是50 QPS那么每实例最少需要2个连接加上冗余按4到6个配就够了。很多人一上来配50、100纯粹是心理安慰实际上反而害了数据库。3.2 缓存不是加速器是保命池很多人都把缓存当成性能优化工具说是“让响应更快”。但在高并发场景下缓存更重要的价值是保护后端尤其是保护数据库。没有缓存流量就直接穿透到数据库有了缓存90%的读请求可以止步于Redis这一层。但缓存也会带来三类经典故障击穿、穿透、雪崩。击穿是某个热点key过期的一瞬间大量请求同时打到数据库穿透是查询了一个根本不存在的数据缓存永远不命中每次都落到数据库雪崩是大量key在同一时间段过期导致数据库瞬间被一波查询淹没。解决办法其实是组合拳。热点key不要设置固定过期时间可以用逻辑过期或者永不过期加后台异步刷新空结果也要缓存设置一个较短的过期时间过期时间加随机值避免大量key同时失效另外还可以在应用层加singleflight同一时刻只有一个请求去查数据库其他请求等待结果复用。这些手段每一样都不复杂但很多人到了线上才想起要加那就晚了。我记得有次帮一个电商团队看事故他们首页有一个商品推荐位缓存key过期时间设在零点整。结果零点一过几十万人同时刷新首页Redis里那一批key全部失效请求瞬间穿透到MySQL数据库IO直接打满整个首页癍痪了十几分钟。事后修复就加了一个随机过期时间问题再没出现过。你看就那么一行配置的事但没提前想明白就是事故。3.3 同步改异步削峰填谷是基本功有些操作不要求用户马上拿到最终结果比如下单后的积分发放、日志记录、消息通知、库存扣减之后的异步对账。这些场景如果都做成同步调用流量高峰时系统的响应时间会被这些非核心操作拖长核心交易的吞吐也跟着下降。改造思路是把这些操作丢进消息队列让消费者按照自己的节奏去处理。这样前端请求只写一条消息立即返回后端消费者可以批量处理、延迟处理、失败重试在流量高峰时起到削峰填谷的作用。举个简单的例子一个下单接口原本要同步调用会员服务发积分、调用短信服务发通知、调用库存服务减库存总耗时500ms。改成异步之后只保留减库存这个强一致操作其他都走消息队列接口直接变成80ms系统能支撑的并发瞬间上了一个台阶。我知道有人会担心异步带来的问题消息丢失怎么办、重复消费怎么办、数据一致性怎么保证。这些担心都对也都可以解决。消息丢失靠生产者确认和消费者手动提交重复消费靠幂等表或者唯一键约束数据一致性靠本地消息表加定时对账。关键是你要判断这笔交易值不值得引入异步而不是无脑异步化。核心原则是用户能感知到“他在等”的操作尽量同步用户感知不到或者能接受延迟的全部异步。3.4 熔断和降级让系统的失败是局部的而不是整体的很多人把熔断降级当成微服务框架的一个配置开关觉得“开了就行”。其实不是熔断和降级的核心是你要提前想清楚哪些依赖挂了可以放它挂哪些功能在压力大时可以主动关掉保护核心链路。我举一个实际的例子。一个内容社区App信息流接口依赖推荐服务、用户服务、评论服务、广告服务。一次大促期间广告服务因为外部接口变慢响应时间从50ms涨到3秒信息流接口因为同步调用广告服务整体耗时被拖到3秒以上用户疯狂下拉刷新直接打垮了整个网关。如果把广告服务加上熔断连续失败10次就断开走降级返回“无广告”的空列表信息流接口就能保持200ms用户无感知损失的只是广告收入的一小部分。做降级很容易真正难的是敢不敢在故障时把开关拨下去。很多团队熔断配置了但阈值设得太高等真正触发的时候系统已经不可用了。我的建议是熔断阈值要保守一点宁可误伤也不能拖垮核心链路。比如下游接口失败率超过10%立即熔断5秒而不是等到50%再来处理。同时配合半开状态做试探恢复成功几次后自动关闭熔断。这套机制在正常情况下的确会牺牲一点可用性但能在真正的流量洪峰面前保住你的命。4. 压测和容量评估怎么证明自己“想明白了”4.1 压测的三种姿势一种都不能少我之前见过太多团队的压测就是拿着Jmeter跑到一定量级看了一眼没报错就宣布“系统可以扛住了”。这种压测充其量叫“跑一遍”不叫验证容量。真正能说明问题的压测至少要覆盖三种模式。第一种是阶梯加压从低并发开始每过一段时间增加一批压力观察系统在哪个点开始出现错误。这种模式能找到系统的“拐点”。比如100并发时RT都在100ms内200并发时RT突然变成2秒那你系统的真实承载上限就在100到200并发之间而不是测试报告里写的“通过”。第二种是尖峰压力模拟突发流量。活动开始瞬间的流量特点不是慢慢涨上去而是几十秒内从低谷跳到峰值。压测时直接在平稳压力基础上瞬间加上5倍的并发看系统能不能撑住会不会出现连接被拒绝、线程池满、超时飙升。第三种是长时间稳定性测试用小压力跑30分钟以上。很多内存泄漏、连接泄漏、文件句柄耗尽的问题短时间压测根本暴露不出来。我遇到过一例系统在高并发下稳定了两个小时然后突然崩溃一查是某个定时任务每次执行都新建了一个HTTP客户端两小时累积了上千个连接没释放。这种事情不超过时间压测你永远发现不了。4.2 压测要看什么数据别只盯着QPS压测不是跑完就完了关键是看数据。除了QPS你至少要记录四类指标响应时间的分布、错误率、系统资源、依赖组件的健康状态。响应时间不能只看平均值要看P95和P99。平均值100ms可能掩盖了5%的请求耗时2秒。在大流量场景里P99才是用户体感的关键。错误率也一样不是为零就万事大吉要看具体是哪类错误超时、拒绝连接、返回5xx、还是业务层的异常。每类错误的处理方法完全不同。系统资源要结合QPS和RT一起分析。如果QPS没上去但CPU已经100%说明代码有计算瓶颈如果CPU不高但RT已经飙升大概率是锁等待、IO等待或者线程池排队如果内存持续上涨GC越来越频繁那基本可以断定有内存泄露或者大对象分配问题。还要监控下游组件数据库的连接数、活跃连接数、慢查询数量Redis的命中率、内存使用、慢命令消息队列的积压数量。这些数据能帮你判断瓶颈到底出在哪个环节。压测报告里如果没有这些维度那这份报告基本没有参考价值。4.3 压测之后的调整闭环压测不是做完就结束了发现问题要修修完要再压。我讲一个标准流程第一轮压测跑到1万并发发现P99从200ms涨到5秒数据库出现大量慢SQL。通过慢日志定位到某条查询没有命中索引给表加上索引。第二轮压测P99降到800ms但Redis命中率只有60%大量请求穿透到数据库。增加缓存key、调整过期策略命中率提高到95%。第三轮压测P99稳定在300ms数据库连接数保持在安全范围但发现消息队列积压超过10万条。增加消费者实例数量之后再做第四轮确认。这个流程看起来很简单但很多团队在第二三轮就停下来了因为他们觉得“已经比以前好了”。问题是你的容量目标是1万并发如果第三轮还没有达到就必须继续调整直到目标达成或者明确知道瓶颈在哪、用多少资源能解决问题。压测的价值恰恰在于逼你在上线之前把这些问题暴露出来而不是把隐患带到线上让真实用户替你踩。5. 流量真来了运行时怎么发现和止损5.1 监控指标先看这几个就够了流量高峰期间监控页面可能几十张图都在报警这时候最忌讳的是眉毛胡子一把抓。我自己的习惯是固定看五类指标入口流量、应用RT和错误率、线程池活跃数、数据库活跃连接数、消息队列积压量。入口流量决定你当前压力的大小RT和错误率决定系统健康程度。线程池活跃数是最灵敏的预警信号它突然涨到接近最大值说明系统处理不过来哪怕当前CPU还不高这个信号会先于CPU到顶。数据库活跃连接数则是另一个提前量它涨到80%以上通常几分钟后就会出现连接池耗尽。消息队列积压量则代表异步链路是否还能消化流量。我建议把这五个指标做到一个Dashboard上不用多一个页面就够。流量高峰期你的精力要放在判断和决策上而不是在各种图表之间来回切换。5.2 日志和链路追踪定位问题的两条腿当错误率开始异常时最怕的就是看到一堆日志但是不知道从哪看起。没有链路追踪的情况下一个请求经过五六个服务你根本不知道在哪个环节慢了、哪个环节出错了。这是很多人到了线上才想起“当时怎么不接链路追踪”的原因。实践上的建议是不用追求接入最复杂的全链路系统用最简单的方案就行。日志里带上全局traceId每个服务打印请求进来和出去的耗时错误日志里带上入参和关键状态码。这样一旦出问题直接grep traceId就能把整条链路的耗时看个大概。如果团队规模允许再上一个轻量级的链路追踪工具把所有服务的调用关系自动串起来。有一个容易踩的坑是日志打太多。高并发下如果每个请求都打印完整参数和返回结果日志系统的磁盘IO会先被打爆反而成了性能瓶颈。我见过某服务平时每天日志2GB高峰期一天涨到50GB最后磁盘满了应用直接崩溃。正确的做法是入口和出口各打一条包含traceId、耗时、状态码参数和返回结果只在错误时打印通过日志级别动态控制别在正常路径上输出无用信息。5.3 应急三板斧限流、降级、熔断真到了系统扛不住的那一刻别想着“再优化一下代码”来不及了。你要做的是止损三板斧按顺序来。第一板斧是限流。在网关或者入口层对核心接口设置最大QPS上限超过的请求直接返回友好的错误提示或者排队等待。这看起来很粗暴但至少保证了系统的可用性而不是大家一起死。限流的阈值绝不是拍脑袋想出来的就是压测时得到的拐点数据比如P99能保持在500ms内的最大QPS是3000那就限流到2500给自己留点余量。第二板斧是降级。把非核心链路的功能关掉。比如推荐位、评论、点赞这些功能返回默认值把资源让给下单、支付这些核心链路。降级不是“坏了再关”而是提前就做好配置开关遇到情况一键切换每个开关背后带上责任人确保拨下去有人会观察效果。第三板斧才是熔断。下游依赖挂了不能拖着主链路一起挂熔断器自动打开快速失败。在应急场景下熔断是保护上游的最后防线。说到底这三板斧不是高技术含量的活儿难的是提前把开关都备好。我见过不少系统限流代码写了但是阈值没配置降级接口做好了但是没有开关熔断器装了但触发条件太苛刻。真出了问题这些机制全都形同虚设。等到下次做架构评审的时候把“应急开关是否可用”当作一个正式验收项你就不用在半夜三点手忙脚乱了。6. 复盘下来“提前想明白”到底是什么意思做过的故障复盘多了之后我越来越觉得大部分事故都不是“运气不好”而是“当时确实没想到”。我自己带项目的时候会强制自己过一个清单每次发布前都做一遍看起来麻烦但每次都帮我堵掉了不少问题。清单拿出来分享一下有没有预估过峰值流量包括活动开场瞬间冲击和重试加倍流量路径上的所有依赖组件有没有各自的容量上限数据数据库连接池和线程池参数是不是按承载目标算过而不是用默认值缓存的那几个经典问题击穿、穿透、雪崩有没有对应的兜底方案非核心操作有没有被同步阻塞在关键路径上能不能改成异步熔断、限流、降级是不是都配置了真实可用的阈值而不是开着没设参数压测有没有做过尖峰测试和长稳测试数据里有没有P99和资源指标线上日志能不能支持快速定位链路会不会在高峰时变成瓶颈应急演流程里每个开关有没有责任人能不能在5分钟内找到并操作这个清单并不复杂但每一条背后都是踩过的坑。你可以把它当成一篇普通文章看完就关掉也可以在下一次给自己负责的系统做容量评估时逐条打钩。区别不在于技术高低而在于你是不是愿意在流量到来之前先把那些不舒服的问题想清楚。流量不会等人但你可以提前准备好。