Skip to content

00 · 一张图串起全部:一次请求的完整旅程 ​

这是全库的总纲。如果只能记一篇,记这篇。 它把四个模块(OS / 网络 / 数据结构 / 数据库)挂在同一条时间线上。

怎么用这篇 ​

  1. 先通读一遍,不求记住。
  2. 学完四个模块后回来,合上资料,自己画一遍。
  3. 画不出来的地方,就是还没真懂的模块。

总图:从敲下回车到页面出现 ​

[0] 你按下回车
     |
     v
+-- [1] 应用层(浏览器)
|     · 解析 URL:协议、域名、端口、路径
|     · 查缓存:强缓存命中就直接用,不发请求
|     · 查 DNS 缓存(浏览器 -> 系统 -> hosts)
|
+-- [2] DNS 解析                                    【网络 05】
|     本地 DNS -> 根 -> 顶级域 -> 权威(迭代查询)
|     拿到 IP:93.184.216.34
|     走 UDP 53,因为报文小、要快
|
+-- [3] TCP 三次握手                                【网络 02】
|     SYN -> SYN+ACK -> ACK
|     内核协议栈完成,不经过应用
|     要分配 socket、进半连接队列 / 全连接队列
|     进程此时是"阻塞"或"异步等待"            【OS 01/04】
|
+-- [4] TLS 握手(HTTPS)                           【网络 04】
|     ClientHello -> ServerHello + 证书 -> 密钥交换
|     非对称协商密钥,对称加密传数据
|     证书链验证可能又是一次网络请求
|
+-- [5] 发 HTTP 请求                                【网络 04】
|     GET /index.html HTTP/1.1 + Host + Cookie
|
+-- [6] 封装:应用 -> TCP -> IP -> 链路             【网络 01】
|     TCP 头:源端口、目的端口 443、序号、窗口
|     IP 头:源 IP、目的 IP、TTL=64
|     MAC 头:源 MAC、下一跳(网关)的 MAC
|     查路由表决定下一跳,查 ARP 拿网关 MAC      【网络 05】
|
+-- [7] 内核发送数据                                【OS 04】
|     应用调 write() -> 系统调用(用户态转内核态)【OS 05】
|     数据从用户缓冲区拷到 socket 发送缓冲区
|     网卡 DMA 取走,发到链路上
|     大文件可能用零拷贝(sendfile)绕过用户态
|
+-- [8] 路由转发                                    【网络 01】
|     每跳:拆 MAC 头 -> TTL 减 1 -> 查路由表 -> 重写 MAC 头
|     IP 头不变,MAC 每跳都变
|     丢包了 TCP 重传(超时 / 3 个重复 ACK)     【网络 03】
|     拥塞控制决定发多快(cwnd)
|
+-- [9] 服务端收包                                  【OS 04】
|     网卡中断 -> 内核协议栈逐层解封装
|     数据进 socket 接收缓冲区
|     epoll 检测到可读 -> 唤醒工作线程
|     应用 read() -> 系统调用 -> 拷到用户缓冲区
|
+-- [10] 查数据库                                   【数据库】
|     a. 连接池取连接(可能又要一次 TCP 握手)
|     b. 解析 SQL -> 优化器选索引 -> 执行计划  【数据库 05】
|     c. 走 B+ 树找数据                        【数据库 01】
|        Buffer Pool 命中 -> 内存里直接返回     【OS 02/04】
|        未命中 -> 从磁盘读 16KB 页(一次 IO)
|     d. 读操作:用 MVCC 判断版本可见性        【数据库 03】
|     e. 写操作:加锁 -> 改 Buffer Pool -> 写 undo
|               -> 写 redo -> 写 binlog -> 两阶段提交 【数据库 02】
|        没走索引 -> 锁全表 -> 别人全等着       【数据库 04】
|
+-- [11] 返回响应                                   【网络 04】
|     200 OK + Cache-Control + ETag + HTML
|     原路返回(7~9 反过来走一遍)
|
+-- [12] 浏览器渲染
      HTML -> DOM -> CSSOM -> 渲染树 -> 布局 -> 绘制
      发现 CSS/JS/图片 -> 再发请求(复用连接)

