简介:一份聚焦大型网站高性能、高并发与高可用架构设计的系统化文档,面向具备一定IT基础、关注系统架构与性能优化的工程师及架构师,帮助梳理从架构目标、模式选型到分层优化的完整思路。内容围绕大型网站特性展开,覆盖分层、分割、分布式部署、集群、缓存与异步等架构模式,并延伸至前端、浏览器、应用层、代码层和存储层的性能优化,以及高可用保障、可伸缩与可扩展设计、安全管理和电商网站架构演进案例。压缩包共1个docx文件,约3.19MB,便于集中阅读与检索。已有1124人学习。读者可从中获得架构设计原则、各层级优化策略、冗余备份与失效转移思路,以及结合案例理解系统成长轨迹的参考,适合用于大规模扩展前的方案梳理与高并发场景下的架构复盘。
1. 从一台服务器到七层逻辑架构:这份指南解决的是「没人算过」的问题
某次大促前 40 分钟,监控面板上的 QPS 从 3000 直接顶到 9000,应用服务器 CPU 打到 95%,数据库连接池在 30 秒内被占满,前端开始大面积超时。事后复盘,崩掉的不是某段代码逻辑,而是没人算过单机到底能扛多少、缓存整批失效时兜底路径是什么、主库挂了谁接管。这份高性能、高并发、高可用的架构设计指南,讲的就是把这些「没人算过」的东西提前算清楚:十种架构模式的适用边界,前端到存储五层的性能优化落点,容量预估的换算公式,多级缓存与读写分离的参数口径,以及可用性指标的拆解方式。它面向的是正在把单体系统往分布式推进的团队——架构师拿它做选型对照,后端负责人拿它对部署图,运维拿它写演练清单。下面按我实际落地的顺序拆。
2. 架构模式落地:十种模式里真正决定成败的是分层、集群和缓存
指南列了十种架构模式:分层、分割、分布式、集群、缓存、异步、冗余、安全、自动化、敏捷。刚接触的人容易把它当成十个并列选项,实际用起来不是这么回事——分层和分割决定系统骨架,分布式和集群决定能不能扛住量,缓存和异步决定响应时间,冗余决定故障时能不能活下来,剩下的安全、自动化、敏捷是长期维护成本。这一章按「骨架 → 扩容 → 提速」的顺序拆,每个环节给出可对照的部署参数。
2.1 七层逻辑架构的职责边界与一次请求的完整路径
指南给的七层逻辑架构是:客户层、前端层、应用层、服务层、数据层、大数据存储层、大数据处理层。很多人看完记不住,是因为没把一次真实请求串进去。一个用户点开商品详情页,路径大致是:客户端发起请求 → DNS 解析到最近接入点 → CDN 命中静态资源直接返回 → 未命中的动态请求落到反向代理 → 反向代理按路径分发到应用集群 → 应用层调服务层的商品服务、库存服务、评价服务 → 服务层查缓存,缓存未命中再落数据库 → 埋点日志异步写入大数据存储层,离线任务在夜里跑批。
把这条路径画出来,每一层的职责边界就清楚了:
| 层级 | 主要职责 | 典型承载 | 常见单点风险 |
|---|---|---|---|
| 客户层 | 请求发起、本地缓存 | 浏览器、移动端 | 客户端版本碎片化导致接口兼容问题 |
| 前端层 | 静态资源、接入调度 | CDN、DNS 轮询、反向代理 | DNS 缓存未过期,切流不生效 |
| 应用层 | 业务编排、页面渲染 | 应用集群 + 负载均衡 | Session 未外置,扩容后登录态丢失 |
| 服务层 | 公共能力复用 | RPC 服务集群 | 无超时、无降级,一个慢服务拖垮全链路 |
| 数据层 | 持久化、读写分离 | 关系库主备、分布式缓存、分布式文件系统 | 主从延迟导致写后读不到 |
| 大数据存储层 | 日志与行为数据归集 | 消息队列 + 批量落地 | 采集端阻塞业务线程 |
| 大数据处理层 | 离线与实时计算 | 批处理、流式计算 | 与在线库争抢 IO |
这张表我一般贴在部署文档第一页。它的价值不在分类,而在于「每一层都要能回答:这一层挂了,上一层的表现是什么」。答不上来的那一行,就是你的隐藏单点。比如应用层如果还留着本地 Session,那负载均衡就等于随时可能把用户踢下线——这类问题在压测时完全看不出来,只有真实扩容时才翻车。
还有一个容易忽略的点:层数不是越多越好。七层是给千万级用户规模准备的,十万级用户硬套七层,运维成本会先把你压垮。判断标准很简单:如果某一层存在的唯一理由是「未来可能会用到」,那它现在就不该存在。
2.2 分层与分割:先切业务还是先切读写
指南里分层是按「应用层、服务层、数据层、管理层、分析层」横切,分割是按「业务/模块/功能」纵切。实际动手时,第一个问题永远是:先按业务拆,还是先按读写拆。
我的经验是先业务后读写,理由是可逆性不一样。业务拆分一旦上线,团队边界、发布流程、数据库归属都跟着变,回退成本极高;读写分离只是在数据访问层加一层路由,改错了改回来就行。所以顺序应该是:先用垂直切分把系统按业务域拆成产品、购物、支付、评论、客服、接口六个子系统,再对其中读多写少的子系统做读写分离和分表。
业务拆分的同时必须做等级定义,这是指南里很实用但常被跳过的一步。按对交易的影响划分:核心系统是商品、购物、支付,非核心系统是评论、客服、对接外部系统的接口子系统。这个分级不是给领导看的,是给降级开关用的——大促流量打满时,先关评论、关推荐、关客服入口,把线程池和数据库连接全部让给下单链路。没有分级,出事时就会变成「全都降级等于都没降级」。
拆完之后还有个坑:公共代码。六个子系统各复制一份用户校验、鉴权、工具类,三个月后就是六个版本。指南给的方向是把用户、订单、支付、安全这些被反复引用的能力抽成服务层的公共模块,通过 RPC 调用,而不是复制代码。抽取的判据我一般用「被三个以上子系统引用且变更频率低于每月一次」,低于这个门槛就先忍一忍,抽早了会变成分布式单体。
2.3 集群与负载均衡:四层和七层的选型参数
应用层的高可用靠集群,集群的前提是负载均衡。这里有个很实际的选型分歧:四层负载均衡按目标地址和端口转发,路径短、性能高,但看不懂报文内容;七层负载均衡能解析 HTTP 头,可以按 URL 做动静分离、按 Header 做灰度路由,代价是多一次完整的连接建立与解析。
什么时候用哪个?我的判断是这样的:流量入口的第一跳(南北向流量)用四层,因为它只做转发不做解析,能扛住突发;系统内部服务之间的调用(东西向流量)用七层,因为要做路径分发和灰度标记。原文提到七层代理可以做动静分离,这个能力在带宽紧张时非常值钱——静态资源和动态接口走不同后端,静态资源直接由代理返回或者转向对象存储,应用服务器只处理业务请求。
下面这段配置是我在项目里最常用的七层反向代理模板:
upstream app_pool { least_conn; # 按当前连接数最少的节点分发,比轮询更适合长请求 server 10.0.0.11:8080 max_fails=3 fail_timeout=10s weight=1; server 10.0.0.12:8080 max_fails=3 fail_timeout=10s weight=1; server 10.0.0.13:8080 max_fails=3 fail_timeout=10s weight=1 backup; # 备用节点,主节点全挂才接流量 keepalive 128; # 到后端的空闲长连接池大小 } server { listen 80; location /api/ { proxy_pass http://app_pool; proxy_http_version 1.1; # 必须显式声明 1.1,否则 keepalive 不生效 proxy_set_header Connection ""; # 清空短连接头,长连接才能复用 proxy_connect_timeout 1s; # 建连超时压到 1s,避免卡在半开连接上 proxy_read_timeout 3s; # 读超时 3s,超过直接换下一台 proxy_next_upstream error timeout http_502 http_504; proxy_next_upstream_tries 2; # 最多换 2 台,防止重试放大流量 } location /static/ { root /data/www; expires 7d; # 静态资源交给代理直接返回,不打到应用服务器 } }几个参数值得单独说。keepalive 128如果不配proxy_http_version 1.1和空的Connection头,是不会生效的,这是最常见的配置失效原因,配置看着没问题但后端 TIME_WAIT 依然堆到几万。max_fails=3 fail_timeout=10s表示 10 秒内失败 3 次就摘除节点,10 秒后重新试探,这个时间窗太短会导致抖动节点反复进出,太长会导致故障恢复慢,10 秒是折中值,慢接口多的系统要调到 30 秒。
proxy_next_upstream要特别注意:它只对幂等请求开重试。GET 重试没问题,POST 订单接口重试会直接造成重复下单,所以订单路径要么单独配一个不重试的 location,要么在应用层用幂等 token 兜住。这个点我在第 5 章还会展开。
3. 容量预估换算:从 1000 万注册用户倒推出每秒 8340 次并发
架构设计里最容易被跳过又最贵的一步,是容量预估。跳过它的后果是:要么机器买少了,大促现场扩容,要么买多了,一年几十万闲置。指南给了一套从注册用户数倒推服务器的算法,我把它整理成可以反复跑的脚本。
3.1 PV、UV 到 QPS 的四步换算
预估链条是这样的:注册用户数 → 日均 UV → 每日 PV → 集中访问时段的每分钟请求数 → 每秒 QPS → 峰值 QPS → 服务器台数。原文的例子是 1000 万注册用户,按二八原则日均 UV 约 200 万,每人每天点击 30 次,PV 就是 6000 万;80% 的访问集中在 20% 的时间里,即 4.8 小时承载 4800 万次访问;换算到每分钟约 16.7 万次,每秒约 2780 次;再乘以 3 倍的峰值系数,得到每秒 8340 次。
这套算法有一个隐含假设:访问在 4.8 小时内是均匀分布的。真实业务不是这样,活动开场那一分钟可能就吃掉半小时的流量。所以我一般会在这个基础上再加一层「瞬时倍数」,把开场前 60 秒单独算一遍。下面是完整脚本:
def estimate_capacity(reg_users, uv_ratio, pv_per_user, busy_hours_ratio, busy_pv_ratio, peak_factor, burst_factor, qps_per_node): """容量预估:从注册用户倒推服务器台数 reg_users 注册用户数 uv_ratio 日活占注册用户比例,成熟业务 0.15~0.25 pv_per_user 人均日点击次数 busy_hours_ratio 集中访问时间占比,二八原则取 0.2 busy_pv_ratio 集中时段承载的 PV 占比,取 0.8 peak_factor 日常峰值系数,活动期取 2~3 burst_factor 开场瞬时倍数,秒杀场景取 5~10 qps_per_node 单机可承载 QPS,需压测校准,不能拍脑袋 """ uv = reg_users * uv_ratio pv = uv * pv_per_user busy_seconds = 24 * busy_hours_ratio * 3600 avg_qps = pv * busy_pv_ratio / busy_seconds peak_qps = avg_qps * peak_factor burst_qps = peak_qps * burst_factor return { "日均UV": int(uv), "日PV": int(pv), "平常QPS": round(avg_qps, 1), "峰值QPS": round(peak_qps, 1), "瞬时QPS": round(burst_qps, 1), "平常所需台数": -(-int(peak_qps * 0.6) // qps_per_node), # 60% 流量走弹性扩容 "峰值所需台数": -(-int(peak_qps) // qps_per_node), "瞬时所需台数": -(-int(burst_qps) // qps_per_node), } print(estimate_capacity(10_000_000, 0.2, 30, 0.2, 0.8, 3, 3, 300))跑出来的峰值 QPS 是 8340,跟原文一致;瞬时 QPS 按 3 倍算是 25020,这个数字决定了秒杀要不要单独做队列削峰——因为靠加机器扛住 25000 QPS 是极其不划算的,按单机 300 QPS 要 84 台,而这 84 台只在开场那一分钟有用。
参数里最需要谨慎的是qps_per_node。它不能写成 300 就完事,必须来自真实压测。压测方法是用生产同等配置的机器,跑核心接口的混合流量,逐步加压到 P99 响应时间超过 200ms 或者 CPU 到 70%,此时的 QPS 才是可用的单机容量。
3.2 「单机 300 并发」的含义:并发数、QPS 与线程数不是一回事
指南里写「按一台 Web 服务器支持每秒 300 个并发计算」,这句话是整篇文档里最容易被误读的一句。并发连接数、并发线程数、QPS 是三个不同的量,混用会导致机器数量差出好几倍。
用利特尔法则换算:并发数 = QPS × 平均响应时间。假设单机 QPS 300、平均响应时间 100ms,那么同时在处理的请求数是 300 × 0.1 = 30 个线程。如果平均响应时间涨到 200ms,同样 300 QPS 就需要 60 个线程;如果响应时间涨到 1 秒,就需要 300 个线程,而默认线程池只有 150 个,这时候 QPS 不但上不去,还会因为排队把响应时间进一步拉长,形成正反馈崩溃。
所以压测时必须同时记录三个数字:QPS、P99 响应时间、活跃线程数。三者对不上,说明压测模型有问题。常见的错误是用固定并发数压测却不看响应时间变化——并发从 50 加到 100,QPS 只从 280 涨到 300,响应时间从 80ms 涨到 260ms,这说明系统已经在拐点之后了,继续加压只会更慢。
线程池配置也要跟着响应时间走。maxThreads设成 150 还是 500,取决于你的接口平均耗时:短接口(< 50ms)150 足够撑 3000 QPS,长接口(> 500ms)设到 500 也只是把排队从队列挪到线程里,根治方法是把长耗时操作异步化,而不是加线程。这也是指南里「异步」模式的真正用处——把同步等待的链路砍掉,比堆机器便宜得多。
3.3 70/90 原则与资源水位红线
指南给的容量红线是:CPU 平常维持 70% 左右、高峰到 90%,内存和 IO 同理。这条原则的意义在于,它定义了「什么时候该扩容」,而不是「什么时候该报警」。
70% 是安全边界。因为一旦超过 80%,单机响应时间开始非线性上升,队列积压会让重试请求叠加进来,很快从 85% 冲到 100%。90% 是硬红线,触线就必须有自动扩容或者限流兜底,不能靠人盯着面板手动加机器——人从看到报警到机器上线,最快也要 5 分钟,而流量洪峰 30 秒就能把系统打死。
落到监控指标上,我会给每个关键资源配三条线:
| 资源 | 观察线 | 扩容线 | 熔断线 |
|---|---|---|---|
| 应用 CPU | 60% | 75% | 90% |
| 应用内存(老年代) | 70% | 80% | 92% |
| 数据库连接数 / 上限 | 50% | 70% | 85% |
| 缓存命中率 | 92% | 88% | 80% |
| 接口 P99 响应时间 | 150ms | 250ms | 500ms |
缓存命中率跌破 92% 就要查原因,因为命中率的下降通常是渐进的,等掉到 80% 时数据库早就被打穿了。这个指标比 CPU 更早发出信号,是我最看重的一个。
4. 多级缓存与读写分离:把 28 原则翻译成 TTL、锁和路由键
性能优化里投入产出比最高的是缓存,最容易出事故的也是缓存。指南给的原则是「80% 的访问落在 20% 的数据上」,所以只缓存热点数据就能覆盖绝大部分请求。问题在于,「热点」是会漂移的,缓存失效的方式、失效瞬间的并发、以及失效之后打到哪一层,才是决定系统稳不稳的关键。
4.1 两级缓存的结构与读写比门槛
指南建议两级:一级本地缓存放数据字典和基本不变的热点数据,二级分布式缓存放全量可缓存数据。查询顺序是本地 → 分布式 → 数据库,任一层命中就返回。
为什么要有本地这一层?因为分布式缓存再快也有一次网络往返,通常 0.5~1ms,而本地内存读是几十纳秒。对于每秒几万次访问的数据字典(地区、类目、配置项),这一层省下的网络开销非常可观。代价是一致性问题:本地缓存各节点独立,更新后不同节点看到的值可能不同,所以要严格限制本地缓存的适用范围——不可变或者变化频率低于分钟级的数据,且变更后能接受短暂的不一致。
分布式缓存则承担热点业务数据,比如商品详情、用户概要。这里有个门槛问题:指南说读写比达到 4:1 就可以考虑上缓存,理论门槛是 2:1。这个 4:1 指的是一个数据「被读次数」和「被写次数」的比值。写多读少的数据上缓存,会出现缓存刚写好就被下一次写操作失效掉,命中率极低,反而多了一次缓存往返和一次失效广播。
我一般的判断标准是三条同时满足才上缓存:读请求占比超过 80%、单条数据被重复读取的次数在 TTL 内超过 10 次、数据一致性要求不是强一致。第三条最容易被忽略,比如账户余额就不能走普通缓存,只能走带版本号的短 TTL 缓存或者直接读主库。
4.2 穿透、击穿、雪崩:三个问题对应三组参数
这三个词经常被混着说,但它们的原因和对策完全不同。
穿透是查一个数据库里也不存在的 key,缓存永远不命中,请求全部打库。原因通常是参数被构造(比如用不存在的 ID 反复请求)或者业务上真的查了空记录。对策是缓存空值并给一个短 TTL(60 秒左右),或者在前面加布隆过滤器拦截明显不存在的 key。
击穿是单个热点 key 过期瞬间,大量请求同时发现缓存没了,一起去查库。对策是重建加锁,只放一个请求去查库,其余请求短暂等待后重试本地缓存。
雪崩是大量 key 在同一时刻集体过期,或者缓存集群整体不可用。对策是 TTL 加随机抖动、分批预热、以及缓存的降级开关。
下面这段是带互斥重建和抖动 TTL 的读取逻辑:
import random, time def get_product(pid): key = f"p:detail:{pid}" # 一级:本地缓存,只给超高热点数据,TTL 短,30 秒强制回源一次 local = local_cache.get(key) if local is not None: return local # 二级:分布式缓存 data = redis.get(key) if data is not None: local_cache.set(key, data, ttl=30) return data # 空值缓存:防穿透,TTL 必须短,否则商品上架后要等很久才能被看到 if redis.get(f"{key}:null"): return None lock_key = f"lock:{key}" # nx=True 保证只有一个请求拿到锁,ex=3 秒防止持锁进程挂掉导致死锁 if redis.set(lock_key, "1", nx=True, ex=3): try: data = db.query("select * from product where id=%s", pid) if data is None: redis.set(f"{key}:null", "1", ex=60) return None # 基础 TTL 1800 秒,抖动 ±10%,把集体过期打散成 3 分钟窗口 ttl = 1800 + random.randint(-180, 180) redis.set(key, data, ex=ttl) local_cache.set(key, data, ttl=30) return data finally: redis.delete(lock_key) # 无论成功失败都要释放,避免锁泄漏 else: time.sleep(0.05) # 没抢到锁的请求等 50ms 后重试 data = redis.get(key) if data is None: return None # 兜底返回降级值,绝不直接打库 return data几个参数的取法:ex=3的锁超时要大于一次数据库查询的 P99,否则会出现锁过期了查询还没回来,第二个请求又进来查库;random.randint(-180, 180)的抖动幅度取 TTL 的 ±10%,太小起不到打散作用,太大会影响命中率;空值 TTL 的 60 秒是经验值,取决于业务对「新数据可见性」的容忍度,商品上架类业务可以压到 30 秒。
最后那段return None是关键。很多实现里没抢到锁的请求会直接去查库,结果就是锁形同虚设,热点 key 失效瞬间照样一堆请求打到数据库。宁可返回降级内容,也不要让请求穿透到最后一层。
4.3 读写分离与分库分表:路由键选错,后面全白干
单库撑不住时,第一刀是读写分离:主库承接写和强一致读,从库承接普通读,通过主备同步。第二刀是分库分表:水平切分把一张大表按某个维度拆开,垂直切分把不同业务的表分到不同库。
读写分离最现实的问题是主从延迟。同步复制保证强一致但写性能腰斩,异步复制性能好但有延迟,通常几十毫秒,高峰期能到几百毫秒。业务上的表现是「刚下单,刷新订单列表看不到」。解决办法不是改成同步复制,而是识别出必须读主库的场景:写操作之后的 N 秒内、支付回调、库存扣减校验,这几类请求在代码里打上强制走主库的标记。我一般把窗口设成 1 秒,覆盖 99% 的延迟情况。
分库分表的核心决策是路由键。选错了,跨库 join 和分布式事务会把你拖死。经验规则:路由键必须是最常用的查询维度,且尽量保证一个业务操作只落一个库。电商场景用买家 ID 做订单表路由键,因为用户查自己订单是最高频操作;商家后台按商家维度查是次要场景,可以用异构索引表解决,而不是改路由键。
路由算法上,取模简单但扩容时要全量迁移,一致性 Hash 加了虚拟节点能减少迁移量,但实现复杂度上升。下面是最小可用的取模路由:
import hashlib def shard_of(uid, shards=16): """按用户 ID 路由到 16 个库,返回 0~15""" # 用 md5 而不是 python 内置 hash,内置 hash 在不同进程间不稳定 digest = hashlib.md5(str(uid).encode()).hexdigest() return int(digest, 16) % shards # 扩容到 32 库时,只有约一半的数据需要迁移,但取模做不到 # 一致性 Hash 的虚拟节点版本才能把迁移量压到 1/N注意这里用 md5 而不是 Python 内置的hash(),内置 hash 对字符串在每个进程里加盐不同,多实例部署会导致同一用户路由到不同库,这是一个很难排查的坑。分片数选 16 而不是 10 的倍数,是为了避免后续扩容到 32、64 时出现倍数关系导致的分布不均。
5. 避坑与排查:上线前我必查的 5 个血泪记录
架构图再漂亮,落地时踩的坑都在细节里。这一章记录的是我在几个项目里真实遇到过、并且反复在新项目里复查的五类问题。每条按现象、原因、解决三段写,建议对照自己的系统过一遍。
5.1 线上问题的定位顺序:先入口,再依赖,后资源
故障发生时最浪费时间的不是修复,是找方向。我的固定顺序是四步:先看入口 QPS 和错误率曲线,确认是流量涨了还是错误涨了;再看依赖的响应时间,确认是自身问题还是下游拖累;再看资源水位(CPU、连接池、GC),确认瓶颈位置;最后才看单机日志。
这个顺序的理由是:错误率突增但 QPS 平稳,八成是依赖挂了或者发布引入了 bug;QPS 突增但错误率平稳,是流量问题,看限流和扩容;QPS 和错误率同时突增,通常是雪崩前兆,先限流再排查。反过来,如果一上来就翻单机日志,很容易被大量超时堆栈误导,以为是代码问题,实际是数据库连接池被打满。
排查时还有一个必备动作:确认监控本身是可信的。有一次故障排查了两小时,最后发现是采集 agent 自己挂了,面板上的曲线是最后一次上报的残留值。从那以后我养成了一个习惯,每次故障先找一个已知正常的指标做对照,确认监控链路是活的。
5.2 五条踩坑记录
第一条:健康检查只探端口,进程假死照样接流量。
现象是负载均衡把流量分到一台机器,那台机器上所有请求都超时,但面板显示节点状态正常,故障持续了十几分钟才被人发现。原因是健康检查配的是 TCP 端口探测,进程还在、端口还在监听,但内部线程池已经被打满,请求全部排队。解决是把检查改成应用层的/health接口,且这个接口必须校验线程池水位和数据库连通性,连续 3 次失败或超时 1 秒就摘除节点。这个改动做完之后,单机假死对用户的影响从分钟级降到秒级。
第二条:批量预热用了同一个 TTL,晚高峰整点集体失效。
现象是每天晚上 8 点整,数据库 QPS 突然翻十倍,持续十几秒后恢复。原因是凌晨预热时用一个 for 循环批量写入缓存,所有 key 的 TTL 都设成 12 小时,到期时间自然全部落在晚 8 点。解决是预热时给每个 key 的 TTL 加随机抖动,同时把预热时间打散到多个时间点。这个问题的隐蔽之处在于,它只在流量高峰期暴露,低峰期集体失效也没什么影响,很容易被当成「偶发慢查询」忽略过去。
第三条:多层重试叠加,下游被打成三倍流量。
现象是一个下游服务超时后,上游重试三次,结果那个服务彻底挂掉,原本只是慢接口变成不可用。原因是调用链上有三层都配了自动重试,2 的 3 次方就是 8 倍放大;而且重试打在了非幂等的下单接口上,产生了重复订单。解决是只在链路最外层保留一次重试,中间层一律不重试;非幂等接口显式关闭重试开关;同时在下游加熔断,错误率超过 50% 直接快速失败,不再把请求送过去。这个坑的教训是:重试是给偶发抖动准备的,不是给故障准备的。
第四条:应用连接池总量超过数据库连接上限。
现象是高峰期应用侧报连接超时,数据库侧报连接数超限。原因是 30 台应用服务器,每台连接池最大 50 个,合计 1500 个潜在连接,而数据库的最大连接数只配了 800。平时用不到上限所以没暴露,流量一涨瞬间打满。解决是按数据库上限倒推:800 个连接留 20% 给运维和批处理,剩下 640 除以 30 台,每台配 20 个,同时给连接池设获取超时 1 秒,超过就快速失败而不是无限排队。这个数字必须在扩容时同步重算,否则每加一批机器就逼近一次上限。
第五条:全链路超时时间倒挂。
现象是大促时前端报 30 秒超时,但用户等了半分钟看到的还是错误页。排查发现配置是前端 30 秒、网关 10 秒、应用 5 秒、下游服务 8 秒——下游比上游还长,意味着应用已经放弃了,下游还在继续跑,白白占用资源。解决是超时时间必须从最下游往上递增,每层留 20% 余量:下游 800ms、应用 1s、网关 1.2s、前端 1.5s。这样任何一层超时,上层都能及时拿到失败结果并触发降级,而不是干等。
6. 把三个九跑成演练清单:灰度、混沌与压测的收口动作
可用性指标不是写在 PPT 上的,是演练出来的。先把数字换算清楚:99.9% 一年允许 8.76 小时不可用,99.99% 是 52.6 分钟,99.99% 往上每加一个 9,允许的故障窗口就缩小一个数量级。多数业务做到 99.99% 已经是成本和收益的平衡点,再往上要靠多机房和多活,投入会陡增。
知道自己站在哪个 9 上之后,落地动作是三步。第一步是灰度,新版本先放 5% 流量,观察 15 分钟的错误率和 P99,无异常再放到 50%、100%,每一档都要有明确的回滚触发条件。第二步是混沌演练,主动杀掉一个应用节点、断开一个从库、清空一次缓存,看系统的自动恢复能力——注意要在流量低谷做,并且提前通知。第三步是压测,用生产流量的录制回放打到 3 倍峰值,重点看扩容触发时间和限流是否生效。
演练前我会跑一遍节点健康巡检,确认所有节点真的健康,避免把「本来就有问题」当成演练结果:
#!/usr/bin/env bash # 演练前置巡检:确认所有节点 /health 返回 200,且响应时间低于 100ms # 用法:./precheck.sh nodes.txt NODES=$(cat "$1") FAIL=0 for node in $NODES; do # -m 1 建连超时 1s,-w 输出总耗时,%{http_code} 取状态码 result=$(curl -s -o /dev/null -m 1 -w "%{http_code} %{time_total}" "http://${node}/health") code=$(echo "$result" | awk '{print $1}') cost=$(echo "$result" | awk '{print $2}') # 状态码非 200 或耗时超过 0.1 秒都记为异常 if [ "$code" != "200" ] || (( $(echo "$cost > 0.1" | bc -l) )); then echo "[FAIL] ${node} code=${code} cost=${cost}s" FAIL=$((FAIL + 1)) fi done echo "异常节点: ${FAIL}" exit $FAIL这个脚本的价值不在多复杂,而在于把「巡检」变成一个有退出码的动作,可以直接挂到发布流水线里,异常节点不为零就阻断发布。参数上,1 秒建连超时对应内网环境,跨机房要放宽到 2 秒;100ms 的阈值对应应用层的健康检查接口,如果这个接口本身要查数据库,阈值要按实际 P99 调整,否则会误报。
最后说一个我踩过的坑。有次演练我按计划杀掉一个应用节点,系统确实没受影响,于是结论是「高可用没问题」。但两周后真实故障时,挂的是一整个机柜的交换机,同机柜的四个节点同时掉线,负载均衡剩下的容量不够,直接雪崩。单节点故障和高关联故障完全是两回事,演练必须包含「按机柜/可用区批量摘除」的场景。从那以后我每次做演练,都强制走一遍关联性故障的用例,而不是只杀一个 Pod 交差。希望帮到你。
本文还有配套的精品资源,点击获取