简介:面向大型网站高性能、高并发与高可用架构设计的学习需求,这份docx技术指南面向具备一定IT基础的工程师和架构师,帮助梳理架构目标、模式选型与分层优化的完整方法。内容覆盖分层、分割、分布式部署、集群、缓存、异步等常见模式,并延伸到前端、浏览器、应用层、代码和存储层优化,以及冗余备份、失效转移、CAP理论、分库分表、一致性Hash、安全架构与电商网站架构演进案例,兼顾理论框架和落地思路。资源包共1个docx文件,大小约3.19MB,便于集中阅读和专题查阅。已有1124人学习,说明其在架构设计入门与进阶场景中具有一定参考价值。读者可据此建立大型网站架构知识地图,理解不同阶段的优化重点与可用性保障策略,并结合案例对照自身系统查漏补缺;对准备大规模扩展或正面临高流量挑战的团队,也可作为架构演进参考。
1. 三台机器到三十台 Tomcat:这份指南补的是哪一段路
有个场景很典型:一个小团队要做全品类电商站点,目标是三到五年做到一千万注册用户,还要定期扛大促。第一次做容量预估,按峰值算出来要三十台 Web 服务器,采购单还没批下来,负责人先问了一句——平时只用十台,剩下二十台一年开几天?这个问题就是高性能、高并发、高可用架构设计真正开始的地方。这份资料把大型网站的架构目标拆成高性能、高可用、可伸缩、可扩展、安全、敏捷六条,再按分层、分割、分布式、集群、缓存、异步、冗余这些模式落到七层逻辑架构上,最后用一套电商站点从单机到分布式服务的完整演化过程串起来。它适合已经能独立上线一个中小系统、但要第一次面对流量突增的工程师,也适合需要把“分布式系统”这四个字从概念变成部署图的人。
2. 容量预估:把“一千万注册用户”折算成每秒 8340 个并发
架构设计最容易出问题的地方,是没有算清楚目标就动手画图。容量预估是整份资料里最该先读的一节,因为它决定了后面所有选型:集群要几台、缓存要多大、数据库要不要拆。这一步算错,后面的负载均衡和分库分表全都要返工。我见过不少团队把“注册用户数”直接当成“并发用户数”报上去,结果要么采购过量,要么上线当天被打穿,两种结果都不会有人满意。
2.1 从 UV 到 QPS 的四步折算
折算的起点不是注册用户,是日活。一千万注册用户按二八原则取 20% 作为日 UV,也就是 200 万;人均日点击按 30 次算,覆盖浏览、搜索、加购、下单这些动作;日 PV 就是 6000 万。真正的并发峰值不落在全天,而是落在一天里 20% 的时间里产生 80% 请求的那一段,24 小时乘 0.2 是 4.8 小时,也就是 288 分钟,这 288 分钟要承接 4800 万次访问,每分钟约 16.7 万次,换算到秒就是 2780 次。大促峰值按平常的三倍估,每秒 8340 次。这个数在资料里写得很明确,也是后面所有服务器台数的计算基准。
| 参数 | 取值 | 计算方式或依据 |
|---|---|---|
| 注册用户 | 1000 万 | 3~5 年业务目标 |
| 日 UV | 200 万 | 注册用户 × 20%,二八原则 |
| 人均日点击 | 30 次 | 浏览 + 搜索 + 加购 + 下单合计 |
| 日 PV | 6000 万 | 日 UV × 人均点击 |
| 集中访问时长 | 4.8 小时 | 24 × 0.2,80% 请求落在 20% 时间 |
| 集中访问量 | 4800 万 | 日 PV × 0.8 |
| 每分钟请求 | 16.7 万 | 4800 万 ÷ 288 分钟 |
| 平时 QPS | 2780 | 16.7 万 ÷ 60 |
| 峰值 QPS | 8340 | 平时 × 3 |
这套折算适合用脚本跑,改一个参数就能看结果,比在表格里手改公式可靠得多:
# capacity.py 容量预估:从注册用户反推每秒并发 REG_USER = 10_000_000 # 3~5 年注册用户目标 ACTIVE_RATIO = 0.2 # 二八原则:20% 用户产生 80% 访问 CLICK_PER_USER = 30 # 人均日点击(浏览 + 搜索 + 加购 + 下单) PEAK_WINDOW = 0.2 # 80% 流量集中在 20% 的时间里 PEAK_FACTOR = 3 # 大促峰值 = 平常值 3 倍 daily_uv = REG_USER * ACTIVE_RATIO daily_pv = daily_uv * CLICK_PER_USER busy_min = 24 * 60 * PEAK_WINDOW # 集中访问时长(分钟) busy_pv = daily_pv * 0.8 qps_avg = busy_pv / busy_min / 60 qps_peak = qps_avg * PEAK_FACTOR print(f"日 UV {daily_uv/1e4:.0f} 万, 日 PV {daily_pv/1e4:.0f} 万") print(f"平常 QPS {qps_avg:.0f}, 峰值 QPS {qps_peak:.0f}")脚本里每个常量都对应一个业务假设,改假设之前先想清楚依据:ACTIVE_RATIO 取 0.2 是行业里比较常见的活跃度,如果你的产品是工具型低频应用,取 0.1 更稳;PEAK_FACTOR 取 3 是按有大促活动的站点估的,没有活动运营的站点取 2 就够。运行结果会和上表对得上,日志里同时打印 UV 和 PV 是为了交叉验证,防止某个参数填错一个量级还看不出来。
提示:容量预估的结果要写进设计文档,并标注每个假设的来源。半年后业务变了,能一眼看出是哪条假设失效,比重新拍脑袋估一遍省事得多。
2.2 并发数不等于 QPS:最容易搞混的一个换算
拿到 2780 这个数之后,很多人直接拿它去除以单机并发能力,得出服务器台数,这一步几乎必错。并发线程数和 QPS 之间隔着响应时间:QPS = 并发数 ÷ 平均 RT。同样 2780 QPS,接口平均 RT 是 100 毫秒,需要同时在跑的线程是 278 个,一台机器就能扛;RT 涨到 500 毫秒,同样流量下需要的线程变成 1390 个,得五台机器才顶得住。性能优化和扩容在这里是同一件事的两面,把 RT 压下去等于凭空多出几台机器。
按资料里的口径,单台 Web 服务器按每秒 300 个并发来估算,日常 2780 QPS 需要约 10 台,峰值 8340 QPS 需要约 30 台。这个 300 是有前提的:Tomcat 默认的 maxThreads 是 150,实际能扛到 300 意味着大部分请求的 RT 要压得足够短,且 CPU 利用率维持在 70% 左右,峰值时才允许冲到 90%。这就是 70/90 原则——平时留三成余量给突发,峰值把余量用满但不打满,既不浪费资源,也不至于一有抖动就雪崩。
| 场景 | QPS | 平均 RT | 所需并发线程 | 单机 300 并发下的台数 |
|---|---|---|---|---|
| 日常 | 2780 | 100 ms | 278 | 1(实际按冗余布 2 台起) |
| 日常 | 2780 | 300 ms | 834 | 3 |
| 峰值 | 8340 | 100 ms | 834 | 3 |
| 峰值 | 8340 | 300 ms | 2502 | 9 |
表格里的数字和资料里“日常 10 台、峰值 30 台”看起来差得不小,原因在 RT:资料按更保守的 RT 和更保守的单机承载估,我这张表按 300 并发、RT 可控来算。两种口径都不算错,差的是对代码性能的信心。实操里我一般先按保守口径报预算,压测跑完拿到真实 RT,再往下修,省得上线前临时加机器来不及采购。
2.3 峰值和日常的差值怎么消化
30 台和 10 台之间的差距,就是这份资料里点出的第一个真问题:大量服务器只有在大促时才用得上,平时是纯浪费。不解决这个差值,架构图画得再漂亮,成本这一关也过不去。消化差值的办法有三类,优先级从高到低是削峰、限流、弹性扩容。
削峰靠异步。下单请求进来不直接写库,先落消息队列,前端立刻返回排队中,后台按数据库能吃下的速度慢慢消费。资料里把这种“请求—响应—通知”的模式归到异步架构模式,它对付的是瞬时洪峰,不是持续高负载。限流是第二道,超过阈值就让请求排队或者返回 429,宁可让 1% 的用户等两秒,也不能让 100% 的用户一起超时。弹性扩容排最后,因为它慢,机器从创建到进集群跑起来,几分钟起步,只适合应对可以预判的活动。
# 按单机实测承压能力的 80% 设阈值,超出后排队而不是直接拒绝 limit_req_zone $binary_remote_addr zone=perip:10m rate=20r/s; limit_req_zone $server_name zone=perserver:10m rate=800r/s; server { location /product/ { limit_req zone=perserver burst=200 nodelay; # 允许 200 个突发排队 limit_req_status 429; # 明确返回限流码,方便监控统计 proxy_pass http://product_cluster; } location /api/comment/ { limit_req zone=perip burst=10 nodelay; # 非核心接口按 IP 收得更紧 limit_req_status 429; proxy_pass http://comment_cluster; } }配置里两个 zone 对应两种限流维度:perserver 按虚拟主机整体限流,护住核心链路;perip 按来源 IP 限流,主要防刷。rate 的单位是每秒请求数,burst 是允许超出的突发量,nodelay 表示突发部分立即放行而不是匀速排队——不加 nodelay,超出的请求会被平滑拉长,用户侧感知就是接口变慢,大促时这种慢会顺着调用链放大。阈值不能凭感觉填,先压测出单机能稳定处理的 QPS,再乘 0.8 写进去,改完观察 429 的比例,超过总请求的 1% 就说明阈值定低了。
3. 分层、分割、分布式、集群:把架构模式翻译成部署拓扑
分层、分割、分布式、集群、缓存、异步、冗余、安全、自动化、敏捷,这十个词背下来只要五分钟,但把它们对应到部署图上是几台机器、几条网络跳数、几个团队负责,才是架构设计真正的工作量。这份资料的价值在于它把每个模式都落到了具体层上,而不是停在概念介绍。
3.1 七层逻辑架构与业务垂直切分
资料给出的七层是从客户端一路排到大数据处理层的,每一层的扩容方式和高可用手段都不一样,混着做就会互相拖累。
| 层级 | 主要职责 | 扩容方式 | 高可用手段 |
|---|---|---|---|
| 客户层 | PC 浏览器、移动端 | 无(客户端侧) | 多入口、降级到简版页 |
| 前端层 | DNS 调度、CDN、反向代理 | 加节点 | 多 CDN、代理集群 |
| 应用层 | 业务集群,按业务垂直拆分 | 水平加机器 | 负载均衡、无状态设计 |
| 服务层 | 公共的订单、支付、用户服务 | 水平加机器 | 服务降级、超时、幂等 |
| 数据层 | 关系库集群、NoSQL、分布式缓存 | 读写分离、分库分表 | 主备、失效转移 |
| 大数据存储层 | 日志与结构化数据归集 | 加存储节点 | 多副本 |
| 大数据处理层 | 离线与实时分析 | 加计算节点 | 任务重试、隔离队列 |
应用层的拆分方式在资料里写得很具体:按业务属性垂直切成产品、购物、支付、评论、客服、接口六个子系统。切完之后还要做等级定义,产品和购物、支付算核心系统,评论、客服、接口算非核心。这个等级不是为了写文档好看,它直接决定了两件事——出故障时先保谁,流量突增时先降谁。核心系统单独部署,非核心系统可以合并部署省机器,两种方案在资源紧张时都能用。
注意:垂直切分不是越细越好。每切一刀,子系统之间就多一次远程调用,故障面从“一个进程挂”变成“一条链路断”。判断标准是这两块业务的变更频率和团队归属是否真的不同,如果同一个团队同时维护、发布节奏一致,切开只会增加运维成本。
3.2 四层和七层的分界:负载均衡怎么选
负载均衡选型是架构落地时第一个要拍板的技术决策,常见的组合是四层设备打头、七层代理做二级。区别在网络模型的层级:四层只看目标 IP 和端口,转发路径短、吞吐高;七层要解出报文内容,能按 URL、Header、Cookie 做路由,但代价是连接被代理层接管,开销更大。
| 类型 | 可见信息 | 性能 | 典型用途 |
|---|---|---|---|
| 硬件四层设备 | IP + 端口 | 最高 | 入口第一层,扛总流量 |
| 四层软件负载均衡 | IP + 端口 | 高 | 入口分流,成本低 |
| 七层代理(Nginx、HAProxy) | 完整 HTTP 报文 | 中 | 动静分离、按业务路由、灰度 |
实际部署里我一般这样排:最外层用四层做流量分发,后面挂一组七层代理,七层代理再按 URL 把请求分到不同的业务集群。这样动静分离、灰度发布、按业务限流这些能力都能在七层做,四层只负责把流量摊平。配置上要注意上游连接复用,不开 keepalive 的话,每次请求都要重新握手,RT 会莫名其妙多出几毫秒:
upstream product_cluster { least_conn; # 按当前连接数分配,长请求多的场景比轮询稳 server 10.0.1.11:8080 max_fails=2 fail_timeout=10s; server 10.0.1.12:8080 max_fails=2 fail_timeout=10s; keepalive 64; # 与上游保持 64 个空闲长连接,省掉握手开销 } server { location /product/ { proxy_http_version 1.1; # keepalive 必须配合 HTTP/1.1 proxy_set_header Connection ""; # 清掉 close,否则长连接失效 proxy_connect_timeout 1s; # 连接超时要短,快速失败 proxy_read_timeout 3s; # 读超时按 P99 RT 的 2~3 倍设 proxy_next_upstream error timeout; # 失败自动换一台,重试一次就够 proxy_pass http://product_cluster; } }几个参数值得单独说。least_conn 在请求耗时差异大时比默认轮询更均衡,因为轮询会把新请求派给还没处理完长请求的节点。max_fails 和 fail_timeout 组合起来决定健康检查的灵敏度,2 次失败、10 秒内不再派流量,阈值太松会把请求打到已经挂掉的节点,太紧则容易误判。代理层的超时一定要设短,并且必须小于上游服务的超时,否则代理还在等、上游已经放弃,两台机器的时间对不上,排查的时候就是黑匣子。proxy_next_upstream 要慎用,只对幂等请求开重试,下单和支付这类接口重试会造成重复扣款。
3.3 集群之后的状态去哪:Session 的三种处理
应用服务器一旦做成集群,第一个撞上的问题就是 Session。同一用户在两次请求之间落到不同机器上,登录态就丢了。资料里把这条路归到“无状态设计”,具体落地有三种做法:会话复制、粘性会话、集中存储。
会话复制是在集群节点之间互相同步 Session 对象,节点数一多,同步的流量会呈平方级增长,三台以内还能忍,十台以上基本不可用。粘性会话靠负载均衡把同一用户的请求固定打到同一台机器,配置简单,但那台机器挂了用户就得重新登录,而且流量分布会越来越不均。集中存储是把 Session 挪到分布式缓存里,应用节点彻底无状态,加机器减机器都不影响登录态,是规模上去之后唯一还能扩展的方案。
// 集中式 Session:只写变化的字段,靠过期时间做滑动续期 public void saveSession(String sid, Map<String, Object> delta) { String key = "sess:" + sid; redis.hset(key, delta); // 增量写入,避免整对象覆盖丢字段 redis.expire(key, 1800); // 30 分钟滑动过期,每次请求重置 } public Map<String, Object> loadSession(String sid) { Map<String, Object> attrs = redis.hgetAll("sess:" + sid); if (attrs.isEmpty()) { return null; // 返回 null 由上层跳登录,不要在这里抛异常 } redis.expire("sess:" + sid, 1800); return attrs; }这段代码里最关键的不是存,是过期策略。每次请求都重置过期时间,等于把“活跃用户”和“登录态”绑在一起,30 分钟内没有操作的会话自动回收,缓存不会无限膨胀。增量写入而不是整对象覆盖,是为了避免并发场景下两个请求各写一半、互相把对方的字段抹掉。清空 Session 时记得用 del 而不是把字段置空,否则 hgetAll 拿到的是一堆空值,上层的登录判断会失效。
4. 多级缓存与数据库拆分:把热点数据挡在数据库之前
数据库永远是这套架构里最难扩的一环。应用层加机器是分钟级的事,数据库加机器要考虑主从同步、数据一致性、分片路由,动一次伤筋动骨。所以思路是尽量不让请求走到数据库——缓存挡在前面,读写分离挡在第二层,分库分表是最后的办法。
4.1 一级本地缓存和二级分布式缓存的分工
缓存能起作用的前提是数据分布不均,资料里引的是二八原则:80% 的访问最终落在 20% 的数据上。热点数据被反复读取,把它放到离应用最近的地方,就能省掉后面所有的网络往返和磁盘 IO。两级缓存的分工是:一级放基本不变或者有规律变化的数据,比如数据字典、类目树;二级放全量热点数据,比如商品详情、活动配置。
请求的读取顺序是一级 → 二级 → 数据库,一级过期或未命中就去二级,二级也没有才回源。回源之后逐级回填,回填时给不同的过期时间,避免所有 key 同时失效。资料里提到缓存比例一般 1:4 就值得上缓存,理论上 1:2 就够了——这个比例说的是“读次数 : 回源次数”,低于这个数,缓存的维护成本超过它省下的数据库压力。
// 两级缓存读取:本地 Caffeine 在前,Redis 在后,回源加互斥锁 public Product getProduct(long skuId) { String key = "sku:" + skuId; Product p = localCache.getIfPresent(key); // 一级:进程内,纳秒级 if (p != null) return p; p = redis.get(key); // 二级:跨进程共享 if (p != null) { localCache.put(key, p); // 回填一级,短过期 return p; } String lock = "lock:" + key; // 只让一个线程去查库 if (redis.setnx(lock, "1", 10)) { try { p = db.queryProduct(skuId); // 回源 if (p != null) { redis.setex(key, 1800 + rnd(300), p); // 二级 30 分钟 + 随机抖动 localCache.put(key, p); // 一级 60 秒,故意设短 } } finally { redis.del(lock); } } else { sleep(50); // 没抢到锁,稍等再读一次二级 p = redis.get(key); } return p; }一级缓存故意设得比二级短,是个血泪经验:本地缓存分布在每台应用机器上,没法主动失效,只能靠过期自然淘汰。二级缓存改了数据还能通知到,一级不行。如果一级设得长,会出现“后台改了商品价格,部分用户十分钟后才看到”这种无法复现的玄学问题。列表页这种对一致性要求不高的场景,一级缓存可以设长一点,详情页和价格相关的一定要短。
4.2 读写分离和分库分表:什么时候切、按什么键切
读写分离解决的是读多写少的场景。主库承担写,从库承担读,靠主备复制同步数据。它能扩读的能力,但扩不了写,而且引入一个绕不过去的问题:主从延迟。刚下单立刻查订单详情,从库可能还没同步到,用户看到的是“订单不存在”。处理办法有两种,一是强制走主库读,二是延迟双查,先查从库,查不到隔 200 毫秒再查一次。前者简单但会把主库压力拉回去,后者体验好一点但要控制重试次数。
分库分表是最后的手段,因为它改变了数据访问的基本假设。垂直切分按业务切,用户业务、商品业务的表放不同库,这个和应用层的垂直拆分是对应的。水平切分是把一张大表按某个键拆成多张,用户表是最典型的例子,按用户 ID 取模分到 16 张物理表。
// 水平分表路由:uid 取模,库内 16 张表 public String routeTable(long uid) { int idx = (int) (uid & 15); // 16 是 2 的幂,位运算比取模快 return String.format("t_user_%02d", idx); } // 跨分片查询:不要拼 UNION ALL,改成先查索引表再按 id 批量取 public List<Order> listOrders(long uid, int page, int size) { String t = routeTable(uid); // 同一用户的数据一定在同一张表 return jdbc.query("SELECT * FROM " + t + " WHERE uid = ? ORDER BY id DESC LIMIT ?, ?", uid, page * size, size); }取模的好处是分布均匀、路由计算快,坏处是扩容时要重新分布。16 张表扩到 32 张,存量数据得全部重算一遍,过程中还得保证不停服。所以分片数一开始就要留余量,按三到五年的量估,宁可从 16 张起步后面只做归档,也别半年就扩一次。想避开这个问题就用一致性 Hash,扩缩容时只有相邻的一段数据需要迁移,代价是实现复杂度高,定位问题时要多绕几层。
提示:分片键选错比不分还糟。订单按用户 ID 分片,那“按订单号查询”和“按商家查询”就变成了跨所有分片的广播查询,本来一个索引能解决的问题变成了 16 次查询。跨分片的查询需求要在设计阶段列出来,数量多的话考虑建异构索引表。
4.3 缓存穿透、击穿、雪崩的三道防线
三个词经常被混着用,但成因完全不同。穿透是查一个数据库里根本没有的数据,缓存永远不命中,每次请求都打到库上;击穿是某个热点 key 过期的瞬间,大量并发同时回源;雪崩是大批 key 在同一时刻集体过期,或者缓存集群整个挂了,压力全部转给数据库。
| 问题 | 成因 | 处理办法 |
|---|---|---|
| 穿透 | 查不存在的数据,缓存不命中 | 空值也缓存,短期过期;参数合法性校验前置 |
| 击穿 | 单个热点 key 过期瞬间并发回源 | 互斥重建,只放一个线程查库 |
| 雪崩 | 大量 key 同时过期 / 缓存集群故障 | 过期时间加随机抖动;二级缓存兜底;数据库限流 |
过期时间打散是最便宜的一招,写入时在基础过期时间上叠一个几百秒的随机数,把原本挤在一个点上的失效分散成一段时间的均匀分布。互斥重建在 4.1 的代码里已经用了,靠 setnx 抢锁,没抢到的线程短暂等待后重读缓存。空值缓存要注意不能设太长,否则正常的商品上架后用户要等好几分钟才能看到,一般给 60 秒到 5 分钟。
5. 高可用避坑:五个把线上拖垮的细节和排查顺序
四个九意味着一年不可用时间只有 53 分钟,这个目标不是靠买贵的设备实现的,是靠把每一个可能出错的地方提前想到。资料里说得很直白:分布式、廉价服务器、开源数据库、操作系统,这些前提决定了故障是不可避免的,能做的只有缩短故障发现时间和恢复时间。下面这五条是我在实际排查里遇到过、并且都能在资料描述的架构里对上号的。
5.1 坑一:Session 同步把内网带宽吃满
现象是每天上午十点左右,应用集群整体 RT 抖动,但没有机器 CPU 打满,日志里也没有异常,网络监控显示内网流量在固定时间有个尖峰。
原因在于用了会话复制方案,而且 Session 里存了大对象——用户的购物车快照和推荐列表被一起塞了进去,单个 Session 几百 KB。集群从 3 台扩到 8 台之后,每次会话变更都要广播到其余 7 台,流量是节点数的平方级增长,扩机器反而让系统更慢。
解决分两步:先把 Session 瘦身,只留用户 ID 和登录态这类小字段,购物车和推荐改成按需查询;再换成集中式存储,应用节点彻底无状态。改完之后集群从 8 台扩到 20 台,内网流量基本没变化。
5.2 坑二:缓存过期时间设成同一个整点
现象是每天晚上零点零五分,数据库 CPU 从 40% 直接冲到 95%,持续两三分钟,慢查询日志里全是商品详情查询。
原因很典型:类目和商品配置是运营每天零点刷新一次,代码里统一setex(key, 86400, value),一万多个 key 的过期时间精确到秒都落在同一时刻。零点一到全部失效,请求全部回源,数据库瞬间被打穿。
解决是把过期时间加上随机抖动,基础时间不变的情况下叠 0 到 600 秒的随机数,失效点被摊平到十分钟里。另外加了一层预热任务,在运营刷新数据之前先把热点 key 主动写一遍,让缓存永远不出现“全空”的状态。
5.3 坑三:主备切换之后读到了旧数据
现象是大促当天主库所在机器故障,自动切换到备库,切换过程只用了 20 秒,但之后陆续有用户投诉“刚下的单查不到”。
原因在主备复制的模式。用的是异步复制,主库写完就返回,binlog 还没来得及传给备库就挂了,切换后备库缺最后一段数据。更麻烦的是应用侧完全不知道发生了切换,读请求照常打到从库,读到的还是更旧的数据。
解决要做三件事:核心的写链路改成半同步复制,至少保证一个备库收到 binlog 才返回成功;切换后的一段时间内,涉及“刚写入的数据”的查询强制走主库;切换动作本身要能被监控捕获并推给业务方,而不是静默完成。
5.4 坑四:分库分表之后跨库 JOIN 全表扫
现象是用户中心上线分库分表之后,后台的订单列表页从 200 毫秒变成 8 秒,DBA 那边看到的是 16 个库同时在被扫。
原因是原来一句SELECT ... FROM orders o JOIN users u ON o.uid = u.id WHERE u.city = 'x',订单按 uid 分片、用户按 id 分片,两个分片规则不对齐,这条 SQL 被改写成对全部订单分片的广播查询,每个分片都要拉全量数据回来再合并。
解决是在设计阶段就把跨分片查询列出来。这个场景的正确做法是让用户中心提供一个按城市查 uid 列表的接口,先拿到 uid 集合,再按 uid 路由到具体的订单分片。异构索引表也是一种选择,把城市到订单的映射单独存一份,代价是多一次写入和一份存储。
5.5 坑五:降级开关只在文档里存在
现象是大促当晚评论服务响应变慢,拖垮了同一台机器上的购物车接口,最后整个下单链路超时。
原因是评论和购物车虽然逻辑上分了核心非核心,但因为机器紧张被合并部署在同一批服务器上,评论服务的线程池占满之后,购物车拿不到线程。文档里写着“可降级”,但代码里根本没有对应的开关,出事时只能靠人工摘节点。
解决是降级要落在代码里,而不是文档里。每个非核心接口都要有独立的线程池或独立的部署单元,配一个可动态修改的开关,开关触发后直接返回兜底数据,不查库不走下游。合并部署的前提是资源隔离做到位,做不到就老老实实分开。
6. 上线前的验证:把 99.99% 当成一次可以演练的故障
架构图画完、代码写完,离“能扛住”还差一整轮验证。容量预估给的是理论值,负载均衡配置给的是预期行为,这两个东西不经过压测和故障演练,没人知道实际会怎样。我的习惯是把验证拆成两块:一块是压测,验证容量;一块是故障注入,验证高可用。
压测不要只打单接口,要按真实的调用链打。用一个混合场景脚本,把浏览、搜索、加购、下单按 50:30:15:5 的比例混着发,跑出来的 RT 才有参考价值。压测过程中重点看三处:单机 CPU 是否稳定在 70% 到 80% 之间,429 的比例是否低于 1%,数据库连接池是否有等待。任何一个指标不对,都要回头改配置,而不是加大压力硬扛。
| 演练项 | 注入方式 | 验收标准 |
|---|---|---|
| 应用节点宕机 | 随机 kill 一个实例 | 30 秒内流量全部转移,错误率低于 0.1% |
| 主库故障切换 | 手动触发主备切换 | 写入中断小于 60 秒,无数据丢失 |
| 缓存集群不可用 | 切断应用与缓存网络 | 数据库 QPS 不超容量上限,接口降级返回 |
| 下游服务超时 | 注入 5 秒延迟 | 上游熔断生效,不出现线程池堆积 |
| 流量突增三倍 | 压测脚本提到峰值 | 限流触发,核心链路 RT 保持稳定 |
故障注入建议在预发环境做,并且一定要有人在旁边盯着,第一次演练翻车是常事。我第一次做缓存不可用演练,切完网络之后数据库三分钟就顶不住了,原因是所有回源请求都没有限流,缓存这层挡板一撤,请求直接压到库里。后来在数据访问层加了一个信号量,回源并发超过阈值就快速失败,让非核心功能先返回兜底数据,才算是真正实现了资料里说的“优雅降级”。
从那以后,我每次做架构设计都会把降级和限流的阈值写进配置中心,而不是留在代码注释里,并且上线前必须把故障演练表跑一遍,跑不过就不发版。这套流程麻烦,但它能把“架构上支持高可用”和“实际上扛得住”之间的那道缝补上。希望帮到你。
本文还有配套的精品资源,点击获取