写在前面
Nginx、Kestrel、Redis、数据库——单看每一层,这个博客都有专文讲过。但它们怎么串成一次真实的请求?这篇把所有层穿起来,跟着一个 GET /api/orders/123 走完全程。
每一跳只讲三件事:原理(链到已有专文,不重复展开)、大概多慢(链到《每个程序员都该知道的延迟数字》)、怎么优化。这是博客的"集大成"篇——读完你会看到这些独立的文章其实是一条线上的事。
场景
用户在 App 里点开一个订单详情,前端发:
| |
后端要做:验 JWT → 查 Redis 订单缓存 → 没命中就查 DB → 订单里要显示用户名,可能调下游用户服务 → 返回 JSON。
一次请求,按本文模型拆成十个阶段(编号 0–9):
| |
第 0 跳:DNS 解析
api.example.com 要先变成 IP。浏览器、OS 或本地解析器缓存命中通常很快;未命中时,递归解析器可能继续查询权威 DNS,耗时受缓存状态、网络路径和解析链影响,不能固定写成一个范围。
优化: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 / 连接池让后续请求复用已经建立的连接,避免重复支付 TCP 和 TLS 握手。TLS 1.3 会减少完整握手往返;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)、可选的鉴权前置。TLS 是否在此终止、到上游是否重新加密,取决于信任边界和部署配置,不能默认上游一定走明文。
详见《Nginx 学习笔记(二):反向代理与负载均衡》《(三):HTTPS、性能优化与实战》。Nginx 自身处理是 ms 级。
优化:最容易忘的一条是 upstream keepalive——Nginx 到 Kestrel 也要复用连接,别每个请求都新建一次 TCP(又掉进第 1 跳的坑)。其次是 HTTP/2、gzip 压缩响应。
第 4 跳:Kestrel 接收(I/O 模型 + Pipelines + 线程池)
包到内核了。Kestrel 使用操作系统的异步/事件驱动 I/O 机制,并利用 System.IO.Pipelines 的池化缓冲区减少分配和不必要的复制,再把请求交给 ASP.NET Core 管线。异步 I/O 避免在等待网络时长期占用请求线程,但业务代码仍可能因同步阻塞、锁竞争或 CPU 密集工作占住线程。
这一跳的关键是避免在线程上同步等待慢 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 → 对端应用。
延迟 = 网络往返 + 两端序列化 + 对端处理 + 可能的代理成本。代理会增加数据路径、CPU 和排队成本,但没有适用于所有环境的固定“每 hop 2–5ms”。Istio 官方基准明确指出,延迟取决于协议、连接数、请求率、负载、worker 数和启用的功能;应在自己的拓扑和策略下测 p50/p99。
优化:
- 并行调用——要调多个下游,用
Task.WhenAll并发,别串行await三次(串行 vs 并行差的是整倍 RTT,见《.NET 高并发编程全景》); - 连接池(同第 1 跳,HttpClient 必须复用);
- 缓存下游结果(用户名这种不常变的,缓存几分钟省一次跨服务调用);
- 如果 sidecar 税痛,考虑 sidecar-less(Ambient / Cilium)。
第 9 跳:序列化 + 返回
对象 → JSON(中等对象 ~10–100μs)→ 原路返回:中间件收尾 → Kestrel 写响应 → Nginx gzip 压缩 → TLS 加密 → 客户端。
优化:压缩省带宽(但别 gzip 已经压过的图/视频);大响应用异步流,别一次性 buffer 进内存;字段精简,别返回前端用不上的列(既浪费序列化又浪费带宽)。
把全程延迟叠起来
下面只是展示如何列预算的示例假设,不是可直接套用的基准。真实系统要用 trace 分阶段测量,并分别观察 p50、p95 和 p99:
| 跳 | 热请求 | 冷启动 / 缓存未命中 |
|---|---|---|
| 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 |
| 下游服务 | 取决于 RTT 与对端处理 | 取决于 RTT、握手与对端处理 |
| 序列化 + 返回 | ~1ms | ~1ms |
| 合计 | 按各阶段实测相加 | 按各阶段实测相加 |
核心洞察:总延迟是各跳之和,抓最贵的那几跳——几乎总是 DB 的磁盘 IO + 网路的 RTT。在一个 μs 级的 JSON 序列化上纠结,不如把一个 DB 查询的索引建对(省 10ms)、把一个跨服务调用并行化(省一个 RTT)。这就是延迟数字篇"把现实操作拆回原子项"方法论的全链路应用。
全链路可观测
这么多跳,出问题了怎么定位"慢在哪"?给每个请求一个 traceId,跨所有跳透传(HTTP header),每一跳是一个 span。OpenTelemetry 把它们串成一条 trace,一眼看到哪一跳吃了 80% 的时间。
没有全链路追踪,慢请求是个黑盒;有了它,瓶颈一目了然。详见《后端架构实战(七):可观测性——微服务的神经系统》。这是全链路的"运维面",和前面十跳的"原理面"同样重要。
小结
- 一次请求可以拆成十个阶段(编号 0–9):DNS → TCP/TLS → LB → Nginx → Kestrel → 中间件 → Redis → DB →(下游服务)→ 序列化返回。不是每个部署都包含全部阶段。
- 总延迟 = 各阶段之和,先抓实测大头:远程 I/O 和网络 RTT 经常占主要部分,但 CPU、锁竞争和排队在高负载下同样可能成为瓶颈。
- 杠杆最大的两处:缓存命中(直接省掉 DB 那一跳)+ 连接复用(省掉握手那两跳)。
- 微服务多一层通信成本:RTT、序列化、对端处理和代理都会进入预算,能安全并行的独立调用不要无谓串行。
- 排障靠全链路 trace:traceId 贯穿所有跳,是"慢在哪"的答案。
单看每一层是知识点,把它们串成一次请求,才是系统。这也是这个博客写到一百多篇时,该有的那张"总图"。
参考资料
这篇是 capstone,主要交叉链接本博客的专题文章(见正文《》引用)。延迟量级全部来自《每个程序员都该知道的延迟数字》。想看跨层的系统性视角,推荐《Designing Data-Intensive Applications》(Martin Kleppmann)。