一个 HTTP 请求的并发链路:从 Web Server 到 MySQL

前面的文章分别讨论了并发是什么、进程线程协程如何执行请求、IO 多路复用如何管理大量连接,以及如何用 QPS、并发数和响应时间衡量系统。

但这些概念真正进入后端系统之后,会形成一条完整的链路:

Client
   ↓
Web Server / Nginx
   ↓
Application
   ↓
Process / Thread / Coroutine
   ↓
Redis / RPC / MySQL
   ↓
Response
   ↓
Client

真正的高并发问题,通常也不是发生在某一个组件上,而是发生在整个链路的容量不匹配上。

例如:

Web Server 可以接收 10000 个连接,应用可以运行 1000 个协程,但 MySQL 连接池只有 50 个连接。

这并不意味着系统只能处理 50 个 HTTP 请求,但意味着同时进行数据库操作的请求最多受到这 50 个连接的约束。

因此,理解高并发,不能只看 Web Server 或应用服务器,而要沿着一次请求完整地看下去。

1. 一个 HTTP 请求到底经过哪些组件?

假设用户访问:

GET /api/orders/10001

一个典型后端系统可能经过:

┌──────────┐
│  Client  │
└────┬─────┘
     │ HTTP
     ▼
┌──────────────┐
│ Nginx / Web  │
│    Server    │
└──────┬───────┘
       │
       ▼
┌──────────────────┐
│ Application      │
│ Hyperf / Java... │
└────────┬─────────┘
         │
     ┌───┴────┐
     │        │
     ▼        ▼
  Redis     MySQL
     │        │
     └───┬────┘
         ▼
      Response

这里至少存在几个不同维度的“并发”:

  • Web Server 的网络连接并发

  • 应用层正在处理的请求并发

  • Redis 请求并发

  • MySQL 查询并发

  • 数据库连接并发

  • RPC 下游请求并发

这些数字通常不会相等。

这是理解后端并发最重要的几个概念之一。


2. Web Server 的并发:能接多少连接?

请求首先到达 Web Server,例如 Nginx。

Web Server 主要负责:

  • 接收 TCP 连接

  • 处理 HTTP 协议

  • TLS

  • 静态文件

  • 反向代理

  • 将请求转发给后端应用

例如:

Client
   │
   │ TCP
   ▼
Nginx
   │
   │ HTTP
   ▼
Hyperf

假设 Nginx 当前维护:

10000 个 TCP 连接

这并不意味着:

10000 个 HTTP 请求正在执行

因为一个 TCP 连接可以复用多个 HTTP 请求。

尤其是在 HTTP/1.1 Keep-Alive、HTTP/2 等情况下,连接和请求本身就是两个不同的概念。

因此:

连接数 ≠ 请求并发数

这也是前面文章反复强调的原因。

对于 Web Server 来说,更重要的问题是:

当前有多少连接?多少请求正在处理?连接是否大量处于等待状态?


3. 应用层:真正开始执行业务逻辑

请求经过 Web Server 后进入应用程序。

例如 Hyperf:

Nginx
  ↓
Hyperf
  ↓
Controller
  ↓
Service
  ↓
Repository
  ↓
Redis / MySQL

如果使用传统 PHP-FPM 模型,可以简单理解成:

请求
 ↓
PHP-FPM Worker
 ↓
执行 PHP
 ↓
等待数据库
 ↓
返回

如果使用 Swoole/Hyperf 协程模型,则可能是:

Event Loop
   │
   ├── Coroutine A → HTTP Request A
   ├── Coroutine B → HTTP Request B
   ├── Coroutine C → HTTP Request C
   ├── Coroutine D → HTTP Request D
   └── ...

当 Coroutine A 执行数据库查询:

$result = $db->query($sql);

如果底层 IO 是异步/协程友好的,当前协程可以进入等待状态,而运行时继续处理其他任务。

例如:

Coroutine A
    │
    ├── SQL
    │
    └── 等待数据库
          ↓
       让出执行
          ↓
Coroutine B
    │
    └── 继续执行

Coroutine C
    │
    └── 继续执行

这就是协程和 IO 多路复用结合后非常重要的优势:

等待 IO 的时候,不必让整个执行线程闲在那里。

但是这里有一个非常重要的限制:

