跳转到正文
zeno's blog
返回

Rust Web:一个请求的完整生命周期

专题: Rust Web

Table of contents

Open Table of contents

全局图

客户端

  │ TCP 字节流

┌─────────────────────────────────────────────────────────────────────┐
│ Linux 内核                                                          │
│  网卡 → 中断 → 协议栈 (IP → TCP) → socket 接收缓冲区                │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ epoll 通知 fd 可读

┌─────────────────────────────────────────────────────────────────────┐
│ tokio (异步运行时)                                                    │
│  mio 收到 epoll 事件 → 唤醒等待这个 socket 的 task                    │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ TcpStream 可读

┌─────────────────────────────────────────────────────────────────────┐
│ hyper (HTTP 协议实现)                                                │
│  读取字节流 → 解析 HTTP 行 + Header → 构造 Request<Body>              │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ Request<Body>

┌─────────────────────────────────────────────────────────────────────┐
│ tower Layer 链 (中间件)                                              │
│                                                                     │
│  TraceLayer     → 创建 span,记录方法/路径/开始时间                    │
│  TimeoutLayer   → 启动 30s 超时计时器                                 │
│  CorsLayer      → 检查 Origin,通过                                  │
│  CompressionLayer → 标记"响应时压缩"                                  │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ Request<Body>(原样传递)

┌─────────────────────────────────────────────────────────────────────┐
│ axum Router (路由匹配)                                               │
│                                                                     │
│  POST /api/v1/players/42/items                                      │
│  → 匹配路由 /api/v1/players/{player_id}/items                       │
│  → 找到 handler: create_item                                        │
│  → 把 {player_id} = "42" 存入 request.extensions                    │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ Request<Body> + 路由匹配结果

┌─────────────────────────────────────────────────────────────────────┐
│ Extractor 提取参数                                                   │
│                                                                     │
│  State(state)         ← extensions 中取 AppState(clone Arc)        │
│  Path(player_id)      ← extensions 中取路由参数,反序列化 "42" → u32  │
│  BearerToken(token)   ← headers 中取 Authorization,剥离 "Bearer "   │
│  Json(body)           ← 读取整个 body,serde 反序列化为 CreateItem    │
│                                                                     │
│  任何一步失败 → Rejection → 直接返回 4xx 错误响应,不进入 handler       │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ 提取完毕的参数

┌─────────────────────────────────────────────────────────────────────┐
│ Handler (你的业务逻辑)                                               │
│                                                                     │
│  async fn create_item(                                              │
│      State(state): State<AppState>,                                 │
│      Path(player_id): Path<u32>,                                    │
│      BearerToken(token): BearerToken,                               │
│      Json(body): Json<CreateItem>,                                  │
│  ) -> Result<Json<Item>, AppError> {                                │
│                                                                     │
│    ┌─ 1. 验证 token ─────────────────────────────────────────┐      │
│    │  查 Redis 缓存: session:{token} 是否存在                  │      │
│    │                                                         │      │
│    │    ┌──────────────────────────────────┐                  │      │
│    │    │ Redis                            │                  │      │
│    │    │  GET session:eyJhbG...           │                  │      │
│    │    │  → 命中: player_id = 42 ✓        │                  │      │
│    │    └──────────────────────────────────┘                  │      │
│    └─────────────────────────────────────────────────────────┘      │
│                                                                     │
│    ┌─ 2. 写入数据库 ─────────────────────────────────────────┐      │
│    │  INSERT INTO items (player_id, name, quantity)            │      │
│    │  VALUES (42, 'AK-47', 1) RETURNING *                     │      │
│    │                                                         │      │
│    │    ┌──────────────────────────────────┐                  │      │
│    │    │ PostgreSQL                       │                  │      │
│    │    │  连接池取一个连接                  │                  │      │
│    │    │  执行 SQL                         │                  │      │
│    │    │  返回新 Item { id: 1001, ... }    │                  │      │
│    │    │  连接归还连接池                    │                  │      │
│    │    └──────────────────────────────────┘                  │      │
│    └─────────────────────────────────────────────────────────┘      │
│                                                                     │
│    ┌─ 3. 失效缓存 ───────────────────────────────────────────┐      │
│    │  DEL player:42:items(下次查询时重新加载)                  │      │
│    │    → Redis DEL                                           │      │
│    └─────────────────────────────────────────────────────────┘      │
│                                                                     │
│    return Ok(Json(item))                                            │
│  }                                                                  │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ Result<Json<Item>, AppError>

