CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
系统设计入门/课时/性能与扩展基础
RawBlame
课时目录12 节

系统设计入门

  1. 01系统设计是什么
  2. 02性能与扩展基础
  3. 03一致性与 CAP
  4. 04负载均衡
  5. 05缓存模式
  6. 06异步与消息
  7. 07数据架构
  8. 08微服务与网关
  9. 09云设计模式
  10. 10可靠性模式
  11. 11性能反模式
  12. 12综合练习

性能与扩展基础

latency vs throughput、垂直/水平扩展。

35 分钟

性能与扩展基础

这一课你将能做到

  1. 区分延迟、吞吐、并发三个指标,并知道各自用什么数字观测(P99 / QPS / 并发数)
  2. 用 Little's Law 对本站任意接口算出「目标 QPS 下需要多少线程/连接」
  3. 针对具体瓶颈,按「代码 → 缓存 → 读写分离 → 分片」的成本顺序给出扩展方案

为什么要学

用户说「系统慢」,运维说「系统撑不住」——这是两个完全不同的问题,对应性能(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 密集型,两者混部会互相抢核——部署层面先拆开,这本身就是最便宜的一次「扩展」。

讲清楚要点

  1. 看 P99 不看平均值:平均值被大量快请求稀释,长尾(慢查询、GC 停顿、锁竞争)全藏在 P99/P999 里。用户体验由长尾决定。
  2. 吞吐受最慢环节限制(木桶效应):应用能扛 1 万 QPS 但 MySQL 只能接 2000,系统吞吐就是 2000。优化要打在最短板上。
  3. 垂直救急、水平长跑:早期升配最快,但规格有上限且价格非线性;水平扩展是终局,前提是无状态化。
  4. 扩展顺序从便宜到贵:优化 SQL/索引(改代码)→ 加缓存(加组件)→ 读写分离(改架构)→ 分片(伤筋动骨)。跳级做后面的,等于放着便宜药不吃直接手术。
  5. 容量规划留余量:压测出的极限 ×70% 才是安全容量,留 30% 应对流量突增和实例故障时的分摊。

你可能会懵的地方

  • QPS 很高但 P99 也很高,正常吗? 这是系统逼近容量上限的典型信号。延迟随利用率非线性上升(利用率 80% 后急剧抬头),此时加机器反而便宜,继续压榨才贵。

  • 「无状态」到底指什么? 任意请求可以被任意实例处理:会话放 Redis、文件放对象存储、本地不缓存关键数据。唯一豁免的是可丢的本地热身缓存。

  • 为什么数据库总是第一个瓶颈? 它有状态(难水平扩)、走磁盘 IO(慢)、要事务(串行化)。应用层随便加实例,数据层每次都要想很久。

  • 单体 + 多实例不够时,是不是就该上微服务? 不。先问瓶颈是不是「组织问题」(多团队互相阻塞),纯流量问题用缓存、读写分离、异步化解决,拆服务是最后选项,本 track 第 8 课细讲。

  • 压测数字能直接当容量承诺吗? 不能。压测环境与生产有差异(数据量、网络、邻租),且要留故障余量:线上单实例承载按压测值 70% 规划。

错法对照

错法问题正解
用平均值汇报性能长尾被掩盖,用户体感差汇报 P99/P999
一慢就加机器索引缺失、N+1 被掩盖先看慢查询日志和监控再动手
会话/验证码存本地内存无法水平扩展放 Redis
判题机和 API 部署同一批机器CPU 抢占,互相拖垮独立部署单元、独立扩容
外部调用不设超时一个慢依赖拖死整个线程池一切 RPC/HTTP/DB 都配超时

卡点急救

  1. 压测 QPS 上不去但 CPU 利用率很低:瓶颈在等锁或等资源——依次查 Tomcat 线程池(max-threads)、HikariCP(maximum-pool-size)、下游接口超时设置;jstack 看线程大量 WAITING 就是证据。
  2. P99 有周期性尖刺:十有八九是定时任务(缓存集中失效、日志切分、Full GC)叠加流量高峰,把定时任务错峰 + 缓存 TTL 加随机抖动。
  3. 加了机器 QPS 不涨:存在共享瓶颈——MySQL、Redis、带宽或某个有状态组件。画出整条链路标注每环压测容量,最低的那一环就是天花板。

面试常问

  • 延迟和吞吐哪个更重要? 场景决定:在线服务(判题状态查询)优先 P99 延迟,离线任务(排行榜重算、日志清洗)优先吞吐。关键是答出「先确认优化目标再动手,两者可能互相牺牲」——比如加大批量合并降低单次延迟换取吞吐。

  • 系统从 1000 QPS 扩到 10000 QPS 的路径? 分层作答:应用无状态化 + 水平扩实例(覆盖 3~4 倍)→ 热点数据加缓存(覆盖大头读流量)→ 数据库读写分离(读再翻倍)→ 仍不够再考虑分片/异步化。每步说清代价。

  • 99.99% 可用性意味着什么,怎么做到? 全年 52.6 分钟。靠冗余消除单点(多实例、主从、多 AZ)+ 快速故障转移(健康检查、自动摘除)+ 演练验证。不是靠「代码写好点」。

自测三问

  1. P99 是什么?为什么不用平均值衡量用户体感?
  2. 并发 200 个请求、平均耗时 100ms,理论吞吐上限是多少?Tomcat 线程数设多少合适?
  3. 水平扩展的前置条件是什么?逐条列出检查项。

小结一句话

先分清要优化延迟还是吞吐,再按「代码 → 缓存 → 读写分离 → 分片」的顺序扩展——数据库几乎总是第一个诚实的瓶颈。

延伸

  • 《Designing Data-Intensive Applications》第 1 章:负载描述与扩展性的经典论述
  • wrk 压测工具:本课动手环节使用的轻量压测器
  • Google SRE Book — Service Level Objectives:SLI/SLO 怎么定
  • 下一课:一致性与 CAP——从「刷新一下才看到提交结果」聊到分布式系统最著名的取舍定理。
加载本节练习…
← 上一课系统设计是什么下一课 →一致性与 CAP