简介:这是聚焦大型应用系统稳定性的实战型PPT,内容整理自新浪微博稳定性经验谈,适合系统架构师、后端研发与运维人员参考。资源共1个文件,为可直接浏览和分享的PPTX演示文稿(约37页),压缩包大小3.12MB,目前已有184人学习。内容围绕‘少出问题、快速解决、清楚系统健康状况趋势’三条主线展开,先梳理依赖异常、网络硬件故障、流量突增、代码bug等稳定性影响因素,再通过分层隔离、SLA保证、超时控制、谨慎重试、容量规划、Failover策略、灰度发布等Design For Failure手段逐层构建防线,并介绍多IDC容灾、限流、降级与紧急扩容等容灾预案,最后引入Touchstone在线容灾演练,帮助读者验证预案有效性。通过学习可借鉴新浪微博在高并发场景下的稳定性设计思路,用于自身系统的故障预防、容量评估与应急机制建设。
1. 从微博热搜看大型应用系统架构设计的常见误区
凌晨两点,热搜榜上一条突发新闻把请求量从平时的 2 万 QPS 推到 30 万,很多团队的第一反应是加机器。但如果加机器就能解决,就不会有那么多公司在活动大促时依然翻车。真正的问题往往出在架构设计阶段:你给系统设计了功能,却没有给系统的失败设计路径。大型应用系统架构设计里最容易被低估的部分,是系统稳定性设计——它不是监控告警的堆砌,也不是应急抢修的手忙脚乱,而是一套把不确定性消化在架构内部的机制。新浪微博这套体量下的稳定性经验,核心不是某个组件,而是把容量、流量、数据、发布这些环节串成一条完整的韧性链路。接下来顺着这条链路展开,适合想把自己的系统从“能跑”推到“倒了也能快速爬起来”的工程师。
2. 系统稳定性设计第一关:用压测给容量定基线
2.1 为什么先算容量而不是先做限流
稳定性设计最常见的失败,是限流阈值和熔断时间全靠拍脑袋。阈值定小了,正常用户也被拦住;定大了,流量压上来时数据库先被打挂。微博这种体量的系统,任何一个全局参数出错,影响的都是百万级用户。所以我的常规做法是,先把容量基线测出来,再讨论限流配置。容量基线一句话解释:在指定延时容忍度下,单机或单集群能扛住的最高压力。有了这个数字,限流阈值、扩容步长、降级开关才有依据。
容量压测不是越猛越好,关键在于模拟真实流量特征。真实流量的特征包括请求量随时间的变化、热点 key 的集中度、读写比例,以及超时重试的放大效应。如果压测只发均匀请求,结果往往偏乐观。微博经验里有一句话值得记住:压测的目的不是证明系统能抗住,而是找出系统在哪个压力点开始劣化。后面的稳定性动作都是围绕这个劣化点展开。
2.2 用 wrk 给单个服务定一个 QPS 基线
最小可行的压测工具是 wrk,它用一个线程模型就能在单机上打满千兆带宽。下面这条命令可以快速测出单个无状态服务的瞬时容量:
wrk -t8 -c400 -d60s --timeout 2s --latency http://api.example.com/feed-t8表示开 8 个线程,-c400表示同时保持 400 个 HTTP 连接,-d60s表示持续压测 60 秒,--timeout 2s让慢请求在 2 秒时按失败算,--latency会输出完整的延迟分位表。注意压测连接数不要一次给太大,先从小并发开始,逐步增加,观察服务端的 CPU 和 GC 指标,避免把压力全堆在网络上。
压测开始前要记录三个基线值:空闲时 CPU、内存、GC 频率。压测结束后,重点看延迟分位数(p50、p95、p99、p99.9)而不是平均值。平均 RT 在长尾分布下会骗人,p99 一旦开始指数级飙升,说明系统已经进入排队区,这时候的 QPS 就是容量拐点。常见情况下,8 核 16G 的 Web 服务在 RT 50ms 时,容量拐点大约在 500 QPS 附近;但这个数字会因语言、框架和下游访问次数产生巨大差异,所以必须实测。
我一般把压测结果整理成下面的表,作为容量评估的输入:
| 指标 | 压测前基线 | 容量拐点 | 目标水位 |
|---|---|---|---|
| QPS | 0 | 1800 | 1200 |
| RT p99 | 10ms | 200ms | 80ms |
| CPU | 8% | 75% | 50% |
| GC 暂停 | 20ms | 120ms | 60ms |
2.3 从容量拐点推导机器数和扩容阈值
有了容量拐点,机器数的计算就变成了简单的除法:线上流量预估峰值除以单机容量,再乘一个冗余系数。以微博超话这种读多写少的场景为例,峰值 20 万 QPS,单机拐点 1800 QPS,冗余系数 1.5,那么至少需要 20 万 / 1800 * 1.5 = 167 台。但这个数字只是起点,还要给多集群容灾留出 2 倍余量,否则一台机器故障就会把剩余机器打到拐点之外。
真正让容量评估落地的是扩容阈值。我通常以容量拐点的 70% 作为水位红线,红线以上自动触发扩容或流量调度。比如上面的例子,单机达到 1260 QPS 就应当扩容。这里有一个容易踩的坑:很多团队只在入口层观察 QPS,忽略了服务内对数据库和缓存的实际请求量。微博这种系统里,一次用户请求可能放大成 20 次对后端的调用,所以容量评估要分别做“入口 QPS”和“依赖 QPS”两张表,才能找到真正的瓶颈。
容量基线不是一次性的,每次上线新功能或改动数据模型后,拐点都可能发生变化。我见过团队三个月前压测过,之后接口里加了全量数据扫描,压测数据完全作废。可靠的做法是把压测变成回归项,至少每个迭代跑一次轻量压测,用阈值告警代替人工判断。
3. 限流、降级与熔断:流量冲击下的稳定性闸门
3.1 限流保护谁:三条链路的优先级
容量基线解决的是“能扛多少”的问题,限流解决的是“扛不住时先保谁”的问题。很多人把限流理解成挡用户,其实限流首先保护的是下游依赖。在微博这种系统里,最怕的不是入口被打爆,而是入口还在正常返回,数据库连接池却被拖垮,导致所有请求都卡在 SQL 层。所以限流的配置顺序是:先给数据库和缓存设置连接数上限,再给入口 API 设置流量阈值,最后才轮到业务消息队列。
这里的核心矛盾是“有人被拒绝”和“所有人都不可用”。如果必须选,我会选择让一部分用户收到“稍后再试”,而不是让整个服务超时到不可用。限流阈值应该来自第 2 章压测出来的依赖 QPS,而不是拍脑袋的整数。比如数据库连接池上限 100,每个连接能处理每秒 10 次查询,那依赖侧的限流阈值就设在 900 QPS,留出一成缓冲。
3.2 用 Redis + Lua 实现令牌桶限流
最常见的限流算法是固定窗口和令牌桶。固定窗口会在窗口边界出现双倍流量,令牌桶允许一定的突发流量,更适合应对热搜带来的瞬时尖峰。微博场景中,基于 Redis 的分布式令牌桶是常见做法,核心逻辑用 Lua 保证原子性:
local key = KEYS[1] local capacity = tonumber(ARGV[1]) local rate = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local last = redis.call('get', key .. ':ts') local tokens = redis.call('get', key .. ':tk') if not last then last = now tokens = capacity end local elapsed = now - last tokens = math.min(capacity, tokens + elapsed * rate) redis.call('set', key .. ':ts', now) if tokens < 1 then return 0 end redis.call('set', key .. ':tk', tokens - 1) return 1这段脚本里,capacity是桶的最大容量,决定突发流量最多放行多少;rate是每秒补充的令牌数,决定长期平均速率;now必须由调用方传入,用 Redis 自身的时间会导致不同实例时钟漂移。脚本先读取上次时间戳和令牌数,计算过去这段时间应补充的令牌,再决定是否放行。调用方通过EVAL保证并发环境下不会出现重复扣减令牌。
注意一个关键参数:rate应该设置为容量基线的 80%,而不是容量拐点的 100%。当令牌桶在满速运行时,网络的轻微抖动也会让延迟升高,按拐点 100% 设置限流等于把自己扔到悬崖边缘。上面的写法和通用令牌桶有一个差异:令牌在请求时才补充,而不是后台定时补充。这套逻辑对 Redis 的压力非常小,单条脚本指令的耗时通常在 0.1ms 以内。
3.3 降级和熔断:从“拦流量”到“绕开故障”
限流是入口动作,降级则是把非核心功能临时摘掉,把资源留给核心链路。微博这种系统里,评论列表可以暂时变为“加载失败”,但信息流必须保持可用。降级开关的设计要粒度细、生效快,一个开关控制一个原子功能,开关变更通过配置中心下发,不能在代码里做 “if (开关) return null” 这种处理,而应该用持久化的开关配置。
熔断器与降级不同,它根据调用结果自动打开,保护下游不被打垮。触发条件可以参照下面的表格:
| 组件 | 触发条件 | 恢复策略 | 注意事项 |
|---|---|---|---|
| 限流 | 当前 QPS 超过阈值 | 过一段时间自动恢复 | 阈值随容量基线调整 |
| 降级 | 人为标记离线 | 人工打开开关 | 开关要可灰度 |
| 熔断 | 错误率 > 50%,且请求量 > 200/min | 30 秒后尝试放行少量流量 | 半开状态的试探比例决定恢复速度 |
我在生产环境里常用的是半开熔断:熔断打开后,每 5 秒放行 1% 的请求进入,如果成功则逐步扩大放行比例,直到完全关闭。这个探活比例不能太高,否则恢复瞬间又被打进故障状态。熔断器记录的错误率要按滑动窗口计算,窗口一般采用 60 秒,太短会灵敏到误伤,太长会让故障持续时间被拉长。
4. 缓存穿透、击穿与雪崩:微博热点场景的稳定性陷阱
4.1 三个缓存问题的本质差异
缓存是微博稳定性设计里最容易被写进复盘的部分。热点事件突然把某个 key 的访问量推高,带来的是缓存击穿;请求带了一大批不存在的 key,打穿缓存直接打到数据库,这是穿透;缓存集体失效,数据库流量瞬间增长几倍,这是雪崩。三者现象相似,但应对手段不同,很多人混为一谈,导致方案和问题对不上。
穿透和击穿的区别在 key 是否真实存在。穿透针对的是恶意构造的不存在 ID,击穿针对的是热点 key 过期,雪崩针对的是大量 key 在同一时刻过期。用一个表格来对比:
| 问题 | 核心原因 | 数量级特征 | 首选手段 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 大量请求命中空值 | 布隆过滤器或缓存空值 |
| 缓存击穿 | 热点 key 过期 | 单 key 高并发重建 | 互斥锁或提前预热 |
| 缓存雪崩 | 大量 key 同时过期 | 缓存层整体失效 | 过期时间加随机值,多级缓存 |
微博经验谈里最值得借鉴的是对热点 key 的主动发现。常见做法不是等击穿发生再处理,而是通过采集 key 的访问频次,把超过一定阈值的 key 提前记录为热点,然后对该 key 设置更长的过期时间或使用本地缓存兜底。热点识别本身也是稳定性能力的一环,需要有实时流计算或定时扫描的保障。
4.2 用互斥锁处理缓存击穿的一个可运行思路
热点 key 过期后,如果几十个请求同时回源数据库,数据库可能直接被压垮。常规手段是让其中一个请求去重建缓存,其他请求等待或返回旧值。下面是一段常见的 Java 伪代码逻辑:
String value = redis.get(key); if (value == null) { // 使用 SETNX 加锁,5 秒自动过期避免死锁 boolean locked = redis.setnx(key + ":lock", "1", 5); if (locked) { value = db.query(key); redis.setex(key, 300, value); redis.del(key + ":lock"); } else { Thread.sleep(100); value = redis.get(key); if (value == null) { // 降级:返回旧缓存或默认值 value = "hot"; } } } return value;这段逻辑的关键在两个方面:SETNX的原子性和锁的过期时间。分布式环境中,如果重建耗时超过 5 秒,锁被自动释放,另一个线程可能同时进入重建流程。实际使用时要给锁加上“重建线程 ID”,只有拿到锁的线程才能删除锁。这里为了让逻辑可读,我简化了锁续期,更安全的做法是使用 Redisson 的看门狗机制,或者按时长分段续期。
上述写法还有一个代价:如果db.query本身延迟不稳定,重建缓存的时间会很长,等待的请求会全部卡在Thread.sleep(100)这段重试上。微博的应对是加一层本地缓存。当 Redis 中 miss 时,先查本地 Caffeine 缓存,本地也没有才尝试加分布式锁。本地缓存过期时间设为 30 秒左右,可以把对 Redis 的压力降低一个数量级,同时让重建缓存期间的请求不再穿透到数据库。
4.3 缓存过期时间为什么要加随机值
雪崩的典型诱因是代码里把同批 key 的过期时间设成同一个值,比如 “所有用户信息缓存 10 分钟”。10 分钟一到,准备大促活动或定时任务,一次性请求十几万用户,缓存层瞬间全部失效。避免的手段很简单:在原有过期时间上加上一个随机偏移量。比如设置过期时间为300 + RandomUtil.randomInt(0, 120)秒,让 key 的失效时间离散化。
注意随机值不能太大,太大的随机偏移会让缓存数据的实际新鲜度不可控。经验值是在基础过期时间的 20% 到 40% 之间取随机值。对于热点 key,还可以通过内网配置中心单独指定不失效,改为定时主动刷新。这种策略下,热点 key 的稳定性从“被动等失效”变成了“主动续命”,代价是会增加代码复杂度,需要结合团队维护能力来选。
5. 多机房容灾与数据同步:把不可用时间压缩到分钟级
5.1 同城双活与异地多活的取舍
微博这类体量,机房级故障不是“会不会发生”的问题,而是“什么时候发生”。稳定性设计演进到一定阶段,必须处理数据中心不可用的情况。同城双活的方案是两机房同时承担流量,任何一个机房故障,流量秒级切换到另一个机房;异地多活则把数据分布到多个城市,抗更高级别故障,代价是数据一致性处理难度大得多。
这里有一个常被忽略的前提:双活的前提是“两端都能活”,如果一端入口和数据库同时故障,另一端要能接收全部流量。所以多机房设计的第一步,是确认每个机房都有完整的服务依赖和独立的数据库副本。微博级别的经验是:宁可让一个机房少接 10% 流量,也不能让某个机房缺少核心依赖。架构设计上常见的失败模式,是把次要服务做了多机房,核心入口却还指着单一机房数据库。
5.2 通过延迟监控发现同步瓶颈
多机房的数据同步通常依赖中间件或 binlog 订阅。常见方案是 MySQL 主从复制加消息队列做异步写。为了及时发现问题,需要监控从库的Seconds_Behind_Master指标。一条快速的检查命令如下:
mysql -h slave001 -uroot -p -e "SHOW SLAVE STATUS\\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running"Slave_IO_Running和Slave_SQL_Running都必须是Yes,任一个为 No 说明复制线程中断,Seconds_Behind_Master的数值表示从库落后主库的秒数,超过 30 秒就要自动告警。注意这个指标在某些场景下会有跳变,因为它是根据 binlog 的时间戳和本地重放时间估算的,最可靠的方式是结合监控曲线看趋势,而不是单看某一时刻。
但主从复制解决不了跨机房的业务一致性。比如用户在某机房发了一条评论,请求落到 A 机房,而读请求被调度到 B 机房,如果 B 机房的数据还没有同步,用户就看不到自己刚发的评论。微博对它的一致性要求是最终一致,但必须保证“重要场景读己之写”。做法是将写入请求的会话绑定到同一个机房,通过会话级路由让用户读到刚写入的数据,再通过异步同步把数据复制到其他机房。
5.3 用可用性预算推动容灾演练
多机房容灾设计最终要落到演练频率上,否则平时看着没问题,故障出现时才发现切换脚本失效。我一般把容灾演练的目标量化成“可用性预算”:比如一个季度内,A 机房故障导致的流量中断最多 10 分钟,那么每季度至少组织一次机房级切换演练,并且要使用真实的读写流量,而不是只在预发环境模拟。
这里有一个表格可以用于制定演练计划:
| 演练项目 | 频率 | 通过标准 |
|---|---|---|
| 单机故障演练 | 每月 | 30 秒内自动摘除流量 |
| 单机房断网 | 每季度 | 5 分钟内切流,数据丢失为 0 |
| 缓存集群故障 | 每半年 | 依赖缓存降级后核心接口可用 |
| 数据库主从切换 | 每季度 | 30 秒完成,无数据回滚 |
这些演练项目需要提前确定回滚策略,一旦演练过程中出现预期外问题,要能一键切回原状态。切回动作本身也要纳入演练。我见过多个团队演练时只测了主故障、没有测回滚,结果下午真的故障时,切过去容易、切不回来难。
6. 故障演练与复盘:让稳定性经验从个人英雄主义变成组织能力
6.1 混沌实验:从“控制变量”到“随机注入”
稳定性的最高级做法,是主动探测系统在异常下的行为。混沌工程不追求完全模拟故障,而是为系统注入不确定性,比如随机杀掉一台机器、延迟某条链路的网络 50ms、让某个磁盘写满。最小可用的混沌工具是 ChaosBlade,下面命令可以让 CPU 负载升高到 90%,用来验证服务的自动扩容和降级是否按预期工作:
blade create cpu load --cpu 90 --timeout 120s这个命令会在当前节点上制造 90% 的 CPU 使用率,持续 120 秒。执行前先确认监控告警已开启,否则实验只会得到一片告警噪音。
混沌实验的重点不是“破坏”,而是验证系统在破坏后的自愈。每次实验前写下预测:故障发生后多少秒内出现告警,多少秒完成自动摘除,多少秒恢复。实验结束后对比预测和实际,误差超过 30% 就说明稳定性机制没有可信度。
6.2 复盘改进项必须能自动验证
故障复盘最怕的产出是“加强监控、对代码进行 review、提升运维能力”这种无法验证的废话。有效的改进项必须落到一个可以执行和验证的动作上。一个可用的模板是把复盘结论转化为“一个配置变更 + 一个自动化检查项”。比如某次故障是因为缓存过期时间设置了同一个值,那么改进项就是同时提交“过期时间加随机值”的代码,和一条 CI 检查脚本,确保新代码里不允许出现无随机偏移的缓存过期配置。
复盘会议不是用来追责的,而是用来补全系统的失效模型。建议每次复盘会后整理一份“失效模式清单”,把已知的故障模式、触发条件、检测方式和逃生手段写成表格,作为下一阶段稳定性设计的输入。这样累积两三年后,团队对自身系统的理解深度会明显超过任何外部资料。
最后分享一个技巧:在引入新的稳定性机制时,优先选择“能够直接暴露问题”的方案,而不是“看起来能让系统更稳”的方案。比如限流和降级,与其加一堆规则配置,不如先设计一个强制走演练的机制。下次做架构评审前,先问问自己:这条稳定性机制,能在哪一次混沌实验里被触发?
本文还有配套的精品资源,点击获取