┌─────────────────────────────────────────────────────────────────────┐
│ IntoResponse (结果 → HTTP 响应)                                      │
│                                                                     │
│  Ok(Json(item))                                                     │
│  → StatusCode: 200                                                  │
│  → Header: Content-Type: application/json                           │
│  → Body: {"id":1001,"name":"AK-47","quantity":1,"player_id":42}     │
│                                                                     │
│  如果是 Err(AppError):                                               │
│  → tracing::error! 记录完整错误链到日志                                │
│  → StatusCode: 500 / 404 / 400(取决于错误类型)                       │
│  → Body: {"error": "..."}                                           │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ Response<Body>

┌─────────────────────────────────────────────────────────────────────┐
│ tower Layer 链 (中间件 — 响应方向,从内往外)                            │
│                                                                     │
│  CompressionLayer → Content-Encoding: gzip,压缩 body               │
│  CorsLayer        → 加 Access-Control-Allow-Origin 响应头            │
│  TimeoutLayer     → 取消计时器(没超时)                               │
│  TraceLayer       → 关闭 span,记录 status=200, latency=23ms        │
│                     → 可观测性: span 导出到 Jaeger/Tempo              │
│                     → 可观测性: 打印日志                               │
│                     → 可观测性: counter!("requests_total").increment  │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ Response<Body>(加了 header + 压缩)

┌─────────────────────────────────────────────────────────────────────┐
│ hyper (HTTP 序列化)                                                  │
│                                                                     │
│  Response<Body>                                                     │
│  → "HTTP/1.1 200 OK\r\n"                                           │
│  → "content-type: application/json\r\n"                             │
│  → "content-encoding: gzip\r\n"                                    │
│  → "access-control-allow-origin: *\r\n"                            │
│  → "\r\n"                                                          │
│  → [gzip 压缩后的 JSON 字节]                                        │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ 字节流

┌─────────────────────────────────────────────────────────────────────┐
│ tokio → TCP write → 内核 → 网卡 → 网络                               │
└──────────────────────────────┬──────────────────────────────────────┘


                            客户端收到响应

各部分的职责

内核层

做了什么:
  网卡收到以太网帧 → IP 层 → TCP 层 → 数据放入 socket 接收缓冲区
  通过 epoll 通知 tokio "这个 fd 可读了"

你需要关心吗: 不需要

tokio

做了什么:
  mio 监听 epoll 事件 → 唤醒对应的 task → poll task 的 Future

你需要关心吗: 不需要,写 async/await 就行
唯一需要注意: 不要在 async 函数里做 CPU 密集操作阻塞事件循环

hyper

做了什么:
  请求方向: TCP 字节流 → 解析 HTTP 协议 → Request<Body>
  响应方向: Response<Body> → 序列化 HTTP 协议 → TCP 字节流
  管理: HTTP/1.1 keep-alive、HTTP/2 多路复用

你需要关心吗: 不需要,axum 封装好了

tower Layer 链

做了什么:
  请求进来时(从外到内):
    TraceLayer      → 创建 tracing span(请求开始)
    TimeoutLayer    → 启动超时计时器
    CorsLayer       → 检查 CORS 头
    CompressionLayer → 记录客户端支持的压缩算法

  响应出去时(从内到外):
    CompressionLayer → 压缩响应 body
    CorsLayer        → 添加 CORS 响应头
    TimeoutLayer     → 取消计时器
    TraceLayer       → 关闭 span,记录耗时和状态码

你需要关心吗: 需要,你决定叠加哪些 Layer

  let app = Router::new()
      .route(...)
      .layer(TraceLayer::new_for_http())
      .layer(TimeoutLayer::new(Duration::from_secs(30)))
      .layer(CompressionLayer::new())
      .layer(CorsLayer::new().allow_any_origin());

