理解并发:一个 HTTP 请求到后端到底发生了什么?
在开发后端系统时,我们经常会看到这样的描述:
“这个接口支持 1000 并发。”
但“1000 并发”到底意味着什么?
- 是不是意味着服务器同时启动 1000 个线程?
- 是不是意味着 CPU 同时执行 1000 个请求?
- 如果一个请求正在等待 MySQL,CPU 又在做什么?
- 一个 PHP/Hyperf 服务为什么可以同时处理大量 HTTP 请求?
要真正理解高并发,不能一开始就从 QPS、限流、缓存这些概念入手,而应该先理解一个最基本的问题:
一个 HTTP 请求进入后端以后,到底发生了什么?
1. 先理解什么是并发
并发(Concurrency)描述的是:
多个任务在同一个时间段内处于进行状态,并且它们的执行过程存在交错。
并发和并行(Parallelism)不是同一个概念。
例如:
任务 A:████████
任务 B: ████████
两个任务都在进行,但可能只有一个 CPU 核心。
CPU 可以先执行 A:
A A A A
然后因为 A 正在等待 IO,转而执行 B:
A A → 等待
↓
B B B B
这属于并发。
如果机器有多个 CPU 核心:
CPU Core 1:A A A A
CPU Core 2:B B B B
两个任务可以在同一时刻真正执行,这才是并行。
所以:
并发关注的是“多个任务同时处于进行状态”;并行关注的是“多个任务是否能够同时执行”。
一台只有一个 CPU 核心的机器,同样可以支持大量并发连接。
这也是后端开发中非常重要的一个认识:
1000 个并发请求,并不意味着 CPU 同时执行 1000 个请求。
2. 一个 HTTP 请求从哪里开始?
假设用户访问:
https://example.com/api/users/100
从宏观上看,请求大致经过:
浏览器
↓
DNS
↓
网络连接
↓
Web Server / 反向代理
↓
后端应用
↓
Redis / MySQL / 其他服务
↓
后端应用
↓
Web Server
↓
浏览器
HTTP 本身属于应用层协议,它定义的是请求和响应的语义,并不规定后端内部必须使用进程、线程还是协程来实现。
RFC 9110 将 HTTP 定义为无状态的应用层请求/响应协议:客户端发送 Request,服务器解析、处理请求并返回 Response。
因此:
HTTP
解决的是:
“客户端和服务器之间如何表达一次请求和响应?”
而:
进程
线程
协程
事件循环
IO 多路复用
解决的是:
“服务器内部如何高效地处理这些请求?”
这是两个不同层次的问题。
3. 一个请求进入后端以后,谁在处理?
假设有 1000 个用户同时请求:
GET /api/users/100
我们可以简单地画成:
1000 个 HTTP 请求
│
▼
Web Server
│
▼
后端应用服务
│
┌────┼────┬────┐
▼ ▼ ▼ ▼
请求1 请求2 请求3 ... 请求1000
但真正的问题是:
后端应用真的会创建 1000 个执行单元吗?
不一定。
不同技术栈有完全不同的并发模型。
例如:
PHP-FPM
→ 多进程
Java Servlet / Tomcat
→ 线程池
.NET ASP.NET Core
→ 线程池 + 异步 IO
Node.js
→ Event Loop + 非阻塞 IO
Swoole / Hyperf
→ 协程 + Event Loop + IO 多路复用
所以不能简单地说:
“一个 HTTP 请求 = 一个线程。”
这个说法在现代后端系统中并不成立。
更准确的理解应该是:
一个 HTTP 请求需要一个执行上下文来运行,而这个执行上下文可以由进程、线程、协程或事件循环等不同机制承载。
4. 请求执行过程中,CPU 到底在干什么?
这是理解并发最关键的一步。
假设有这样一个接口:
public function user(int $id)
{
$user = $this->userRepository->find($id);
return $user;
}
看起来非常简单,但实际上可能发生:
HTTP 请求
↓
解析请求
↓
路由匹配
↓
Controller
↓
Service
↓
Repository
↓
MySQL
↓
等待数据库响应
↓
拿到结果
↓
业务处理
↓
生成 JSON
↓
返回 HTTP Response
注意中间这一段:
MySQL
↓
等待
此时 CPU 不需要一直执行这个请求。
因为数据库查询属于 IO 操作。
例如:
CPU:
处理请求
↓
发起 MySQL 查询
↓
等待
如果采用合适的异步/事件驱动模型,CPU 可以在等待数据库的时候处理其他请求:
请求 A
↓
查询 MySQL
↓
等待 ──────────────┐
│
▼
CPU 去处理
│
请求 B │
↓ │
业务计算 │
↓ │
返回 │
│
└── MySQL 返回
↓
继续 A
这就是高并发系统非常重要的思想:
不要让 CPU 在等待 IO 的时候闲着。
当然,这并不意味着所有 IO 都天然不会阻塞。
如果应用使用的是阻塞式 IO,那么线程本身可能会在等待期间阻塞;具体能否继续处理其他请求,取决于整个运行时和并发模型。
5. CPU 密集型和 IO 密集型
理解并发时,经常会遇到两个概念:
CPU 密集型
任务的大部分时间都在消耗 CPU。
例如:
大量计算
图片处理
视频编码
复杂算法
加密计算
数据压缩
特点:
CPU ████████████████
IO ██
这种任务增加并发并不一定能提高性能。
如果 CPU 已经达到:
100%
继续增加任务,只会产生更多调度、上下文切换和排队。
IO 密集型
任务的大部分时间在等待 IO。
例如:
MySQL
Redis
HTTP API
文件系统
网络服务
消息队列
大致可以表示:
CPU ███
IO █████████████████
这种情况下,一个执行单元在等待 IO 时,如果运行时能够把 CPU 让给其他任务,就可以显著提高整体资源利用率。
这也是为什么现代高并发 Web 服务大量采用:
异步 IO
非阻塞 IO
IO 多路复用
事件循环
协程
线程池
连接池
等技术。
6. 1000 个并发请求,不等于 1000 个 CPU 任务
假设现在有:
1000 个 HTTP 请求
其中:
100 个正在执行 PHP 业务代码
700 个正在等待数据库/Redis/网络 IO
200 个正在排队
那么“1000 并发”描述的是:
当前系统中有 1000 个请求处于处理流程中
而不是:
CPU 同时执行 1000 个请求
实际上,这 1000 个请求可能处于完全不同的状态:
请求 A → CPU 计算
请求 B → 等待 MySQL
请求 C → 等待 Redis
请求 D → 等待外部 HTTP API
请求 E → 等待 Socket
请求 F → 排队
请求 G → 正在发送响应
...
因此:
并发量本质上描述的是系统同时处理多少“进行中的工作”。
这也是后面理解连接池、限流、队列、背压等概念的基础。
7. 一个请求为什么会占用资源?
一个 HTTP 请求并不是一个简单的数字。
它在执行过程中可能占用:
网络连接
↓
Socket
↓
内存
↓
CPU
↓
执行上下文
↓
数据库连接
↓
Redis 连接
↓
其他服务连接
例如:
客户端
│
│ HTTP Request
▼
Nginx
│
│ 转发
▼
Hyperf
│
├── Redis
│
├── MySQL
│
└── HTTP API
如果同时存在 1000 个请求:
1000 HTTP 请求
│
├── 可能存在大量 Socket
│
├── 大量请求上下文
│
├── 大量内存占用
│
└── 大量下游 IO
但是,下游资源未必有 1000 个。
例如:
HTTP 并发:1000
MySQL 连接池:50
那么很可能出现:
1000 个请求
│
▼
50 个 MySQL 连接
│
├── 50 个执行 SQL
│
└── 950 个等待
这时候真正的瓶颈已经不是 HTTP 服务本身,而是:
MySQL 连接池
这就是后面“并发链路”文章要重点讨论的问题:
一个系统能够承受多少并发,最终取决于整条链路上最受限的资源。
8. 并发、吞吐量和响应时间是什么关系?
理解并发不能只看一个数字。
后端系统最常见的几个指标包括:
并发数
当前正在处理的请求数量。
例如:
100 个请求正在处理
可以理解为:
Concurrency = 100
QPS
Queries Per Second,通常用于描述每秒处理多少请求/查询。
例如:
QPS = 1000
表示系统平均每秒完成约 1000 次请求处理。
对于 HTTP API,更常见的说法也包括:
RPS
Requests Per Second
具体指标名称应结合系统统计口径理解,不能机械认为所有场景下 QPS 和 RPS 完全等价。
RT
Response Time,即一次请求从发出到获得响应所经历的时间。
例如:
RT = 100ms
表示请求平均或某个统计分位上的响应时间约为 100ms,具体要看指标定义。
生产环境更应该关注:
P50
P90
P95
P99
P999
而不是只看平均值。
例如:
平均 RT:100ms
P99:2s
说明绝大多数请求很快,但尾部请求已经非常慢。
9. 一个非常重要的关系:并发 ≈ 吞吐量 × 响应时间
在稳定系统中,可以用排队论中的 Little’s Law 理解:
L = λW
其中:
L = 系统中的平均任务数量
λ = 平均到达率
W = 平均任务停留时间
对于 HTTP 服务,可以近似理解为:
平均并发数 ≈ QPS × 平均响应时间
例如:
QPS = 1000
RT = 100ms
将:
100ms = 0.1s
代入:
并发数 ≈ 1000 × 0.1
≈ 100
也就是说,在稳定状态下:
1000 QPS
100ms 平均响应时间
对应的平均在途请求量大约是:
100
如果 RT 变成:
10s
那么:
并发数 ≈ 1000 × 10
≈ 10000
这解释了一个非常常见的现象:
请求变慢,本身就会增加系统中的并发请求数量。
例如:
正常:
1000 QPS
100ms RT
≈ 100 并发
MySQL 变慢:
1000 QPS
5s RT
≈ 5000 并发
QPS 没有增加,但系统中的在途请求突然从约 100 增加到了约 5000。
如果每个请求还占用:
内存
连接
协程
线程
Socket
数据库连接
系统就可能进一步恶化。
这就是很多高并发故障的典型演化过程:
下游变慢
↓
RT 上升
↓
在途请求增加
↓
资源占用增加
↓
请求排队
↓
RT 进一步上升
↓
更多请求堆积
↓
系统雪崩
因此,高并发问题很多时候并不是:
“请求突然变多了。”
而可能是:
某个环节变慢,让整个系统积累了越来越多未完成请求。
10. 从一个 HTTP 请求理解整个并发模型
现在重新看一个简单请求:
GET /api/users/100
它可以抽象成:
HTTP Request
│
▼
Web Server
│
▼
后端服务
│
┌──────┴──────┐
│ │
CPU IO
│ │
│ ┌─────┼─────┐
│ │ │ │
│ MySQL Redis HTTP API
│ │ │ │
└───────┴─────┴─────┘
│
▼
业务处理
│
▼
HTTP Response
当只有一个请求时,一切都很简单。
但是当请求变成:
1
10
100
1000
10000
问题就开始出现:
谁来接收这些连接?
谁来执行这些请求?
需要多少进程?
需要多少线程?
需要多少协程?
CPU 是否够?
内存是否够?
数据库连接够不够?
Redis 连接够不够?
请求是否需要排队?
请求等待 IO 时谁来处理其他请求?
这些问题共同构成了后端系统的并发模型。
因此,可以把后端并发理解成这样一条链:
客户端请求
↓
网络连接
↓
Socket
↓
Web Server
↓
进程 / 线程 / 协程
↓
业务代码
↓
CPU / IO
↓
数据库 / Redis / 外部服务
↓
等待 / 执行 / 排队
↓
响应
真正的高并发,并不是简单地“同时处理更多请求”。
它实际上是在解决:
如何让有限的 CPU、内存、网络、连接和下游资源,在大量同时到来的请求之间得到高效调度。
总结
理解并发,首先要建立几个正确的认识。
第一,并发不等于并行。
并发表示多个任务同时处于进行状态;并行表示多个任务真正同时执行。
第二,一个 HTTP 请求不等于一个线程。
请求最终由什么执行模型承载,取决于具体的服务架构,可以是进程、线程、协程,也可以是事件循环驱动的非阻塞模型。
第三,CPU 和 IO 是理解并发的关键。
CPU 密集型任务主要消耗计算资源;IO 密集型任务大量时间处于等待状态。高并发服务需要尽可能避免执行单元在 IO 等待期间浪费计算资源。
第四,1000 并发不意味着 CPU 同时执行 1000 个请求。
1000 个请求可能分别处于:
执行
等待 IO
排队
发送响应
等不同状态。
第五,系统并发能力不是由 HTTP 层单独决定的。
一个请求进入后端以后,还会继续访问:
Redis
MySQL
RPC
HTTP API
消息队列
文件系统
任何一个环节都可能成为瓶颈。
最后,可以用一句话概括本文:
后端并发,本质上是在有限计算和 IO 资源下,让大量同时存在的请求能够被高效地执行、等待、调度和完成。
理解了这一点,接下来再讨论进程、线程、协程,就会非常自然。