跟着一个 API 请求走完全程:后端全链路解剖

写在前面

Nginx、Kestrel、Redis、数据库——单看每一层,这个博客都有专文讲过。但它们怎么串成一次真实的请求?这篇把所有层穿起来,跟着一个 GET /api/orders/123 走完全程。

每一跳只讲三件事:原理(链到已有专文,不重复展开)、大概多慢(链到《每个程序员都该知道的延迟数字》)、怎么优化。这是博客的"集大成"篇——读完你会看到这些独立的文章其实是一条线上的事。


场景

用户在 App 里点开一个订单详情,前端发:

1
2
GET https://api.example.com/orders/123
Authorization: Bearer <JWT>

后端要做:验 JWT → 查 Redis 订单缓存 → 没命中就查 DB → 订单里要显示用户名,可能调下游用户服务 → 返回 JSON。

一次请求,九跳:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
浏览器 / App
   │  GET /orders/123 (JWT)
[DNS]  api.example.com → IP                              第 0 跳
[TCP + TLS 握手]   冷启动 2-3 RTT / 复用 0                第 1 跳
[四层 LB (LVS / 云 SLB)] ───▶ [Nginx 反代]               第 2-3 跳
                                 │ TLS 终止 / 路由 / 限流
                            [Kestrel]   epoll / IOCP     第 4 跳
                                 │ 中间件管线             第 5 跳
                            [Controller]
                              │        │
                    Redis ◀───┘        └──▶ 数据库       第 6-7 跳
                   (缓存命中)              (索引 / 磁盘 IO)
                              ▼  (可选)
                       [下游用户服务]                     第 8 跳
                       HttpClient + 可能穿 sidecar
                  JSON 返回 ▶ gzip ▶ TLS ▶ 客户端         第 9 跳

第 0 跳:DNS 解析

api.example.com 要先变成 IP。浏览器 / OS 本地缓存命中就 <1ms;没命中就走递归 DNS 一层层问上去,10–100ms。移动端弱网和运营商劫持会放大这一跳。

优化:DNS 预解析、合理 TTL、HTTPDNS(绕过运营商)。冷启动才痛,热请求基本免费。


第 1 跳:TCP + TLS 握手

TCP 三次握手 1 RTT;TLS 1.3 握手 1 RTT(1.2 是 2 RTT)。所以冷启动一个 HTTPS 请求要付 2–3 RTT,同机房就是 1–2ms,跨城就是几十 ms。

这一跳最重要的结论:连接复用是第一优化。keep-alive / 连接池把 2 个握手 RTT 省成 0,之后每次只剩 1 RTT。TLS 1.3 + session resumption + 0-RTT 进一步压。

这正是《HttpClient 的前世今生》那篇的核心——new HttpClient() 每次新建连接,把本该复用的 TCP 连接主动掐断、端口踢进 TIME_WAIT,是 .NET 史上最著名的反模式之一。


第 2 跳:入口(CDN / 四层负载均衡)

静态资源走 CDN 就近返回(不进后端);动态 API 进四层负载均衡(LVS / 云 SLB / NLB),按 IP + 端口把包转发到某一台 Nginx。LB 本身 μs–ms 级,几乎不花钱。

优化:CDN 边缘缓存把静态挡在外面;LB 健康检查要及时摘掉挂掉的 Nginx,别把流量往死节点送。


第 3 跳:Nginx 反向代理

到七层入口 Nginx 了,它做四件事:TLS 终止(在这里解密,上游走明文)、路由(按 path / host 转发到对应上游)、限流limit_req)、可选的鉴权前置。然后转发给 Kestrel。

详见《Nginx 学习笔记(二):反向代理与负载均衡》《(三):HTTPS、性能优化与实战》。Nginx 自身处理是 ms 级。

优化:最容易忘的一条是 upstream keepalive——Nginx 到 Kestrel 也要复用连接,别每个请求都新建一次 TCP(又掉进第 1 跳的坑)。其次是 HTTP/2、gzip 压缩响应。


第 4 跳:Kestrel 接收(I/O 模型 + Pipelines + 线程池)

包到内核了。Kestrel 用异步 I/O(Linux 上 epoll,Windows 上 IOCP)拿到数据,用 Pipelines(池化的零拷贝缓冲区)解析 HTTP 帧,再把请求丢给 ASP.NET Core 管线。全程不阻塞线程。

这一跳的灵魂是为什么不能阻塞线程——I/O 是 ms 级、线程是宝贵的纳秒级资源,让线程等 I/O 是用"贵的东西等便宜的东西"。完整的 I/O 模型谱系(阻塞/非阻塞/IO多路复用/异步)见《操作系统学习笔记(一):I/O 模型全解》,.NET 视角的落地见《.NET 高并发编程全景》。

延迟:内核↔用户态拷贝 μs 级,几乎可以忽略。


第 5 跳:ASP.NET Core 中间件管线

请求流过中间件链:路由匹配 → 鉴权(验 JWT 签名和过期)→ 限流 → 日志 / 追踪埋点 → 进 Controller。这一段是纯 CPU,μs–ms 级。

详见《ASP.NET Core 学习笔记(一):基础架构与启动流程》(中间件管线的本质)。

优化:中间件顺序有讲究(能短路的尽量靠前,比如鉴权失败直接 401,别白跑后面的活);热路径上别 new 重对象、别做重活;依赖注入按需,别把瞬态服务当万金油。


第 6 跳:Redis 缓存层

Controller 先查 Redis(order:123)。命中 → 直接反序列化返回,跳过第 7 跳

