流量一涨就超时、一挂全挂,很多团队上来就加机器、上消息队列,结果复杂度涨了稳定性没涨。这篇不推销具体技术栈,而是给一套从上到下的分层判断顺序:先量基线、再拆瓶颈、最后才谈冗余和降级,并说清哪些做法在小体量下反而不适用。
流量一涨就超时、一挂全挂,很多团队第一反应是加机器、上消息队列、拆微服务,结果复杂度上去了,稳定性却没跟着上去。问题通常不在技术栈选得对不对,而在于顺序错了:还没有基线就开始扩容,还没找到瓶颈就开始冗余,还没定义目标就开始降级。
这篇不讲具体中间件选型,只给一套从上到下的判断顺序:先量基线、再拆瓶颈、然后才谈冗余和降级,最后说清楚什么阶段不该这么做。每个环节都给可执行的动作和判断标准,以及什么情况下这条经验不适用。模型和接口的实时价格与可用性,可以到 站内价格追踪页 核对当时的数字。
先量基线,再谈架构
没有延迟和吞吐基线,任何扩容都是猜。加机器之前先问自己:现在的 P50 和 P99 各是多少,峰值出现在什么时段,超过某个延迟的请求占比多少。这些数字用压测拿,也可以从真实流量的监控里捞,但一定要有,否则你无法判断改动是变好还是变坏。
- 记录链路耗时占比把一次请求拆成入口网关、应用逻辑、缓存、数据库、外部依赖几段,分别记各自耗时占总延迟的比例。找最粗的那一段,而不是均匀优化。
- 统一并发口径同时在线数、同一秒请求数、排队长度,这三个数说的完全不是一回事。跟产品和运维对齐口径,写进文档,别在会议上各说各话。
- 定义够用的标准写清业务能接受的可用性目标和延迟上限。没有终点线的优化会无限做下去,最后变成成本黑洞。
- 基线要区分常态和峰值,用峰值做容量规划,用常态做成本核算
- 延迟看分位不看均值,均值会掩盖长尾问题
- 压测环境和生产的差异要写在报告里,别把压测结论直接当生产结论
- 监控口径要和压测口径一致,否则数据对不上,排查会更慢
读写分离与热点识别
读写分离和多级缓存不是默认选项,而是针对特定形态的手段。先看读写比例,读多写少的系统加只读副本和缓存收益明显;写重的系统加副本反而增加同步开销和一致性负担,可能比不加还慢。所以动手之前,先把这个系统的读写特征说清楚。
| 特征 | 含义 | 适合的做法 |
|---|---|---|
| 读多写少 | 读远多于写,读放大明显 | 只读副本、多级缓存 |
| 写多读少 | 写是主要压力来源 | 分片、批量合并、异步落盘 |
| 单键高频读 | 同一条数据被反复访问 | 本地缓存加集中缓存 |
| 单键高频写 | 同一个键被高频更新 | 串行化、分片、合并写 |
缓存一定要定失效策略和击穿兜底。缓存集体失效时,所有请求会同时打到数据库,比没有缓存更糟。常见手段是过期时间加随机抖动、热点键永不过期加主动更新、以及缓存未命中时的单飞控制。这些机制要在设计阶段就写清楚,而不是出事之后补。
- 不适用场景:强一致要求高的账务类数据,缓存只能做展示层加速,不能作为读源
- 内部管理系统的读放大通常不明显,上多级缓存收益有限,维护成本反而高
- 热点识别要靠采样和统计,不要凭直觉猜哪个键热,猜错的概率不低
- 缓存和数据库的一致性方案要提前选好,是失效还是更新,选了就别中途换
入口层的限流、排队与背压
限流不是一道闸门,而是分三层做的:网关按来源限,防止单个调用方打满;服务按自身容量限,保护自己不被上游压垮;下游依赖处也限,保护别人。只做一层的话,压力只是从一处转移到另一处,整体稳定性不会变好。
- 定拒绝策略限流触发后是排队等待、快速失败还是降级返回,要按接口重要性分别设定,不能全局一刀切。
- 设最大等待时长排队不是失败,但没有上限的排队等于雪崩。给每个队列设明确的等待上限,超时就拒绝,让上游尽快感知。
- 打通背压信号下游变慢时要能把信号往上传,而不是每一层都自己超时重试。重试次数和超时时间要一起算总账,避免放大下游压力。
- 做资源隔离核心接口和后台任务共用同一套线程池或连接池,一方出问题会拖死另一方。按业务重要性分池,是最省事的隔离手段。
判断限流做得好不好,看一个指标就够了:当流量超过容量时,系统是整体变慢还是只拒绝一部分请求。前者说明没有保护,后者说明限流在起作用。要观察这个指标,可以借助 站内工具目录 里的压测与监控类工具,具体能用哪些到 站内工具目录 页面看当时的收录情况。
冗余、故障域与切换
冗余的关键在故障域,不在实例数量。同一个机房里挂十个实例,挡不住交换机或机房级故障。冗余要跨机架、跨可用区,甚至跨地域,具体跨到哪一层,取决于业务能接受多大的中断和多少成本。
无状态化是水平扩展的前提。会话、上传文件、定时任务这三类是常见的有状态残留,有它们在,加机器解决不了问题。把状态挪到外部存储,实例才能随时被替换和扩缩。
- 故障域
- 一处故障会同时影响的范围,冗余要跨域才有意义
- 无状态化
- 实例本身不保存请求之间的状态,可随时替换
- 自动摘除
- 健康检查失败时把实例从负载均衡里移出
- 多活
- 多个地域同时承载流量,代价是数据一致性变复杂
降级、熔断与兜底
降级的前提是先给依赖排优先级。核心链路依赖必须保,非核心依赖比如推荐、统计、附加信息,坏了可以直接砍掉,让核心流程继续走。很多系统出事不是因为核心挂了,而是被一个非核心依赖拖死。
降级还有一个容易被忽略的点:降级期间的返回结果要有明确语义,是旧数据、空值还是默认值,前端和调用方要能区分。否则用户会以为看到的是实时结果,产生误判,事后排查也会更混乱。
数据层才是真瓶颈
大部分所谓的并发问题,最后都落在数据层。连接池打满、慢查询、锁竞争、大事务,这四个是最常见的隐形杀手。遇到性能下降,先查这四个,再考虑分库分表这种重型手段。
| 排查项 | 典型表现 | 先做的动作 |
|---|---|---|
| 连接池 | 等待连接超时增多 | 看池大小和平均持有时间 |
| 慢查询 | 数据库 CPU 高但 QPS 不高 | 抓慢日志,看执行计划 |
| 锁竞争 | 吞吐上不去、延迟抖动 | 看锁等待和事务粒度 |
| 大事务 | 主从延迟、回滚变慢 | 拆小事务,缩短持锁时间 |
分库分表是最后手段。一旦拆了,跨片查询、分布式事务和运维成本都会上一个台阶。拆之前先确认单表数据量和写入量是不是真的到了瓶颈,很多情况加索引、归档历史数据就能缓解。
容量要留水位线,磁盘、连接数、内存都定好告警线,别等用满了才发现。备份这件事,恢复演练比备份本身更重要,恢复时间决定了你的实际可用性,而不是备份是否成功。
什么时候不该这么设计
前面讲的都是手段,不是默认配置。用户量和请求量都小的系统,加上消息队列、多活、服务网格,只会拖慢迭代速度,出了问题还更难排查。阶段性简单,是一种主动选择,不是能力不足。
- 单体加水平复制能扛住的阶段,不要提前拆微服务
- 没有明确容量压力时,多活和分库分表都先放一放
- 判断标准是瓶颈在不在架构上,而不是代码里一个慢查询或一次同步调用
- 把复杂度预算花在真正会出问题的地方,其余保持简单
架构不是越复杂越稳,而是复杂度刚好覆盖你真实会遇到的那几类故障。下一步把你的链路按入口、应用、缓存、存储、外部依赖拆一遍,记下各段耗时占比和峰值压力;然后到站内降价与可用性追踪页面核对依赖服务的实时状态,再决定哪些环节值得上冗余和降级。→
常见问题
高并发是不是一定要用消息队列?
不是。消息队列解决的是削峰、解耦和异步,如果你的瓶颈是同步调用拖长响应,先优化调用链和缓存可能更划算。只有当写入峰值远高于下游处理能力、或者需要解耦上下游发布节奏时,引入队列才值得,否则只是多了一个要运维的组件。
限流设多少合适?
没有一个通用数字。正确做法是先压测出单实例在可接受延迟下的吞吐上限,再按实例数和安全系数(通常留一部分余量)反推整体阈值,并让阈值可动态调整。可以到 站内 /tools/speed-test 先量出你的延迟基线,再决定留多少余量。
多机房多活是不是高可用的标配?
不是标配,是成本很高的选项。单机房多可用区已经能挡掉大部分故障。多活要额外解决数据同步、流量调度和冲突合并,运维复杂度成倍上升。只有当业务对中断时间的要求高到单机房切换也来不及,并且团队有相应运维能力时才考虑。
降级和熔断有什么区别?
熔断是被动保护:下游错误率或延迟超阈值时自动断开,避免雪崩,恢复靠半开探测。降级是主动取舍:提前决定哪些功能可以被牺牲,返回简化结果或缓存值。两者通常配合用——熔断触发后走哪条路,就是降级策略。
怎么判断架构该不该重构?
看瓶颈位置和改动成本。如果每次扩容都要改代码、故障都出在同一处、且已经影响到业务指标,就值得动。如果只是担心未来量会涨,先把监控和水位告警做起来,等比数据出来再决定,避免为想象中的流量买单。