一个 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 上升?
↓
压力是否向下游扩散?
↓
如何限制压力继续传播?
这也是从“会写接口”走向“理解高并发系统”的一个重要转变。**