详见《Redis 学习笔记(二):缓存实战》。同机房 GET 大约 ~0.1–0.3ms

优化:缓存 key 设计、过期 + 预刷新、防穿透(空值也缓存一层)、防击穿(热 key 互斥重建)、防雪崩(过期时间加随机抖动)。

这一跳命中省下的是第 7 跳的 ~ms–10ms,是全链路杠杆最大的一处。缓存命中率从 90% 提到 99%,DB 压力差 10 倍。


第 7 跳:数据库(缓存未命中)

Redis 没命中 → 查 DB。SELECT ... WHERE id = 123 走一遍:解析 → 优化器选执行计划 → 走索引(B+ 树)→ 命中 buffer pool 直接返回;没命中触发磁盘 IO。

  • buffer pool 命中:~0.5–2ms
  • 触发磁盘 IO:再 +1ms(SSD)/ +10ms(HDD)。

索引原理见《数据库系列(三):索引原理》,慢查询定位和 SQL 调优见《数据库性能优化实战》,《数据库系列(四):事务与 ACID》里给过 fsync 的实测对照(HDD ~10ms / SSD ~1ms / NVMe ~0.1ms)。

优化索引建对是 DB 性能的头号杠杆(一个该建没建的索引能让查询从 1ms 变 100ms);其次是连接池、慢查询监控、把随机写转顺序写(WAL)。绝大多数"接口慢"的根因在这一跳,不在代码里。


第 8 跳:下游服务调用(微服务场景)

订单要显示用户名,本地没有,得调用户服务:应用层 HttpClient 发请求 → 可能穿过 sidecar(Envoy / ztunnel)→ 网络 → 对端 sidecar → 对端应用。

延迟 = 1 RTT + 两端序列化 + 对端处理 + sidecar 税。其中 sidecar 每 hop 多 2–5ms(Istio 官方 ~3ms p50 / ~10ms p99),详见《服务网格:从 sidecar 到 Ambient,那笔"分布式税"怎么省》。这也是《后端架构实战(四):微服务的"分布式税"》讲的"分布式税"的本金。

优化

  • 并行调用——要调多个下游,用 Task.WhenAll 并发,别串行 await 三次(串行 vs 并行差的是整倍 RTT,见《.NET 高并发编程全景》);
  • 连接池(同第 1 跳,HttpClient 必须复用);
  • 缓存下游结果(用户名这种不常变的,缓存几分钟省一次跨服务调用);
  • 如果 sidecar 税痛,考虑 sidecar-less(Ambient / Cilium)。

第 9 跳:序列化 + 返回

对象 → JSON(中等对象 ~10–100μs)→ 原路返回:中间件收尾 → Kestrel 写响应 → Nginx gzip 压缩 → TLS 加密 → 客户端。

优化:压缩省带宽(但别 gzip 已经压过的图/视频);大响应用异步流,别一次性 buffer 进内存;字段精简,别返回前端用不上的列(既浪费序列化又浪费带宽)。


把全程延迟叠起来

同机房、热请求(连接已复用、Redis 命中),这一路大概:

热请求冷启动 / 缓存未命中
DNS<1ms(命中)10–100ms
TCP + TLS0(连接复用)1–2ms(2–3 RTT)
LB + Nginx~1ms~1ms
Kestrel + 中间件~1ms~1ms
Redis(命中)~0.2ms
DB(未命中才走)~2–10ms
下游服务0–5ms(视是否调用)+2–5ms
序列化 + 返回~1ms~1ms
合计~3–5ms~15–30ms+

核心洞察:总延迟是各跳之和,抓最贵的那几跳——几乎总是 DB 的磁盘 IO + 网路的 RTT。在一个 μs 级的 JSON 序列化上纠结,不如把一个 DB 查询的索引建对(省 10ms)、把一个跨服务调用并行化(省一个 RTT)。这就是延迟数字篇"把现实操作拆回原子项"方法论的全链路应用。


全链路可观测

这么多跳,出问题了怎么定位"慢在哪"?给每个请求一个 traceId,跨所有跳透传(HTTP header),每一跳是一个 span。OpenTelemetry 把它们串成一条 trace,一眼看到哪一跳吃了 80% 的时间。

没有全链路追踪,慢请求是个黑盒;有了它,瓶颈一目了然。详见《后端架构实战(七):可观测性——微服务的神经系统》。这是全链路的"运维面",和前面十一跳的"原理面"同样重要。


小结

  • 一次请求 = 九跳:DNS → TCP/TLS → LB → Nginx → Kestrel → 中间件 → Redis → DB →(下游服务)→ 序列化返回。每一跳都有专文展开原理、有延迟量级、有优化点。
  • 总延迟 = 各跳之和,抓大头:DB 磁盘 IO 和网络 RTT 永远是重点,μs 级 CPU 处理不是。
  • 杠杆最大的两处:缓存命中(直接省掉 DB 那一跳)+ 连接复用(省掉握手那两跳)。
  • 微服务多一跳税:sidecar 每 hop +2–5ms,能并行就别串行。
  • 排障靠全链路 trace:traceId 贯穿所有跳,是"慢在哪"的答案。

单看每一层是知识点,把它们串成一次请求,才是系统。这也是这个博客写到 117 篇,该有的那张"总图"。


参考资料

这篇是 capstone,主要交叉链接本博客的专题文章(见正文《》引用)。延迟量级全部来自《每个程序员都该知道的延迟数字》。想看跨层的系统性视角,推荐《Designing Data-Intensive Applications》(Martin Kleppmann)。