四条横向对照:同一个概念在四个模块里的样子 ​

这张表是全库最值钱的部分。面试时能把一个概念跨模块对照,就是"真懂"的信号。

抽象概念操作系统计算机网络数据结构数据库
分层/封装用户态 / 内核态TCP/IP 四层—存储引擎 / Server 层
缓存CPU 缓存、TLB、页缓存DNS 缓存、ARP 缓存、HTTP 缓存—Buffer Pool、查询缓存
淘汰策略页面置换(Clock)DNS TTL、ARP 超时LRU 缓存Buffer Pool 的改良 LRU
队列就绪队列、阻塞队列半连接队列、全连接队列BFS、优先队列锁等待队列
锁互斥锁、自旋锁、读写锁——行锁、表锁、间隙锁
死锁四条件 + 银行家算法——等待图检测 + 回滚
多版本/乐观COW(写时复制)—持久化数据结构MVCC
索引页表(虚拟>物理)路由表(目的>下一跳)B+ 树、哈希表B+ 树索引
分片/局部性分页、NUMA子网、CDN 就近数组 vs 链表(缓存行)分库分表、分区表
日志/重放journal(文件系统)——redo log、binlog
地址翻译虚拟地址 > 物理地址域名 > IP > MAC—逻辑行 > 页 > 磁盘偏移

怎么用这张表:随便挑一行,试着讲清"这四列分别是什么、为什么这么像"。讲得通,说明你的知识是网状的。

三个"横切问题":一个现象牵出四个模块 ​

问题 1:我的程序为什么卡住了? ​

程序卡住
+- 在等 CPU?        > 就绪队列太长,负载过高            【OS 调度】
+- 在等锁?          > 互斥锁/自旋锁,或者数据库行锁      【OS 并发 / 数据库锁】
+- 在等 IO?         > 阻塞 IO,对端没回,磁盘慢          【OS IO / 网络】
+- 在等数据库?      > 慢 SQL、锁等待、连接池打满         【数据库】
+- 在等 GC?         > 内存不够,频繁回收                 【语言运行时】
+- 在等网络?        > RTT 大、丢包重传、拥塞窗口太小      【网络】

排查顺序:先看 CPU 使用率(高 > 计算密集/自旋;低 > 在等)> 看线程栈(在哪一行阻塞)> 看数据库慢查询 > 看网络 RTT。

问题 2:为什么我的接口 P99 是 2 秒,但平均只有 50ms? ​

P99 远高于平均值 = 有"长尾"
长尾的常见来源:
+- 缓存击穿/雪崩         > 少量请求打到数据库          【数据库 + 缓存】
+- 锁等待               > 少数请求排队                  【OS / 数据库】
+- 慢 SQL               > 个别查询没走索引              【数据库】
+- GC 停顿              > 周期性 STW                   【语言运行时】
+- TCP 重传             > 丢包导致等待 RTO(几百 ms)   【网络】
+- 连接池耗尽           > 请求排队等连接                【数据库 + 网络】

关键:平均值会掩盖长尾。优化 P99 往往比优化平均值更有价值。

问题 3:系统能扛多少并发? ​

并发能力 = 最窄的那个瓶颈

① CPU 够吗?        > 计算密集型的上限 = 核数          【OS】
② 内存够吗?        > Buffer Pool / 缓存 / 线程栈        【OS】
③ 连接数够吗?      > 文件描述符、端口数、连接池大小      【OS / 网络】
④ 数据库扛得住吗?  > QPS、锁竞争、连接数                【数据库】
⑤ 带宽够吗?        > 出口带宽 / 每请求大小              【网络】
⑥ 有没有串行点?    > 全局锁、单点写入、自增 ID           【全模块】

"能扛多少并发"这个问题,答案永远是"最窄的那一环"。找到它,比盲目扩容有用得多。

一张极简版总图(贴在墙上那种) ​

用户
 |
 v
浏览器 -------- DNS 查 IP -------------------- 【网络 05】
 |
 |  TCP 握手 / TLS 握手 --------------------- 【网络 02/04】
 v
