理解并发:一个 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 资源下,让大量同时存在的请求能够被高效地执行、等待、调度和完成。

理解了这一点,接下来再讨论进程、线程、协程,就会非常自然。