协程可以提高执行资源的利用率,却不能突破下游资源本身的容量。


4. 一个 HTTP 请求,可能对应多个下游请求

这是实际系统中非常容易被忽略的一点。

假设一个接口:

GET /api/home

业务逻辑需要:

1. Redis 查询用户信息
2. Redis 查询配置
3. Redis 查询推荐数据
4. MySQL 查询订单
5. MySQL 查询商品
6. RPC 查询库存

那么一个 HTTP 请求实际上可能产生:

1 HTTP Request
    ↓
3 Redis Operations
2 MySQL Queries
1 RPC Request

这叫做一种典型的 Fan-out(扇出)。

假设同时有:

1000 个 HTTP 请求

那么理论上可能产生:

Redis:
1000 × 3 = 3000 次操作

MySQL:
1000 × 2 = 2000 次查询

RPC:
1000 × 1 = 1000 次请求

因此:

HTTP QPS = 1000

并不代表:

MySQL QPS = 1000

更不能认为:

所有下游 QPS 都是 1000

真实系统的下游压力取决于业务代码产生了多少操作。

所以分析高并发系统时,不能只问:

“接口 QPS 是多少?”

还应该问:

“每个请求会访问哪些下游?访问几次?是否并行访问?”


5. MySQL 连接池:并发链路中的重要边界

继续看 MySQL。

假设应用有:

1000 个并发 HTTP 请求

同时应用可以运行:

1000 个协程

但 MySQL 连接池只有:

50 个连接

这时并不是只有 50 个 HTTP 请求能够运行。

而是:

1000 个 HTTP 请求
        │
        ▼
1000 个应用协程
        │
        ▼
   MySQL 连接池
        │
   ┌────┴────┐
   │         │
  50个连接   等待
   │         │
   ▼         ▼
 MySQL    950 个请求

也就是说:

应用并发 ≠ 数据库并发

其中只有获得数据库连接的请求,才能真正进入数据库执行阶段。

其余请求可能处于:

等待连接池

状态。

因此,连接池实际上形成了一个并发边界。


6. 为什么数据库连接池不是越大越好?

看到这里,一个很自然的想法是:

“既然 50 个连接不够,那直接改成 500 个不就行了?”

通常不能这么简单。

因为数据库本身也存在容量限制。

例如 MySQL 同时处理大量连接和查询时,会受到:

  • CPU

  • 磁盘 IO

  • Buffer Pool

  • 锁竞争

  • 内存

  • 临时表

  • 排序

  • 索引效率

  • SQL 执行时间

  • 最大连接数

等因素影响。

例如:

应用连接池:500
        ↓
MySQL:500 个连接
        ↓
大量 SQL 同时执行
        ↓
CPU / IO / Lock 竞争
        ↓
SQL RT 上升
        ↓
应用请求 RT 上升

最终可能出现:

连接池越大
    ↓
数据库并发越高
    ↓
数据库竞争越严重
    ↓
单条 SQL 越慢
    ↓
连接占用时间越长
    ↓
连接池更加容易耗尽

所以:

连接池大小应该根据数据库实际处理能力、SQL 特征和应用并发模型进行压测确定,而不是简单地越大越好。

例如某个系统设置:

MySQL Pool Max = 50

这并不意味着系统最多只能处理 50 QPS。

如果每个请求只偶尔访问数据库,而且大量请求主要命中缓存,那么 HTTP QPS 完全可能远高于 50。

反过来,如果每个请求都需要执行复杂 SQL,即使连接池设置成 500,数据库也未必能承受。


7. 下游变慢,为什么会导致整个系统越来越慢?

这是高并发系统最典型的级联问题。

假设正常情况下:

HTTP RT = 100ms

系统:

1000 RPS

根据前面介绍的 Little’s Law:

并发数 ≈ RPS × RT

所以:

1000 × 0.1 = 100

大约只有:

100 个请求处于在途状态

现在 MySQL 出现问题:

SQL RT:
20ms
↓
500ms
↓
2s

假设接口整体 RT 变成:

2 秒

在 RPS 仍然为 1000 的情况下:

1000 × 2 = 2000

在途请求就可能增加到大约:

2000

于是:

MySQL 变慢
    ↓
HTTP RT 上升
    ↓
