大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
性能与扩展基础
这一课你将能做到
- 区分延迟、吞吐、并发三个指标,并知道各自用什么数字观测(P99 / QPS / 并发数)
- 用 Little's Law 对本站任意接口算出「目标 QPS 下需要多少线程/连接」
- 针对具体瓶颈,按「代码 → 缓存 → 读写分离 → 分片」的成本顺序给出扩展方案
为什么要学
用户说「系统慢」,运维说「系统撑不住」——这是两个完全不同的问题,对应性能(Performance) 和可扩展性(Scalability)。搞混它们,优化就会做错方向:该加缓存的时候加机器,该异步的时候死磕 SQL。
面试里这是开场送分题也是送命题:「延迟和吞吐有什么区别?」「系统从 1000 QPS 扩到 10000 QPS 你怎么做?」答不出层次感,后面的深挖基本没机会。
人话:性能是「单个请求跑多快」(你点外卖 30 分钟到手),扩展性是「订单翻 10 倍还跑不跑得动」(一万个骑手同时抢单系统崩不崩)。优化性能是消除浪费,提升扩展性是消除单点。
先建立图像
三个指标各自量什么:
| 指标 | 含义 | 观测方式 | 本站例子 |
|---|---|---|---|
| 延迟 Latency | 单次请求耗时 | P50 / P99 / P999 | 题目详情页打开 180ms |
| 吞吐 Throughput | 单位时间处理量 | QPS / TPS | 列表页扛 800 QPS |
| 并发 Concurrency | 同一时刻在处理数 | 活跃线程/连接数 | 判题同时跑 300 个沙箱 |
三者由 Little's Law 锁死:并发数 = QPS × 平均耗时。判题 30 QPS × 10 秒 = 300 并发槽,这条公式在本站被验证了两次:一次是判题池容量,一次是 Tomcat 线程数。
可用性也有「月亮数字」:
99.9% → 全年故障 8.76 小时 (三个 9,普通业务底线)
99.99% → 全年故障 52.6 分钟 (四个 9,核心交易)
99.999% → 全年故障 5.26 分钟 (五个 9,成本翻几倍,慎承诺)
再背一张延迟基准表(估算全靠它):
内存随机读 ~100ns
Redis 一次读写 ~0.5ms(机房内)
MySQL 主键查询 ~1-5ms(命中 buffer pool)
SSD 随机读 ~100μs-1ms
机房内网络 RTT ~0.5ms
跨地域 RTT ~30ms+
扩展只有两条路:
垂直扩展 Scale Up:8C16G → 16C32G → 32C64G
优点:不改代码;缺点:有物理上限、价格非线性(规格翻倍价格×3)
水平扩展 Scale Out:1 个 Spring Boot 实例 → N 个实例 + Nginx
优点:理论上无限加;缺点:要求无状态,引入 LB/分布式复杂性
跟着做
步骤 1:压测本站接口,拿到真实基线
用 wrk 压题目列表接口(JMeter/ Gatling 同理):
# 4 线程、保持 100 个连接、压 30 秒
wrk -t4 -c100 -d30s http://localhost:8080/api/problems?page=1&size=20
输出里重点看三行:Requests/sec(吞吐)、Latency Distribution 的 99%(P99)、Socket errors(有没有非 2xx)。假设结果:QPS 900、P99 220ms——这就是基线,写进监控看板。
步骤 2:用 Little's Law 做容量规划
目标:列表接口要扛 5000 QPS、P99 保持在 200ms 以内。
需要的并发处理能力 = 5000 × 0.2s = 1000 个并发请求
Tomcat 默认 max-threads = 200 → 远远不够
两条路对比:把 P99 从 200ms 降到 40ms(缓存),并发需求降为 200——降延迟比加线程划算 5 倍。这题的标准答案就是引出下一课的缓存。
Spring Boot 侧对应配置:
server:
tomcat:
max-threads: 400 # 并发上限 = 线程数 × (1/单请求耗时)
accept-count: 100 # 等待队列,满了就拒绝
spring:
datasource:
hikari:
maximum-pool-size: 50 # 连接池通常是比线程池先触顶的那个
步骤 3:垂直 vs 水平的成本推演
假设单实例压测 900 QPS,目标 10000 QPS:
方案 A 垂直:8C16G → 32C64G,QPS 约 900 → 3200(不到线性),月成本 ×4
方案 B 水平:8 实例 + Nginx,QPS 约 900 × 8 = 7200,月成本 ×8 但可继续加
关键判断点:实例是无状态的才能水平加。检查清单——会话在 Redis 而不是内存?文件在 OSS 而不是本地磁盘?定时任务没有多实例重复跑?本站三条都满足,所以走水平。
步骤 4:定位第一个瓶颈
流量上涨时的经典触顶顺序,用监控逐项核对:
1. 数据库:连接数打满、慢查询堆积 ← 90% 的项目死在这
2. 应用:GC 频繁、线程池队列积压
3. 带宽/静态资源:大 JSON、图片挤占出口
判题平台的特殊性:判题机是 CPU 密集型,API 是 IO 密集型,两者混部会互相抢核——部署层面先拆开,这本身就是最便宜的一次「扩展」。
讲清楚要点
- 看 P99 不看平均值:平均值被大量快请求稀释,长尾(慢查询、GC 停顿、锁竞争)全藏在 P99/P999 里。用户体验由长尾决定。
- 吞吐受最慢环节限制(木桶效应):应用能扛 1 万 QPS 但 MySQL 只能接 2000,系统吞吐就是 2000。优化要打在最短板上。
- 垂直救急、水平长跑:早期升配最快,但规格有上限且价格非线性;水平扩展是终局,前提是无状态化。
- 扩展顺序从便宜到贵:优化 SQL/索引(改代码)→ 加缓存(加组件)→ 读写分离(改架构)→ 分片(伤筋动骨)。跳级做后面的,等于放着便宜药不吃直接手术。
- 容量规划留余量:压测出的极限 ×70% 才是安全容量,留 30% 应对流量突增和实例故障时的分摊。
你可能会懵的地方
-
QPS 很高但 P99 也很高,正常吗? 这是系统逼近容量上限的典型信号。延迟随利用率非线性上升(利用率 80% 后急剧抬头),此时加机器反而便宜,继续压榨才贵。
-
「无状态」到底指什么? 任意请求可以被任意实例处理:会话放 Redis、文件放对象存储、本地不缓存关键数据。唯一豁免的是可丢的本地热身缓存。
-
为什么数据库总是第一个瓶颈? 它有状态(难水平扩)、走磁盘 IO(慢)、要事务(串行化)。应用层随便加实例,数据层每次都要想很久。
-
单体 + 多实例不够时,是不是就该上微服务? 不。先问瓶颈是不是「组织问题」(多团队互相阻塞),纯流量问题用缓存、读写分离、异步化解决,拆服务是最后选项,本 track 第 8 课细讲。
-
压测数字能直接当容量承诺吗? 不能。压测环境与生产有差异(数据量、网络、邻租),且要留故障余量:线上单实例承载按压测值 70% 规划。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 用平均值汇报性能 | 长尾被掩盖,用户体感差 | 汇报 P99/P999 |
| 一慢就加机器 | 索引缺失、N+1 被掩盖 | 先看慢查询日志和监控再动手 |
| 会话/验证码存本地内存 | 无法水平扩展 | 放 Redis |
| 判题机和 API 部署同一批机器 | CPU 抢占,互相拖垮 | 独立部署单元、独立扩容 |
| 外部调用不设超时 | 一个慢依赖拖死整个线程池 | 一切 RPC/HTTP/DB 都配超时 |
卡点急救
- 压测 QPS 上不去但 CPU 利用率很低:瓶颈在等锁或等资源——依次查 Tomcat 线程池(
max-threads)、HikariCP(maximum-pool-size)、下游接口超时设置;jstack看线程大量 WAITING 就是证据。 - P99 有周期性尖刺:十有八九是定时任务(缓存集中失效、日志切分、Full GC)叠加流量高峰,把定时任务错峰 + 缓存 TTL 加随机抖动。
- 加了机器 QPS 不涨:存在共享瓶颈——MySQL、Redis、带宽或某个有状态组件。画出整条链路标注每环压测容量,最低的那一环就是天花板。
面试常问
-
延迟和吞吐哪个更重要? 场景决定:在线服务(判题状态查询)优先 P99 延迟,离线任务(排行榜重算、日志清洗)优先吞吐。关键是答出「先确认优化目标再动手,两者可能互相牺牲」——比如加大批量合并降低单次延迟换取吞吐。
-
系统从 1000 QPS 扩到 10000 QPS 的路径? 分层作答:应用无状态化 + 水平扩实例(覆盖 3~4 倍)→ 热点数据加缓存(覆盖大头读流量)→ 数据库读写分离(读再翻倍)→ 仍不够再考虑分片/异步化。每步说清代价。
-
99.99% 可用性意味着什么,怎么做到? 全年 52.6 分钟。靠冗余消除单点(多实例、主从、多 AZ)+ 快速故障转移(健康检查、自动摘除)+ 演练验证。不是靠「代码写好点」。
自测三问
- P99 是什么?为什么不用平均值衡量用户体感?
- 并发 200 个请求、平均耗时 100ms,理论吞吐上限是多少?Tomcat 线程数设多少合适?
- 水平扩展的前置条件是什么?逐条列出检查项。
小结一句话
先分清要优化延迟还是吞吐,再按「代码 → 缓存 → 读写分离 → 分片」的顺序扩展——数据库几乎总是第一个诚实的瓶颈。
延伸
- 《Designing Data-Intensive Applications》第 1 章:负载描述与扩展性的经典论述
- wrk 压测工具:本课动手环节使用的轻量压测器
- Google SRE Book — Service Level Objectives:SLI/SLO 怎么定
- 下一课:一致性与 CAP——从「刷新一下才看到提交结果」聊到分布式系统最著名的取舍定理。