axum Router

做了什么:
  拿到 Request → 根据 method + path 匹配路由
  找到对应的 handler → 把路由参数存入 request.extensions

你需要关心吗: 需要,你定义路由

  Router::new()
      .route("/api/v1/players/{player_id}/items", post(create_item))

Extractor

做了什么:
  从 Request 的各个部分提取 handler 需要的参数
  提取失败 → 直接返回错误响应,handler 不会被调用

  提取来源:
    Parts.extensions  → State, Path
    Parts.uri         → Query
    Parts.headers     → HeaderMap, 自定义 BearerToken
    Body              → Json, String, Bytes

你需要关心吗: 需要,你选择用哪些 extractor 作为 handler 参数

Handler

做了什么:
  你的业务逻辑。可能涉及:
  - Redis: 查缓存、验证 session、限流计数
  - PostgreSQL: CRUD 操作
  - 外部 API 调用
  - 计算逻辑

你需要关心吗: 这是你唯一真正需要写的部分

IntoResponse

做了什么:
  把 handler 的返回值转成 HTTP Response

  Ok(Json(item)) → 200 + application/json + 序列化
  Err(AppError)  → 500/404/400 + 错误信息

你需要关心吗: 需要为自定义错误类型实现 IntoResponse

可观测性(贯穿整个过程)

Traces (tracing + OpenTelemetry):
  TraceLayer 创建顶层 span
  handler 里的 #[instrument] 创建子 span
  Redis/PostgreSQL 操作各自创建子 span
  → 整棵 span 树导出到 Jaeger/Tempo

Logs (tracing):
  info!("creating item for player {}", player_id)
  error!("database query failed: {}", err)
  → JSON 输出到 stdout → Promtail → Loki

Metrics (metrics crate):
  counter!("http_requests_total", "method" => "POST", "path" => "/items")
  histogram!("http_request_duration_seconds").record(elapsed)
  gauge!("db_pool_active_connections").set(pool.size())
  → /metrics 端点 → Prometheus 抓取 → Grafana 看板

时间线视角

t=0.0ms   内核: TCP 数据到达 socket 缓冲区,epoll 通知
t=0.1ms   tokio: 唤醒 task,开始 poll
t=0.2ms   hyper: 开始解析 HTTP
t=0.5ms   hyper: Request<Body> 构造完成
t=0.6ms   TraceLayer: 创建 span {method=POST, path=/api/v1/players/42/items}
t=0.7ms   TimeoutLayer: 启动 30s 计时器
t=0.8ms   CorsLayer: 检查通过
t=0.9ms   Router: 路由匹配成功
t=1.0ms   Extractor: State 提取(clone Arc)
t=1.1ms   Extractor: Path 提取("42" → u32)
t=1.2ms   Extractor: BearerToken 提取
t=1.5ms   Extractor: Json body 读取 + 反序列化
t=2.0ms   Handler: 开始执行
t=2.5ms   Handler → Redis: GET session:token (命中)
t=3.0ms   Handler → PostgreSQL: 从连接池取连接
t=3.5ms   Handler → PostgreSQL: INSERT 执行中...
t=8.0ms   Handler → PostgreSQL: 返回新 Item,归还连接
t=8.5ms   Handler → Redis: DEL 缓存
t=9.0ms   Handler: return Ok(Json(item))
t=9.1ms   IntoResponse: Json → Response<Body>
t=9.2ms   CompressionLayer: gzip 压缩
t=9.3ms   CorsLayer: 加响应头
t=9.4ms   TimeoutLayer: 取消计时器
t=9.5ms   TraceLayer: 关闭 span {status=200, latency=8.9ms}
t=9.6ms   hyper: 序列化 HTTP 响应
t=10.0ms  tokio: TCP write → 内核发出

一个简单请求,从到达到返回约 10ms,其中 5ms 花在数据库上。


分享这篇文章:

上一篇
Rust Web:Tonic gRPC Server 结构指南
下一篇
Rust Web:Axum 返回值-IntoResponse 与错误处理