写在前面
Nginx、Kestrel、Redis、数据库——单看每一层,这个博客都有专文讲过。但它们怎么串成一次真实的请求?这篇把所有层穿起来,跟着一个 GET /api/orders/123 走完全程。
每一跳只讲三件事:原理(链到已有专文,不重复展开)、大概多慢(链到《每个程序员都该知道的延迟数字》)、怎么优化。这是博客的"集大成"篇——读完你会看到这些独立的文章其实是一条线上的事。
场景
用户在 App 里点开一个订单详情,前端发:
| |
后端要做:验 JWT → 查 Redis 订单缓存 → 没命中就查 DB → 订单里要显示用户名,可能调下游用户服务 → 返回 JSON。
一次请求,九跳:
| |
第 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 + TLS | 0(连接复用) | 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)。