高并发与高可用技术经验
-
- 如何读这份文档
- [1. 核心心智模型](#1. 核心心智模型)
-
- [1.1 高并发在抢什么](#1.1 高并发在抢什么)
- [1.2 延迟、吞吐、并发的关系](#1.2 延迟、吞吐、并发的关系)
- [1.3 同步调用链上的「放大」](#1.3 同步调用链上的「放大」)
- [1.4 高可用 ≠ 高并发,但经常一起做](#1.4 高可用 ≠ 高并发,但经常一起做)
- [2. 易误解概念澄清(必读)](#2. 易误解概念澄清(必读))
-
- [2.1 「端口是 65535,所以最多同时 65535 个请求?」------错](#2.1 「端口是 65535,所以最多同时 65535 个请求?」——错)
- [2.2 「nofile=65535 就是最多 65535 连接」------不精确](#2.2 「nofile=65535 就是最多 65535 连接」——不精确)
- [2.3 「连接数 = 并发请求数 = QPS」------三者不同](#2.3 「连接数 = 并发请求数 = QPS」——三者不同)
- [2.4 「把 Tomcat max-threads 调到 2000,性能就 ×10」------错](#2.4 「把 Tomcat max-threads 调到 2000,性能就 ×10」——错)
- [2.5 「超时设大一点更稳定」------往往更不稳定](#2.5 「超时设大一点更稳定」——往往更不稳定)
- [2.6 「CPU 只有 20%,说明系统很空」------不一定](#2.6 「CPU 只有 20%,说明系统很空」——不一定)
- [2.7 「Redis / DB 连接池开越大越好」------错](#2.7 「Redis / DB 连接池开越大越好」——错)
- [2.8 「限流从不触发才说明阈值合理」------错](#2.8 「限流从不触发才说明阈值合理」——错)
- [2.9 「多实例了就不会挂」------不充分](#2.9 「多实例了就不会挂」——不充分)
- [2.10 「重试能提高可用性」------看情况](#2.10 「重试能提高可用性」——看情况)
- [2.11 「监听 8080,所以本机只能有一个服务用 8080」------对;但和并发无关](#2.11 「监听 8080,所以本机只能有一个服务用 8080」——对;但和并发无关)
- [2.12 「带宽 1Gbps,所以 QPS 上限很好算」------只算了一维](#2.12 「带宽 1Gbps,所以 QPS 上限很好算」——只算了一维)
- [2.13 「Keep-Alive / 连接池会让一台机器无限并发」------错](#2.13 「Keep-Alive / 连接池会让一台机器无限并发」——错)
- [2.14 「backlog / accept-count 调很大就能抗住尖峰」------危险](#2.14 「backlog / accept-count 调很大就能抗住尖峰」——危险)
- [2.15 一张对照表(贴墙上)](#2.15 一张对照表(贴墙上))
- [3. 多层防护架构(通用 L0~L7)](#3. 多层防护架构(通用 L0~L7))
- [4. 操作系统与机器规格](#4. 操作系统与机器规格)
-
- [4.1 规格怎么想](#4.1 规格怎么想)
- [4.2 文件句柄 nofile------怎么设、怎么验](#4.2 文件句柄 nofile——怎么设、怎么验)
- [4.3 网络 sysctl------每项干什么、怎么设](#4.3 网络 sysctl——每项干什么、怎么设)
- [4.4 JVM------怎么设(Java 17 可执行步骤)](#4.4 JVM——怎么设(Java 17 可执行步骤))
-
- [步骤 1:先算机器给 JVM 多少内存](#步骤 1:先算机器给 JVM 多少内存)
- [步骤 2:选 GC(Java 17)](#步骤 2:选 GC(Java 17))
- [步骤 3:必开的诊断项(建议默认带上)](#步骤 3:必开的诊断项(建议默认带上))
- [步骤 4:用指标判断「堆小了还是大了」](#步骤 4:用指标判断「堆小了还是大了」)
- [步骤 5:和「线程打满」一起看(避免误判)](#步骤 5:和「线程打满」一起看(避免误判))
- [4.4.1 一张「填空式」JVM 决策表(可直接抄到运行文档)](#4.4.1 一张「填空式」JVM 决策表(可直接抄到运行文档))
- [5. Nginx / 入口层](#5. Nginx / 入口层)
-
- [5.1 职责边界](#5.1 职责边界)
- [5.2 配置骨架(学习用)](#5.2 配置骨架(学习用))
- [5.3 知识点清单](#5.3 知识点清单)
- [5.4 Nginx 限流与连接------怎么设](#5.4 Nginx 限流与连接——怎么设)
- [5.5 边缘还可以做什么(进阶)](#5.5 边缘还可以做什么(进阶))
- [6. 应用入口线程模型(Tomcat / Spring Boot)](#6. 应用入口线程模型(Tomcat / Spring Boot))
-
- [6.1 为什么「调大 max-threads」不是根治](#6.1 为什么「调大 max-threads」不是根治)
- [6.2 推荐基线(先抄这个,再按 6.3 算)](#6.2 推荐基线(先抄这个,再按 6.3 算))
- [6.3 Tomcat 线程------怎么算、怎么调](#6.3 Tomcat 线程——怎么算、怎么调)
- [6.4 线程池通用规范](#6.4 线程池通用规范)
- [7. 超时分层对齐(极易错)](#7. 超时分层对齐(极易错))
-
- [7.1 对齐关系(先抄结构,再填数字)](#7.1 对齐关系(先抄结构,再填数字))
- [7.2 超时------怎么设(填空法)](#7.2 超时——怎么设(填空法))
- [8. 出站 HTTP:超时、连接池、舱壁](#8. 出站 HTTP:超时、连接池、舱壁)
-
- [8.1 三个能力缺一不可](#8.1 三个能力缺一不可)
- [8.2 强制规范(任何项目通用)](#8.2 强制规范(任何项目通用))
- [8.3 配置示例与「怎么设」](#8.3 配置示例与「怎么设」)
- [8.4 舱壁 vs 熔断(易混)](#8.4 舱壁 vs 熔断(易混))
- [9. 限流、熔断、系统保护(Sentinel 等)](#9. 限流、熔断、系统保护(Sentinel 等))
-
- [9.1 规则类型速查](#9.1 规则类型速查)
- [9.2 资源怎么划分](#9.2 资源怎么划分)
- [9.3 熔断参数阅读示例](#9.3 熔断参数阅读示例)
- [9.3.1 限流 / 熔断------怎么设(起步法)](#9.3.1 限流 / 熔断——怎么设(起步法))
- [9.4 降级策略(产品可接受)](#9.4 降级策略(产品可接受))
- [9.5 更多流量治理手段](#9.5 更多流量治理手段)
- [10. 缓存](#10. 缓存)
-
- [10.1 何时缓存](#10.1 何时缓存)
- [10.2 单飞加载(防击穿)](#10.2 单飞加载(防击穿))
- [10.3 三大经典问题](#10.3 三大经典问题)
- [10.4 进阶](#10.4 进阶)
- [10.5 Redis 连接池------怎么设](#10.5 Redis 连接池——怎么设)
- [11. 数据库与预计算](#11. 数据库与预计算)
-
- [11.1 连接池------怎么设](#11.1 连接池——怎么设)
- [11.2 查询与模型](#11.2 查询与模型)
- [11.3 异步削峰(写与通知)](#11.3 异步削峰(写与通知))
- [12. 业务 API 设计与读路径改造](#12. 业务 API 设计与读路径改造)
- [13. 前端 / 客户端(L0)](#13. 前端 / 客户端(L0))
- [14. 多实例、网关与弹性](#14. 多实例、网关与弹性)
- [15. 安全与鉴权对容量的影响](#15. 安全与鉴权对容量的影响)
- [16. 可观测、压测与应急](#16. 可观测、压测与应急)
-
- [16.1 黄金信号](#16.1 黄金信号)
- [16.2 必打日志 / 指标](#16.2 必打日志 / 指标)
- [16.3 应急步骤(通用)](#16.3 应急步骤(通用))
- [16.4 压测清单](#16.4 压测清单)
- [16.5 混沌与 SLO(进阶)](#16.5 混沌与 SLO(进阶))
- [17. 完整改造清单(自学自检用)](#17. 完整改造清单(自学自检用))
-
- [A. 架构](#A. 架构)
- [B. 入口与网络](#B. 入口与网络)
- [C. 应用运行时](#C. 应用运行时)
- [D. 出站与韧性](#D. 出站与韧性)
- [E. 数据](#E. 数据)
- [F. 客户端](#F. 客户端)
- [G. 运维闭环](#G. 运维闭环)
- [18. 分阶段落地路线图](#18. 分阶段落地路线图)
- [19. 验收门禁(通用)](#19. 验收门禁(通用))
- [20. 知识点速查表](#20. 知识点速查表)
- [21. 常见误区(对照自查)](#21. 常见误区(对照自查))
如何读这份文档
- 先建立心智模型:第 1 章(抢什么资源、容量公式)。
- 立刻看易误解概念:第 2 章(端口 65535、线程≠QPS 等,避免后面越学越偏)。
- 再按流量方向往里走:多层架构 → OS/JVM(含「怎么设」)→ Nginx → Tomcat → 超时 → 出站 → 限流熔断 → 缓存/DB → 前端 → 多实例 → 可观测。
- 最后用清单与验收:改造清单、路线图、验收门禁。
- 配置怎么用 :凡写「怎么设」的小节,都按 选起点 → 算关系 → 看指标 → 压测校准 四步走;示例数字不是标准答案。
一句话总纲:
高并发不是「把线程调大」,而是:入口削峰 → 本服务资源有界 → 依赖被关进笼子 → 读多走缓存/预计算 → 故障快速失败并降级 → 全程可观测。
1. 核心心智模型
1.1 高并发在抢什么
任何一次请求,最终都在消耗几类有界资源:
| 资源 | 典型上限来源 | 打满后的现象 |
|---|---|---|
| 工作线程 | Tomcat max-threads、业务线程池 |
新请求排队或拒绝,像「服务死了」 |
| 连接 | Nginx worker_connections、TCP 端口、HTTP 连接池 |
Too many open files、建连失败 |
| 队列 | accept-count、线程池 queue、MQ |
尾延迟爆炸,然后超时雪崩 |
| 下游配额 | 第三方 QPS、DB 连接池、Redis 连接 | 等待超时,错误率飙升 |
| CPU / 内存 / 带宽 | 机器规格、GC、序列化 | 吞吐下降或 Full GC |
知识点: CPU 不高也可能已经「挂了」------大量线程阻塞在 Socket.read / getConnection 时,吞吐接近 0,但 CPU 很闲。
1.2 延迟、吞吐、并发的关系
text
可持续 QPS ≈ 有效并发数 / 平均响应时间(秒)
例:出站舱壁并发 = 64,下游平均 0.5s
→ 理想吞吐 ≈ 64 / 0.5 = 128 QPS
实际再乘安全水位 0.3~0.5(GC、锁、抖动)
| 概念 | 含义 | 调参误区 |
|---|---|---|
| 并发 | 同一时刻「在飞」的请求数 | 盲目加大并发 ≠ 更高 QPS |
| 吞吐 (QPS) | 单位时间完成数 | 延迟上升时,同样并发下 QPS 下降 |
| 延迟 (P95/P99) | 用户体感与超时预算 | 只看平均值会掩盖长尾 |
知识点: 提升吞吐只有三条路------缩短平均耗时 、提高有效并发(在资源够的前提下) 、减少无效请求(缓存/合并/限流)。
1.3 同步调用链上的「放大」
text
1 个页面打开
× N 个组件并行请求
× 每个请求扇出 M 个下游
= 入口 QPS × N × M 的下游压力
大屏、门户、管理后台「一屏多卡」最容易踩这个坑。
知识点: 先消灭放大(合并、缓存、预聚合),再谈加机器。
1.4 高可用 ≠ 高并发,但经常一起做
| 高并发 | 高可用 | |
|---|---|---|
| 目标 | 扛住流量与延迟 | 故障时服务仍可用(可降级) |
| 手段 | 限流、缓存、扩容、异步 | 多实例、熔断、降级、隔离、多活 |
| 关系 | 没隔离的高并发更容易雪崩 | 高可用设计本身也提高稳态并发能力 |
2. 易误解概念澄清(必读)
这一章专门拆「听起来像对、用起来全错」的说法。自学时建议每条都能用自己的话复述一遍。
2.1 「端口是 65535,所以最多同时 65535 个请求?」------错
端口号是 16 位整数(0~65535),但这不等于「整机只能同时接 65535 个连接」。
要分清两件事:
| 角色 | 端口怎么用 | 并发上限主要卡在哪 |
|---|---|---|
| 服务端监听 | 只占用 1 个 (或少数几个)端口,如 :8080 |
文件句柄 nofile、内存、应用线程/连接配置、CPU |
| 客户端出站 | 每个到「同一目标 IP:端口」的连接,通常要占一个本机临时端口 | 临时端口范围 + TIME_WAIT + nofile |
一条 TCP 连接的身份是五元组:
text
(协议, 本机IP, 本机端口, 对端IP, 对端端口)
- 一万个浏览器连你的
0.0.0.0:8080:服务端都是 同一个监听端口 8080 ,靠「对端 IP+端口」区分连接,不是「一连接占一个 8080」。 - 你的应用作为客户端去打同一个第三方
1.2.3.4:443:本机临时端口会消耗;默认ip_local_port_range大约几万个,再叠加 TIME_WAIT,出站并发才可能碰到端口墙。 - 打不同目标 IP/端口时,临时端口可以复用同一本地端口号(五元组仍唯一),所以「65535」更不是全局并发天花板。
正确记忆:
- 入站高并发:优先看
nofile、Nginxworker_connections、Tomcatmax-connections、机器内存。 - 出站高并发:才额外担心临时端口与连接复用(HTTP keepalive / 连接池)。
2.2 「nofile=65535 就是最多 65535 连接」------不精确
nofile 限制的是进程能打开的文件描述符数量。每个 TCP socket 通常占 1 个 FD,此外还有:
- 监听 socket、管道、日志文件、jar、加载的 so
- 每个线程、stdout 等也会占 FD
所以:
text
可支撑连接数 ≈ nofile - 非 socket 占用(往往留 10%~20% 余量)
且 Nginx 还要满足:worker_processes × worker_connections 不超过进程 nofile(以及系统级限制)。
2.3 「连接数 = 并发请求数 = QPS」------三者不同
| 概念 | 含义 | 举例 |
|---|---|---|
| 连接 (Connection) | TCP/HTTP 通道是否还开着 | Keep-Alive 下,1 个连接可串行很多请求 |
| 并发请求 (In-flight) | 此刻正在处理、尚未返回的请求 | Tomcat 工作线程大致对应「正在处理」的数 |
| QPS | 每秒完成多少请求 | 200 并发 × 平均 50ms → 约 4000 QPS |
text
QPS ≈ 并发请求数 / 平均耗时(秒)
1 万个 Keep-Alive 空闲连接,可能只有 50 个请求在飞 → QPS 由「在飞」决定,不是由「连接总数」决定。
误导用法: 看到「支持十万连接」就以为有十万 QPS------若每个请求 1 秒且只有 200 线程,上限仍约 200 QPS。
2.4 「把 Tomcat max-threads 调到 2000,性能就 ×10」------错
线程多了:
- 每个线程约占用栈内存(常见默认约 1MB 量级,视
-Xss) - 上下文切换变多
- 更致命:若线程都阻塞在慢第三方,2000 个慢请求 = 拖死整站,比 200 个死得更彻底
吞吐仍受 QPS ≈ 有效并发 / 耗时 约束;不降耗时、不加舱壁,加线程只是加「同时卡死的人数」。
2.5 「超时设大一点更稳定」------往往更不稳定
超时从 3s 调到 30s:
- 单个故障请求占用线程/连接的时间 ×10
- 同样流量下,池更容易被打满
- 用户侧更晚失败,重试更容易叠成雪崩
正确做法: 超时按 SLA 设短;靠缓存、降级、熔断在失败时快速返回。
2.6 「CPU 只有 20%,说明系统很空」------不一定
阻塞型服务常见:
- 线程全卡在
Socket.read/ 等 DB 连接 → CPU 很低,接口全超时 - 要看:繁忙线程、队列长度、连接池等待、P99、GC、FD
2.7 「Redis / DB 连接池开越大越好」------错
池过大:
- 数据库/Redis 服务器被过多会话拖慢
- 应用侧线程更容易「一起冲进去」放大故障
池是限流器,不是越大越快。见第 10、11 章「怎么设」。
2.8 「限流从不触发才说明阈值合理」------错
从不触发的限流 = 没装安全气囊。
合理限流应在压测尖峰或故障重试时真的触发,并返回明确 429,保护核心容量。
2.9 「多实例了就不会挂」------不充分
若:
- 会话粘在单机内存
- 限流按「单机很大」却用集群共享同一套过大阈值
- 所有实例同步打同一个慢依赖且无舱壁
则多实例只是「一起挂」或「轮流挂」。高可用要靠:无状态 + 隔离 + 熔断降级 + 健康检查。
2.10 「重试能提高可用性」------看情况
| 安全 | 危险 |
|---|---|
| 幂等 GET、有限次、指数退避+抖动 | 非幂等写、齐刷重试、超时很长还重试 |
下游正在恢复时,无节制重试 = 第二次打死它。
2.11 「监听 8080,所以本机只能有一个服务用 8080」------对;但和并发无关
端口冲突只约束「谁绑定这个端口」。
绑定成功后,该端口上的并发连接数与「65535」无直接相等关系(见 2.1)。
2.12 「带宽 1Gbps,所以 QPS 上限很好算」------只算了一维
text
粗算:QPS_带宽 ≈ 带宽 / 平均响应体大小
实际还可能先撞上:CPU、GC、线程、DB、下游 QPS、锁。带宽只是天花板之一。
2.13 「Keep-Alive / 连接池会让一台机器无限并发」------错
连接复用能减少握手和临时端口消耗,但同时在飞的请求 仍受:线程池、舱壁、max-requests、CPU、下游配额限制。
复用解决的是「建连成本」,不是「处理能力」。
2.14 「backlog / accept-count 调很大就能抗住尖峰」------危险
队列只是把失败变成「晚一点失败」:队列越长,P99 越差,超时越多,重试越多。
尖峰应靠:限流快速拒绝 + 扩容 + 缓存,而不是无限排队。
2.15 一张对照表(贴墙上)
| 误传 | 更准确的说法 |
|---|---|
| 65535 端口 = 65535 并发 | 入站共享监听端口;出站才消耗临时端口 |
| 连接数 = QPS | QPS ≈ 在飞请求 / 耗时 |
| 线程越大越快 | 线程是并发上限,耗时不降则吞吐不升,故障时更惨 |
| 超时越大越稳 | 超时越长占用越久,更容易雪崩 |
| CPU 低 = 空闲 | 可能是 IO 堵死 |
| 池越大越好 | 池是保护阀,要按下游能力设 |
| 限流别触发 | 限流要在尖峰时能保护 |
| 多实例 = 高可用 | 还要隔离、无状态、降级 |
| 连接复用 = 无限能力 | 只减建连成本,在飞仍有界 |
| 队列调很大抗尖峰 | 队列过长会拖死尾延迟 |
3. 多层防护架构(通用 L0~L7)
按流量从外到内,建议建立 8 层(可分阶段上线):
text
L0 客户端:降频、合并、退避、降级 UI
L1 边缘 / Nginx:连接与速率限流、超时、静态缓存、TLS
L2 网关:路由鉴权、粗粒度限流(Sentinel gw-flow 等)
L3 应用入口:Tomcat/Netty 线程、accept 队列、慢请求隔离
L4 业务读路径:缓存、预聚合、批量、去重、异步削峰
L5 出站依赖:超时 + 连接池 + 舱壁(Bulkhead)
L6 韧性:限流 / 熔断 / 系统保护规则 + 产品降级
L7 基础设施:Redis/DB/MQ 池、多实例、资源规格、容器弹性
原则:
- 越靠外越粗、越便宜;越靠内越细、越贵。
- 慢依赖必须在 L5/L6 被关进笼子,绝不能占满 L3 入口线程。
- 每一层超时要对齐(见第 6 章)。
4. 操作系统与机器规格
4.1 规格怎么想
| 角色 | 思考点 |
|---|---|
| 反向代理 | 主要吃网络与少量 CPU;连接数要够 |
| 网关 | 鉴权 + 限流热点;建议多实例 |
| 业务 API | 出站舱壁需要线程与内存;勿与重计算混部 |
| 缓存 | 内存按 key 估算 × 1.5;尽量独立 |
| 数据库 / OLAP | 按查询模式单独评估,别和应用抢盘 |
怎么定规格(实用): 先用压测得出「单实例稳态 QPS 与 P95」,再按峰值流量 ÷(单实例稳态 × 0.5~0.7 安全水位)得到实例数;CPU 长期 >70% 或 GC/线程已触顶再加核加内存,而不是先盲买大机器。
4.2 文件句柄 nofile------怎么设、怎么验
要解决的问题: 连接/文件太多时出现 Too many open files,新连接失败。
推荐起点:
ini
# /etc/security/limits.d/app.conf (或 systemd: LimitNOFILE=65535)
* soft nofile 65535
* hard nofile 65535
| 场景 | 建议 |
|---|---|
| 普通业务 API | 65535 通常够用 |
| Nginx / 网关高连接 | 与 worker_processes × worker_connections 对齐,常设 65535 或更高(如 1048576,需系统允许) |
| 容器 | 查 ulimit -n 与 cgroup;只改宿主机 limits 而容器内仍是 1024 等于没改 |
怎么验证生效:
bash
# 当前 shell
ulimit -n
# 看已启动的 Java 进程(把 <pid> 换成实际)
cat /proc/<pid>/limits | grep 'open files'
怎么估算要不要再加大:
text
预留 FD ≈ 预期峰值连接 × 1.2 + 日志/jar 等(可先按 2000 估)
若 Nginx: worker_processes × worker_connections < nofile(每个 worker 进程各自一份)
4.3 网络 sysctl------每项干什么、怎么设
conf
# /etc/sysctl.d/99-net-app.conf
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.core.netdev_max_backlog = 16384
vm.swappiness = 10
应用:sudo sysctl --system 或 sysctl -p。
| 参数 | 什么时候改 | 怎么定 |
|---|---|---|
somaxconn |
突发建连被拒、accept 队列满 | ≥ 应用 listen backlog / Nginx 相关队列;常见 1024→4096 |
tcp_max_syn_backlog |
SYN 洪水或突发握手 | 与 somaxconn 同量级或更大 |
ip_local_port_range |
出站连同一目标太多、报端口耗尽 | 扩大范围(如 1024 65535);更优先用连接池复用 |
tcp_tw_reuse |
TIME_WAIT 多、出站建连慢 | 作客户端的机器可开;按内核版本文档理解语义 |
swappiness |
偶发延迟尖刺、有 swap | 业务机常设 1~10,尽量别靠 swap 撑内存 |
验证出站端口是否吃紧:
bash
ss -s
# 或统计 TIME_WAIT
ss -ant | awk '{print $1}' | sort | uniq -c
若 TIME_WAIT / 出站连接接近临时端口容量 → 先加连接复用,再考虑调 range。
4.4 JVM------怎么设(Java 17 可执行步骤)
目标不是背参数,而是填一张内存账单,再选出堆大小与 GC。
步骤 1:先算机器给 JVM 多少内存
假设物理机或容器内存为 M(例如 16GB):
text
留给操作系统 / 页面缓存 ≈ 1~2GB(容器可少些,但别为 0)
堆外(直接内存、线程栈、Code Cache、元空间等)≈ 1~3GB(Netty/OkHttp 多则偏大)
可给堆 -Xmx ≈ M - 上述预留
| 容器/机器内存 | 常见堆起点(单 Java 进程) | 说明 |
|---|---|---|
| 4GB | -Xms1g -Xmx1g 或 2g |
小服务;堆外也要留 |
| 8GB | -Xms2g -Xmx2g~4g |
中等 API |
| 16GB | -Xms4g -Xmx4g~8g |
常见业务节点 |
| 32GB+ | 先 8g~16g,用监控决定 |
堆不是越大越好 |
硬规则:
-Xms与-Xmx设成一样,避免运行时扩堆带来的抖动。- 堆 + 堆外 + 系统 < 物理内存,否则会 swap 或被 OOM Killer 杀掉。
- 同一机器多个 Java 进程:把内存账单按进程拆开,禁止每个都按「整机 70%」设堆。
步骤 2:选 GC(Java 17)
| 选择 | 何时用 | 怎么开 |
|---|---|---|
| G1(默认推荐起点) | 堆几 GB~十几 GB,通用延迟 | -XX:+UseG1GC(17 常已默认)-XX:MaxGCPauseMillis=200 |
| ZGC | P99 极敏感、堆较大、已会压测对比 | -XX:+UseZGC(按发行版文档) |
| Parallel | 批处理吞吐优先、可接受较长停顿 | 一般不用于在线大屏/API |
MaxGCPauseMillis=200 的含义: 给 G1 的目标停顿,不是保证;设太小(如 20)可能导致频繁 GC、吞吐变差。在线接口从 200ms 目标起步,看 GC 日志再调。
步骤 3:必开的诊断项(建议默认带上)
text
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump/
-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=50M
有 Netty / 大量直接内存时再加(按版本二选一或组合,以你 JDK 文档为准):
text
-XX:MaxDirectMemorySize=1g
线程特别多时关注栈:默认 -Xss 约 1MB,线程数 × Xss 会吃掉堆外;不要无故把 Tomcat 调到几千。
步骤 4:用指标判断「堆小了还是大了」
| 观察 | 判断 | 动作 |
|---|---|---|
| Young GC 很频繁,且占用明显 CPU时间;Old 持续上涨最终 Full GC | 堆偏小或泄漏 | 先查泄漏;确认无泄漏再适度加大堆 |
| 单次停顿经常 > 超时预算(如经常 300ms+ 而接口超时 1s) | 堆可能偏大或 GC 不合适 | 略减堆,或评估 ZGC;优化分配 |
| 堆占用长期 < 40%,GC 很轻松 | 堆可能浪费 | 可减小堆,把内存还给 OS/页面缓存/同机其它进程 |
| 进程 RSS 接近机器内存,偶发卡住 | 可能堆外+堆打满或 swap | 查直接内存、线程数、是否 swap |
看 GC 日志是否健康(自学抓手):
- 停顿时间是否经常接近或超过
MaxGCPauseMillis - Full GC / 长暂停是否与接口超时时间对齐出现
- 分配速率是否异常(短时间大量对象)
步骤 5:和「线程打满」一起看(避免误判)
text
CPU 低 + 接口超时 → 先 jstack / 看繁忙线程,不要先加堆
CPU 高 + GC 日志停顿长 → 再调堆 / GC
CPU 高 + GC 很少 → 可能是业务计算或序列化,不是堆大小问题
bash
# 示例:看线程在干什么(把 pid 换掉)
jstack <pid> | head
# 或用 arthas thread / dashboard(若环境允许)
4.4.1 一张「填空式」JVM 决策表(可直接抄到运行文档)
| 项 | 你的值 | 填写说明 |
|---|---|---|
| 容器/机器内存 M | ___ GB | |
| 同机 Java 进程数 | ___ | |
本进程 -Xmx |
___ GB | ≈ (M − 系统预留) / 进程数,再留堆外 |
-Xms |
与 Xmx 相同 | |
| GC | G1 / ZGC | 默认 G1 |
MaxDirectMemorySize |
___ | 有 Netty 时建议显式设 |
| HeapDump 路径 | /data/logs/heapdump/ |
目录要可写、有盘空间 |
| GC 日志路径 | /data/logs/gc.log |
上线后至少看一周 |
例(16GB 机器、单 Java、偏 IO 的 API):
text
系统+页缓存 2g + 堆外约 2g + 堆 4g = 8g 量级占用,仍有余量
→ -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 ...
压测后若 GC 轻松且堆占用低,可试 3g;若频繁 Full GC 且无泄漏,再试 6g。
5. Nginx / 入口层
5.1 职责边界
| 做 | 不做 |
|---|---|
| TLS、静态资源、反向代理 | 细粒度业务鉴权细节 |
| 连接数 / 请求速率限流 | 替代业务级 per-API 限流 |
| upstream 超时与谨慎重试 | 对非幂等写无限重试 |
| 访问日志(含上游耗时) | 替代 Redis 业务缓存 |
5.2 配置骨架(学习用)
nginx
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 10240;
multi_accept on;
use epoll; # Linux
}
http {
sendfile on;
keepalive_timeout 65;
keepalive_requests 1000;
limit_req_zone $binary_remote_addr zone=api_per_ip:20m rate=20r/s;
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
upstream app_backend {
least_conn;
server 10.x.x.11:8080 max_fails=3 fail_timeout=10s;
server 10.x.x.12:8080 max_fails=3 fail_timeout=10s;
keepalive 64;
}
log_format main_timed '$remote_addr [$time_local] "$request" '
'status=$status ut=$upstream_response_time '
'rt=$request_time upstream=$upstream_addr';
server {
listen 443 ssl http2;
client_max_body_size 16m;
location /api/ {
limit_req zone=api_per_ip burst=40 nodelay;
limit_conn conn_per_ip 50;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 3s;
proxy_send_timeout 15s;
proxy_read_timeout 15s;
# 仅幂等 GET 谨慎开启;POST 默认不要重试
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
proxy_pass http://app_backend;
}
location / {
root /data/www/app;
try_files $uri $uri/ /index.html;
expires 7d;
}
}
}
限流友好响应:
nginx
limit_req_status 429;
limit_conn_status 429;
error_page 429 = @too_many;
location @too_many {
default_type application/json;
return 429 '{"code":429,"msg":"请求过于频繁,请稍后重试"}';
}
5.3 知识点清单
| 配置项 | 含义 | 调参注意 |
|---|---|---|
limit_req |
令牌桶限速 | burst 过小误杀首屏并发;过大削峰失效 |
limit_conn |
并发连接上限 | 防单客户端占满 |
least_conn |
选连接少的上游 | 适合长短请求混杂 |
keepalive + Connection "" |
复用到上游的连接 | 显著降低握手 |
proxy_read_timeout |
等上游响应 | 必须与后端超时对齐 |
proxy_next_upstream |
失败换机 | 禁止对非幂等写盲目重试 |
$upstream_response_time |
上游耗时 | 区分 Nginx 排队 vs 后端慢 |
| HTTP/2 | 多路复用 | 减轻浏览器连接数限制 |
5.4 Nginx 限流与连接------怎么设
worker_connections:
text
单机理论连接能力 ≈ worker_processes × worker_connections
且每个 worker 的连接数 < 该进程 nofile
worker_processes auto 时约等于 CPU 核数。例:4 核 × 10240 ≈ 4 万连接量级(理论),还要被内存和后端能力挡住。
limit_req(速率):
- 估单 IP 正常峰值(大屏一页并发打开可能短时几十 QPS)。
rate设为「正常峰值以上一点」,burst设为「首屏毛刺」。- 例:
rate=20r/s+burst=40→ 平滑 20/s,允许短突发到约 60。 - 压测时看是否误杀:若正常首屏大量 429 → 加大 burst 或按 path 拆分限流带。
limit_conn(并发连接): 防单客户端占满;API 常见单 IP 20~100,按是否经 NAT(多人共出口 IP)调整。
proxy_*_timeout: 直接填第 7 章表格里的值,与后端 callTimeout 对齐。
upstream keepalive: 到后端的空闲长连接数,常见 32~128;配合 proxy_http_version 1.1 + Connection ""。
5.5 边缘还可以做什么(进阶)
- CDN / 静态与 API 分离
- TLS 会话复用、证书链优化
- gzip/brotli(注意 CPU;已压缩内容勿再压)
- L4 SLB + 深度健康检查(不只端口通)
6. 应用入口线程模型(Tomcat / Spring Boot)
6.1 为什么「调大 max-threads」不是根治
- 每个阻塞在下游
read的请求占 1 个工作线程。 - 200 → 800:更多请求一起卡死,内存与连接压力更大。
- 正确做法:限制「能进入出站等待」的并发,并让超时快速失败。
6.2 推荐基线(先抄这个,再按 6.3 算)
yaml
server:
tomcat:
threads:
max: 200
min-spare: 20
accept-count: 200
max-connections: 10000
connection-timeout: 5000
keep-alive-timeout: 15000
max-keep-alive-requests: 100
accesslog:
enabled: true
pattern: '%h %t "%r" %s %D %b'
| 参数 | 作用 | 怎么设(实用) |
|---|---|---|
threads.max |
同时「正在处理」的请求上限 | 见 6.3;无舱壁时它≈能同时堵在下游的人数 |
min-spare |
空闲保活线程 | 约为 max 的 10%~20% |
accept-count |
线程满后 TCP/请求排队长度 | 与 max 同量级或略小;宁肯快拒绝,勿排几千 |
max-connections |
NIO 连接总数(含 keep-alive 空闲) | 远大于 threads.max;受 nofile 约束 |
connection-timeout |
连接建立/等待超时(ms) | 常见 3~5s |
keep-alive-timeout |
空闲连接保持 | 与 Nginx/客户端习惯对齐,常见 10~30s |
%D |
访问日志耗时 ms | 用来找慢接口,建议打开 |
6.3 Tomcat 线程------怎么算、怎么调
核心公式(再背一次):
text
目标 QPS ≈ threads.max / 平均响应时间(秒) (理想情况)
安全 QPS ≈ 上面结果 × 0.3~0.5
步骤:
- 量平均耗时:压测或线上 P50/P95(例如平均 200ms=0.2s)。
- 定目标 QPS:单实例要扛多少(例如 500 QPS)。
- 反推线程:
threads ≈ 目标QPS × 耗时→500 × 0.2 = 100,再留余量 → 从 150~200 做起点。 - 加上游舱壁约束: 若出站舱壁只有 32,把
threads.max调到 800 没有意义------多出来的线程仍会堵在拿舱壁许可或更快把无舱壁路径打爆。 - 看栈内存: 约
threads.max × Xss(如 200×1MB≈200MB)要算进堆外账单。
| 调参信号 | 动作 |
|---|---|
| 繁忙线程长期 ≈ max,队列也满,P99 暴涨 | 先降耗时(缓存/超时/舱壁),再考虑略增线程或扩实例 |
| 繁忙线程只有 max 的 20%,CPU 很低 | 线程不是瓶颈,别加线程 |
| 下游故障时繁忙线程顶满 | 加舱壁/熔断/缩短超时,不要加线程 |
6.4 线程池通用规范
| 规则 | 原因 |
|---|---|
| 池必须有界 | 无界队列 / CachedThreadPool 会在故障时 OOM 或打爆机器 |
拒绝策略慎用 CallerRuns |
会把压力打回调用方线程(如 Tomcat) |
| 出站池用 Abort | 池满立刻失败,保护入口 |
| 禁止默认公共 ForkJoin 扛 IO | CompletableFuture 默认池不适合大量阻塞 IO |
| 池要命名 | 便于 jstack 与监控归因 |
进阶: Java 21 虚拟线程适合大量阻塞 IO,但仍要对下游并发与连接池设界,否则会打爆依赖。
7. 超时分层对齐(极易错)
7.1 对齐关系(先抄结构,再填数字)
text
客户端 timeout 例如 15s
Nginx proxy_read 例如 15s
Gateway 路由超时 ≤ Nginx
业务整体超时 ≤ Gateway
出站 callTimeout 例如 8~10s(第三方)
舱壁 awaitTimeout 略大于 callTimeout(例如 12s)
DB / Redis timeout 更短(例如 1~3s)
7.2 超时------怎么设(填空法)
- 定用户可接受上限
T_user(例如大屏卡片 15s 还不出就该失败)。 - 留给网络与排队 10%~20%(Nginx/网关抖动)。
- 剩余给业务 ;业务里若调第三方,第三方
callTimeout必须明显小于业务剩余时间。 - DB/Redis 用更短超时,避免慢查询占满线程。
- 全链路写进一张表,上线前对一下,禁止「前端 5s、后端 30s」这种错位。
| 层级 | 你的值 | 填写提示 |
|---|---|---|
| 前端 | ___ s | = 产品可接受上限 |
Nginx proxy_read_timeout |
___ s | ≈ 前端,或略小 |
出站 callTimeout |
___ s | ≤ 前端的 50%~70%(给聚合留时间) |
| Redis/DB | ___ ms | 通常 1000~3000ms |
| 知识点 | 说明 |
|---|---|
| 外层 ≥ 内层 | 或明确短于内层并接受截断 |
| 错位后果 | 一端已超时关闭,另一端仍占线程 |
| 预算分配 | 总超时要留给「排队 + 业务 + 最慢依赖」 |
| 宁短勿长 | 短超时 + 降级,优于长超时把池打满 |
8. 出站 HTTP:超时、连接池、舱壁
8.1 三个能力缺一不可
| 能力 | 作用 |
|---|---|
| 超时 | connect / read / write / call,避免永久卡死 |
| 连接池 | 复用连接;限制全局与单主机并发 |
| 舱壁 Bulkhead | 独立线程池或信号量;池满快速失败,保护入口线程 |
8.2 强制规范(任何项目通用)
- 禁止无超时的
RestTemplate/HttpURLConnection/ 随意 new 客户端。 - 统一出口(一个 Outbound 客户端或封装),禁止业务自行创建。
- 拒绝策略:Abort,不要用会打回调用方的策略扛出站。
- 超时日志必须带 METHOD + URL/path,否则无法运维。
- 按依赖拆舱壁(厂家 A / B / 内部服务),互不拖死。
8.3 配置示例与「怎么设」
yaml
# 概念示例:按你们组件属性名映射即可
outbound:
http:
connect-timeout-ms: 3000
read-timeout-ms: 8000
write-timeout-ms: 8000
call-timeout-ms: 10000
max-idle-connections: 32
keep-alive-minutes: 5
max-requests: 64
max-requests-per-host: 16
bulkhead-core-size: 16
bulkhead-max-size: 32
bulkhead-queue-capacity: 64
bulkhead-await-timeout-ms: 12000
| 参数 | 怎么定 |
|---|---|
connect-timeout-ms |
建连;内网常见 1~3s,公网第三方可 3s |
read / call-timeout |
按第 7 章预算;先看下游 P95,再加一点余量,不要按偶发最大值设 |
max-requests |
全局同时进行的 HTTP 调用上限;≈ 舱壁 max 或略大 |
max-requests-per-host |
单下游主机并发;保护对方也保护自己,常见 8~32 |
bulkhead-max-size |
「最多允许多少请求同时等这个依赖」;用 目标出站QPS × 耗时 估算 |
bulkhead-queue-capacity |
等待许可的队列;宜小,满了 Abort |
bulkhead-await-timeout-ms |
略大于 call-timeout,避免许可等到了调用却已无意义 |
舱壁大小例算: 某第三方平均 0.5s,希望最多占用 50 QPS 出站 → 并发 ≈ 50×0.5=25 → bulkhead-max-size 从 24~32 起。
硬约束: bulkhead-max-size 应明显小于 tomcat.threads.max,否则舱壁形同虚设。
| 现象 | 动作 |
|---|---|
| 舱壁拒绝增多,Tomcat 仍健康 | 隔离在生效;业务受不了 → 加缓存/扩实例,或与对方扩容后再略增舱壁 |
| Tomcat 打满、舱壁却很空 | 还有出站没走统一客户端,或慢在 DB |
| 临时端口 / TIME_WAIT 飙升 | 加大连接复用;禁止每次 new HTTP 客户端 |
下游 P95 很长时:优先缓存 / 预聚合 / 异步,而不是无限加大超时。
8.4 舱壁 vs 熔断(易混)
| 舱壁 Bulkhead | 熔断 Circuit Breaker | |
|---|---|---|
| 目的 | 限制同时占用的资源 | 发现故障后短时快速失败 |
| 手段 | 线程池 / 信号量 | 错误率 / 慢调用比例 |
| 故障时 | 池满立即拒绝 | 打开后一段时间内直接拒绝 |
| 关系 | 不能互相替代,应叠加 | 同上 |
9. 限流、熔断、系统保护(Sentinel 等)
9.1 规则类型速查
| 类型 | 控制什么 | 典型用途 |
|---|---|---|
| 流控 flow | QPS / 并发线程数 | 入口削峰 |
| 网关流控 gw-flow | 按 route / api | Gateway 粗限流 |
| 熔断 degrade | 慢调用比例 / 异常比例 / 异常数 | 不稳定依赖 |
| 系统 system | LOAD / CPU / 入口 QPS / 线程 | 整机兜底 |
| 热点 param-flow | 带参数限流 | 按租户、部门、大屏 ID |
9.2 资源怎么划分
| 资源粒度 | 例子 | 用途 |
|---|---|---|
| 路由 / 服务 | service-a |
网关粗限流 |
| 整机 | system 规则 | 动态 API 很多时的兜底 |
| 业务 API | 单接口 QPS | 热点接口单独收紧 |
| 下游 host | tp:{host} |
按厂家隔离熔断 |
| 出站逻辑名 | outbound:vendor-x |
与舱壁配合 |
9.3 熔断参数阅读示例
json
{
"resource": "tp:依赖host",
"grade": 0,
"count": 0.5,
"timeWindow": 30,
"minRequestAmount": 20,
"statIntervalMs": 10000,
"slowRatioThreshold": 0.5
}
含义(以具体 Sentinel 版本文档为准):
统计窗口内请求数达标且慢调用占比过高 → 熔断一段时间 → 期间走降级,不再打下游。
9.3.1 限流 / 熔断------怎么设(起步法)
网关 / 接口 QPS 限流:
- 压测得出单实例稳态 QPS(错误率可接受时的最大值)。
- 限流阈值 ≈ 稳态 × 0.7~0.8(留余量);多实例若用「单机限流」则每实例各自按此设,集群总能力 ≈ 阈值 × 实例数。
- 热点大屏 API 单独更严;整机再用 system 规则兜底。
- 验收: 压测尖峰时限流必须能触发并返回明确码,而不是「从不触发」。
下游熔断:
- 先定义「慢」:例如超过 1s(或超过你的
callTimeout的 50%)算慢调用。 minRequestAmount:窗口内至少多少请求才统计(太小会误熔断),常见 10~20。- 慢调用 / 异常比例阈值从 0.5(50%)起,
timeWindow熔断时长 10~30s。 - 熔断打开时必须有降级返回(缓存/空数据/友好文案),否则只是换一种 500。
9.4 降级策略(产品可接受)
| 场景 | 可降级返回 |
|---|---|
| 统计数字 | 最近成功缓存 + 「非实时」标记 |
| 列表 | 空列表 + 「数据源繁忙」 |
| 地图 / 点位 | 上一帧 / 降采样 |
| 登录鉴权 | 不可降级,单独保护、阈值宽松 |
9.5 更多流量治理手段
- 按租户 / 用户多维限流
- 优先级:核心读 > 报表导出 > 后台任务
- 与第三方合同 QPS 对齐的本地令牌桶
- 过载保护(load shedding):负载高时主动丢低优先级
- 重试:仅幂等 + 抖动退避;写接口要幂等键
知识点: 限流保护的是自己 ;熔断保护的是依赖故障时的自己。
10. 缓存
10.1 何时缓存
| 数据类型 | 建议 TTL | 说明 |
|---|---|---|
| 统计 KPI / 排行 | 30s~5min | 轮询友好 |
| 字典 / 组织树 | 5~30min | 变更少 |
| 第三方原始结果 | 30s~2min | key 含业务维度 |
| 强实时 | 不缓存或 1~3s | 单独限流 |
10.2 单飞加载(防击穿)
text
getOrLoad(key, ttl):
v = redis.get(key)
if v != null: return v
with lock(key): // 同 key 只放行 1 个打源站
v = redis.get(key)
if v != null: return v
v = callSource() // 走出站舱壁
redis.setex(key, ttl, v)
return v
10.3 三大经典问题
| 问题 | 现象 | 手段 |
|---|---|---|
| 击穿 | 热点 key 过期瞬间打穿 | 单飞锁 / 逻辑过期 |
| 穿透 | 非法参数一直打库 | 空值短缓存 + 参数校验 + 布隆 |
| 雪崩 | 大量 key 同时过期 | TTL 加随机抖动 + 多级缓存 |
10.4 进阶
- 多级缓存:Caffeine(本地短 TTL)+ Redis
- 预热:发布 / 重启后灌热点
- 逻辑过期:先返回旧值,异步刷新
- 热点拆分:超热 key / 大 value 拆分,避免 Redis 单核热点
- 批量:mget / pipeline 减 RTT
- 一致性:Cache-Aside / 延迟双删等,按能否脏读选型
- Redis 挂了要能降级:有限等待,避免整个应用堵在 Redis
10.5 Redis 连接池------怎么设
yaml
spring:
data:
redis:
timeout: 2000ms
lettuce:
pool:
max-active: 64
max-idle: 16
min-idle: 8
max-wait: 1000ms
| 参数 | 怎么定 |
|---|---|
timeout |
单次命令超时;常见 1~3s。太长会让线程堵在 Redis |
max-active |
起点:与 Tomcat 繁忙线程同量级或略小(如 32~64),再压测 |
max-wait |
必须有限(如 1s)。池满时快速失败,便于降级 |
min-idle / max-idle |
保持少量热连接;idle 不要大于 active |
调参信号: 经常等连接超时 → 先查 Redis 慢命令/网络,再略增 max-active;Redis CPU 打满 → 减连接、治热点 key,而不是只加池。
知识点: 缓存是读多场景的第一优化手段;Redis 挂了要有降级路径。
11. 数据库与预计算
11.1 连接池------怎么设
yaml
spring:
datasource:
hikari:
maximum-pool-size: 30
minimum-idle: 10
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
公式:
text
pool_size ≈ CPU核数 × (1 + 平均等待时间 / 平均计算时间)
| 场景 | 起点举例(再压测) |
|---|---|
| 8 核、SQL 较快 | 10~20 |
| 8 核、SQL 经常等锁/IO | 20~40 |
| 多实例 | 所有实例池之和 < DB max_connections − 运维预留 |
例:DB 最大 500 连接,预留 100,3 实例 → 每实例理论上限约 133,实际常设 20~40(连接过多 DB 可能更慢)。
| 参数 | 怎么定 |
|---|---|
connection-timeout |
拿连接等待上限,常见 3s;满了应失败而非无限等 |
max-lifetime |
小于 DB 端空闲断开时间,避免半死连接 |
minimum-idle |
略小于 maximum,保持热连接 |
| 知识点 | 说明 |
|---|---|
| 池过大 | 数据库上下文切换与锁争用更严重,可能更慢 |
| 池耗尽 | 表现像线程卡死(堵在 getConnection) |
| 事务边界 | 事务内禁止调第三方 HTTP |
11.2 查询与模型
- 慢 SQL、缺索引必须先治
- 深分页改游标 / seek;
count(*)要缓存或避免 - 批量读写,消灭 N+1
- 语句超时、连接泄漏检测
- 读写分离:接受副本延迟,或关键读打主
- 统计类主路径走预聚合 / 物化视图 / ADS,禁止运行时全市扇出下游
11.3 异步削峰(写与通知)
适合:下单、审计、通知、批量导入、非即时报表。
手段:Kafka / RocketMQ / RabbitMQ 等;消费端同样要限流与重试幂等。
知识点: 能异步的别同步扛;能预计算的别实时算。
12. 业务 API 设计与读路径改造
| 手段 | 说明 |
|---|---|
| BFF / 聚合接口 | 一卡一聚合,减少前端扇出 |
| 服务端去重 | 相同参数短时共用结果 |
| 批量 API | 多 id 一次查 |
| 字段裁剪 | 禁止「一个接口塞全量树」 |
| 大结果异步化 | 导出 / 大包走任务,不占同步线程 |
| 明确 429/503 | 带 Retry-After,让客户端有序退避 |
| CQRS | 读写模型分离,读走优化视图 |
13. 前端 / 客户端(L0)
| 措施 | 说明 |
|---|---|
| 轮询间隔 | 统计类宜 ≥ 30s;强实时单独通道 |
| 可见性 | 页签不可见时暂停轮询 |
| 首屏分批 | 关键优先,次要延后 |
| 失败退避 | 429/503 指数退避 + 抖动 |
| 合并请求 | 能合则合 |
| 超时对齐 | 与网关 / 后端一致 |
| 降级 UI | 骨架屏 / 旧数据 / 「暂不可用」 |
| 长连接 | WebSocket / SSE 替代高频短轮询(按场景) |
知识点: 客户端齐刷重试 = 人为制造第二波高峰。
14. 多实例、网关与弹性
text
LB / Nginx / Gateway
│
┌────┴────┐
│ inst-1 │ inst-2 │ ...
└─────────┘
| 项 | 建议 |
|---|---|
| 最少实例 | 生产关键路径 ≥ 2 |
| 会话 | Token 无状态(JWT/Redis),勿粘单机内存会话 |
| 限流核算 | 集群限流,或「单机阈值 × 实例数」心算清楚 |
| 发布 | 滚动发布;观察超时率与线程池 |
| 容器 | CPU/内存 request·limit;HPA(CPU/QPS/自定义);PDB 防同时踢光 |
| 健康检查 | 区分存活与就绪;依赖全挂时不要假装 Ready |
15. 安全与鉴权对容量的影响
- Token / 会话校验走本地或 Redis,避免每请求打用户库
- 鉴权结果可短缓存
- WAF / 网关防刷与业务限流分层,避免误杀合法突发
- 加解密尽量本地完成,勿同步远程加解密成为新瓶颈
16. 可观测、压测与应急
16.1 黄金信号
| 信号 | 含义 |
|---|---|
| 流量 QPS | 是否突增 / 被限流 |
| 错误率 | 4xx/5xx、业务错误码 |
| 延迟分位 | P50 / P95 / P99 |
| 饱和度 | 线程、连接池、队列、CPU、FD |
16.2 必打日志 / 指标
| 信号 | 告警建议 |
|---|---|
| 出站超时(带 path) | 同 path 短时超 N 次 |
| 舱壁 active / queue | queue 持续接近容量 |
| 入口繁忙线程 | busy ≈ max 持续 |
| 网关 / Nginx 429、5xx | 突增 |
| 限流 / 熔断次数 | 突增 |
| Redis / DB 池等待 | 等待 > 0 持续 |
有条件加上:全链路 Trace,一次页面打开能看到最慢一跳。
16.3 应急步骤(通用)
- 是否线程 / 连接打满?(jstack、池指标)
- 卡在哪一跳?(入口排队 vs DB vs 出站)
- 止血:降前端频率、收紧限流、手动熔断、下线非关键卡片
- 恢复后观察超时率,再逐步放流量
- 复盘:缺缓存?扇出?超时过长?无舱壁?
16.4 压测清单
| 场景 | 目的 |
|---|---|
| 首屏多用户并发 | 入口与缓存 |
| 单热点 API 打满 | 限流是否生效 |
| 下游故意 delay | 舱壁 + 熔断,入口不被打满 |
| 杀一个实例 | failover 与限流余量 |
| Redis / 单依赖宕机 | 降级是否可接受 |
| 尖峰 + 齐刷重试 | 客户端退避是否有效 |
16.5 混沌与 SLO(进阶)
- 为关键链路定 SLO / 错误预算
- 定期注入:慢依赖、挂依赖、杀实例、网络丢包
- 验证「文档里的降级」在真实故障时是否真的触发
17. 完整改造清单(自学自检用)
把「还可以设置 / 配置 / 改造」的事项收成一张表,按层勾选:
A. 架构
- 读写分离 / CQRS
- 预聚合 / 物化视图
- MQ 异步削峰(写/通知)
- 分库分表或分片(真有量再上)
- 多活 / 单元化(高可用进阶)
B. 入口与网络
- OS:
nofile、somaxconn、端口范围 - Nginx:限连、限速、超时对齐、keepalive、上游耗时日志
- 静态 CDN / HTTP/2
- LB 健康检查与多实例
C. 应用运行时
- JVM / GC / 堆与堆外
- Tomcat(或其它容器)线程与 accept 队列有界
- 业务线程池命名、有界、拒绝策略正确
- 异步日志
- 序列化体积治理
D. 出站与韧性
- 统一 HTTP 客户端:超时 + 池 + 舱壁
- 按依赖拆隔离
- 网关 / 应用限流
- 熔断 + 产品降级
- 系统保护规则(整机兜底)
- 幂等与谨慎重试
E. 数据
- Redis 池与超时;多级缓存;击穿/穿透/雪崩治理
- DB 池、慢 SQL、事务边界
- 批量与分页改造
F. 客户端
- 轮询降频、合并、退避、超时对齐、降级 UI
G. 运维闭环
- 黄金指标 + 出站 path 日志
- 压测基线与故障演练
- 季度复核阈值
18. 分阶段落地路线图
| 阶段 | 内容 | 产出 |
|---|---|---|
| P0 止血 | 出站统一超时 + 连接池 + 舱壁;禁无超时客户端;超时日志带 URL | 慢依赖挂不死整站 |
| P1 入口 | Nginx 限流/超时对齐;网关粗限流;入口线程合理上限 | 削峰、明确 429 |
| P2 读优化 | 热点缓存 + 单飞;统计预聚合;接口合并 | QPS 与时延下降一个数量级 |
| P3 熔断降级 | 按 host/资源熔断;产品级降级 | 故障自动隔离 |
| P4 稳态 | 大盘、压测基线、应急演练、弹性扩缩 | 可运营 |
| P5 进阶 | MQ 削峰、多级缓存、读写分离、SLO/混沌 | 长期演进 |
19. 验收门禁(通用)
- 人为将某依赖 delay 很长:入口繁忙线程上升但被舱壁/队列上限兜住,无关接口仍可用。
- 超时日志带 path,可在短时间定位接口。
- 超限返回明确 429/503,而不是整站无响应。
- 读多场景缓存命中率在高峰可接受(如统计类 > 80%)。
- 双实例杀一台,核心功能仍可用(允许降级)。
- 压测报告中有:基线 QPS、P95、饱和度曲线、故障注入结果。
20. 知识点速查表
| 知识点 | 一句话 |
|---|---|
| 有界资源 | 线程、连接、队列、池都必须有上限 |
| 容量公式 | QPS ≈ 并发 / 耗时;先降耗时再加并发 |
| 端口 65535 | 只是端口号空间;入站共享监听口,≠ 全局并发上限 |
| 连接 ≠ 在飞 ≠ QPS | Keep-Alive 下空闲连接很多,真正干活的是在飞请求 |
| 放大效应 | 一页 × N 组件 × M 下游 |
| 线程模型 | 入口线程阻塞在 IO = 吞吐塌方 |
| JVM 堆 | 先做内存账单;Xms=Xmx;用 GC 日志判断大小,而不是猜 |
| 超时对齐 | 全链路超时预算一致,避免错位占坑 |
| 连接池 | HTTP/DB/Redis 皆有界 + 有限等待;多实例要算总和 |
| 舱壁 | 限制「同时依赖某下游」的并发,且应小于 Tomcat 线程 |
| 熔断 | 故障后短时快速失败,给系统喘息 |
| 限流 | 保护自己,不是保护别人;尖峰时应能触发 |
| 缓存 | 读多场景第一优化;注意击穿/穿透/雪崩 |
| 预聚合 | 重统计禁止运行时扇出 |
| 拒绝 Abort | 池满立刻失败,避免拖死调用方 |
| 幂等重试 | 只对安全的读重试;写要幂等键 |
| 可观测 | 没有 path 的超时日志 ≈ 没有运维能力 |
| 降级 | 高可用的最后一公里:产品可接受比「全挂」重要 |
21. 常见误区(对照自查)
概念级误导(65535 端口、连接≠QPS 等)见 第 2 章;这里是落地时的短清单。
| 误区 | 正确方向 |
|---|---|
| 线程不够就加大 Tomcat | 先超时、舱壁、减耗时;按 6.3 公式算 |
| 超时调很大「更稳定」 | 更大超时 = 更长占用 = 更容易雪崩 |
| 只看 CPU | 同时看线程、池、队列、FD、P99 |
堆抄 -Xmx4g 却不知为何 |
按 4.4 填内存账单,用 GC 日志校准 |
| 缓存了就万事大吉 | 还有击穿、穿透、雪崩、一致性 |
| 限流调到「从不触发」 | 限流要在压测尖峰时真的能保护 |
| 多实例但粘会话 | 无状态或外置会话 |
| 重试能救命 | 齐刷重试会二次打死恢复中的依赖 |
| 配置抄默认值 | 默认值不是你的容量模型 |
| DB 池按单实例开很大 | 先算「实例数 × 池」是否打爆 DB |