ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

大型应用系统架构设计:稳定性设计与高并发防护实践

大型应用系统架构设计:稳定性设计与高并发防护实践 简介这是聚焦大型应用系统稳定性的实战型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 附近但这个数字会因语言、框架和下游访问次数产生巨大差异所以必须实测。我一般把压测结果整理成下面的表作为容量评估的输入指标压测前基线容量拐点目标水位QPS018001200RT p9910ms200ms80msCPU8%75%50%GC 暂停20ms120ms60ms2.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/min30 秒后尝试放行少量流量半开状态的试探比例决定恢复速度我在生产环境里常用的是半开熔断熔断打开后每 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_RunningSlave_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 检查脚本确保新代码里不允许出现无随机偏移的缓存过期配置。复盘会议不是用来追责的而是用来补全系统的失效模型。建议每次复盘会后整理一份“失效模式清单”把已知的故障模式、触发条件、检测方式和逃生手段写成表格作为下一阶段稳定性设计的输入。这样累积两三年后团队对自身系统的理解深度会明显超过任何外部资料。最后分享一个技巧在引入新的稳定性机制时优先选择“能够直接暴露问题”的方案而不是“看起来能让系统更稳”的方案。比如限流和降级与其加一堆规则配置不如先设计一个强制走演练的机制。下次做架构评审前先问问自己这条稳定性机制能在哪一次混沌实验里被触发本文还有配套的精品资源点击获取
返回列表