内核协议栈 ---- 封装 / 解封装 ---------------- 【网络 01】
 |
 |  socket 缓冲区 + epoll ------------------- 【OS 04】
 v
应用进程 ------ 线程调度 / 锁 ----------------- 【OS 01/03】
 |
 |  SQL
 v
数据库 -------- 优化器 -> B+ 树 -> Buffer Pool - 【数据库 01/05】
 |             事务 / MVCC / 锁 --------------- 【数据库 02/03/04】
 |  缺页 / 刷盘
 v
磁盘 ---------- 虚拟内存 / 页缓存 ------------- 【OS 02】

和其他知识的连接 ​

  • 这篇是 00-导读 各模块地图的"总装"
  • 所有细节都在 01-操作系统/、02-计算机网络/、03-数据结构与算法/、04-数据库原理/ 里

自测 3 问 ​

Q1:不看资料,把"输入 URL 到页面出现"讲一遍,能讲到几层?

点开看答案

能讲到 8 层以上算过关,关键节点: DNS > TCP 握手 > TLS 握手 > 发 HTTP > 封装 > 路由转发 > 服务端收包 > 查数据库 > 返回 > 渲染。

加分点:

  • 每一步能说出"属于哪个模块"
  • 能说出"这一步如果失败,会看到什么现象"(比如 DNS 失败 > 域名解析错误;TCP 失败 > 连接超时)

减分点:

  • 只会说"发请求、收响应",中间全是黑的
  • 说不清封装/解封装

Q2:为什么"能扛多少并发"的答案永远是"最窄的那一环"?

点开看答案

因为系统是串行依赖的:请求必须依次经过 CPU、内存、网络、数据库。

木桶效应:整个系统的吞吐量 = 所有环节里最小的那个。

举例:

  • 应用层能处理 10 万 QPS,但数据库只能 5000 QPS > 系统上限 5000。
  • 数据库能 5 万 QPS,但出口带宽只够 1 万请求 > 上限 1 万。
  • 都够,但有个全局锁 > 上限就是锁的吞吐。

所以扩容不能盲目:加机器但数据库是瓶颈,只会让数据库更早崩。先找到瓶颈,再针对性优化。

Q3:为什么说"P99 比平均值更值得优化"?

点开看答案

因为用户感知的是最慢的那部分。

假设 100 个请求:99 个 50ms,1 个 2 秒。

  • 平均值 = 69.5ms,看起来很好。
  • 但那个 2 秒的请求,用户会明显感觉"卡了"。

而且长尾往往有系统性原因(锁等待、GC、丢包重传、缓存击穿),不是随机的。不解决,它会周期性出现,且随流量增长而恶化。

工程实践:监控要看 P95 / P99 / P999,而不是只看平均值。平均值会掩盖问题。

类比失效处 / 易错点 ​

  • "一条流水线"类比会让人以为请求是严格串行的。实际上服务端有连接池、线程池,多个请求是并发处理的,图里画的是"单个请求的路径"。
  • 不要以为"图里的顺序"就是"真实的时序"。比如 TLS 握手和 DNS 查询可能并行(HTTP/3 甚至合并),浏览器会预连接、预解析。
  • "四层对照表"不是严格对应。比如"锁"在网络层就没有对应物——不要为了凑对称硬编。
  • 这张图是"典型路径",不是"所有路径"。有 CDN 时请求可能根本不打到源站;有缓存时可能不查数据库。
  • 能背图 ≠ 懂原理。图是索引,真正的理解在每个模块的"为什么这么设计"里。

✅ 验收清单 ​

  • 能合上资料,自己画出这张总图(至少 8 个节点,标出所属模块)
  • 能口述"输入 URL 到页面出现",每个环节说出"属于哪个模块、关键机制是什么"
  • 能挑"四层对照表"里任意 3 行,讲清四列的对应关系
  • 能用"程序卡住了"这个问题,牵出至少 4 个模块的原因
  • 能解释"为什么 P99 比平均值更值得关注"
  • 能解释"系统并发上限 = 最窄的一环",并举一个具体例子
  • 能画出极简版总图(浏览器 > 协议栈 > 进程 > 数据库 > 磁盘)
  • 合上书,用自己的话讲 5 分钟,中途不卡壳