在途请求增加
    ↓
应用协程 / 线程 / 内存占用增加
    ↓
连接池等待增加
    ↓
请求继续变慢
    ↓
超时请求增加
    ↓
重试请求增加
    ↓
下游压力进一步增加

最终形成:

下游变慢
   ↓
请求堆积
   ↓
资源耗尽
   ↓
系统整体变慢
   ↓
更多请求堆积

这就是典型的级联故障。

真正危险的并不是某一次 SQL 慢了,而是:

一个下游组件的性能下降,通过请求链路传播到了整个系统。


8. Redis、RPC、MySQL 都可能成为瓶颈

因此,一个接口的性能不能只看 PHP 或 Java 代码本身。

例如:

                   ┌── Redis
                   │
HTTP → Application ├── MySQL
                   │
                   ├── RPC
                   │
                   └── MQ

任何一个下游都可能成为瓶颈。

例如:

Redis 变慢

Redis RT ↑
   ↓
应用等待时间 ↑
   ↓
HTTP RT ↑

MySQL 连接池耗尽

请求
 ↓
等待 DB Connection
 ↓
请求 RT ↑

RPC 服务变慢

Service A
    ↓
RPC
    ↓
Service B
    ↓
响应变慢
    ↓
Service A 请求堆积

所以在微服务环境中,这种问题会进一步扩散:

Gateway
   ↓
Service A
   ↓
Service B
   ↓
Service C
   ↓
MySQL

如果 Service C 的数据库突然变慢,影响可能一路向上返回到 Gateway。

这也是为什么高并发系统最终一定会涉及:

  • 超时

  • 限流

  • 排队

  • 背压

  • 熔断

  • 降级

  • 缓存

  • 异步化

这些机制。

它们解决的核心问题都是:

当某个环节处理能力下降时,如何阻止压力继续向整个系统扩散。


9. 从完整链路理解“并发”

现在可以把一个请求完整串起来:

                    HTTP Request
                         │
                         ▼
                ┌─────────────────┐
                │ Web Server      │
                │ 连接 / 请求管理 │
                └────────┬────────┘
                         │
                         ▼
                ┌─────────────────┐
                │ Application     │
                │ Process/Thread  │
                │ Coroutine       │
                └────────┬────────┘
                         │
               ┌─────────┼─────────┐
               │         │         │
               ▼         ▼         ▼
            Redis      MySQL      RPC
               │         │         │
               │      Connection   │
               │        Pool       │
               │         │         │
               └─────────┼─────────┘
                         ▼
                      Response

这里至少存在五个不同的问题:

第一,Web Server 能接收多少连接?

这是网络层面的容量。

第二,应用能同时处理多少请求?

这与进程、线程、协程、Event Loop、CPU 等有关。

第三,一个请求会产生多少下游操作?

这决定 Fan-out 后的实际下游压力。

第四,下游服务能够承受多少并发?

Redis、MySQL、RPC 都有自己的容量边界。

第五,下游变慢之后怎么办?

如果没有超时、限流、排队、熔断和降级,请求很容易沿着整个链路堆积。

因此:

高并发不是某一个组件的属性,而是整个请求链路的容量问题。


10. 总结:并发如何贯穿整个系统?

一个请求进入真实系统之后:

Client
 ↓
Web Server
 ↓
Application
 ↓
Coroutine / Thread / Process
 ↓
Redis
 ↓
MySQL Connection Pool
 ↓
MySQL
 ↓
Response

每一层都有自己的:

容量
并发
队列
延迟
资源限制

真正的高并发问题,往往就是这些容量之间出现了不匹配。

例如:

Web Server      10000 connections
Application      1000 coroutines
MySQL Pool         50 connections
MySQL               ↓
                  实际容量

因此,优化高并发系统不能简单地说:

“把协程数量调大。”

也不能简单地说:

“把数据库连接池调大。”

正确的思路应该是沿着完整链路寻找瓶颈:

请求从哪里进入?
       ↓
在哪里等待?
       ↓
等待什么资源?
       ↓
哪个资源达到上限?
       ↓
为什么 RT 上升?
       ↓
压力是否向下游扩散?
       ↓
如何限制压力继续传播?

这也是从“会写接口”走向“理解高并发系统”的一个重要转变。**