高并发系统中,如何避免惊群效应
惊群(Thundering Herd Problem) 是并发系统中的一种典型问题,指大量等待同一事件的线程、进程或请求,在事件发生时同时被唤醒,争抢同一资源,导致系统性能急剧下降。
本质上就是: 本来只需要一个人去处理,结果所有人都一起冲过去。
举例说明
假设有1000个客户端访问缓存。缓存中没有数据(Cache Miss),于是:
1000 个请求
│
▼
查询 Redis
│
▼
Redis 没有
│
▼
1000 个请求同时访问 MySQL
结果:
- MySQL 瞬间承受1000倍压力
- 大量连接建立
- CPU 飙升
- IO 饱和
- 最终数据库可能被打垮
这就是缓存击穿导致的惊群。
Linux中最经典的惊群
例如多个进程都阻塞在:
1 | accept(sockfd, ...) |
listen socket
│
┌───────────┼───────────┐
│ │ │
Process1 Process2 Process3
│ │ │
accept() accept() accept()
│ │ │
全部睡眠等待连接
当一个新的 TCP 连接到来时:
新连接
│
▼
内核唤醒所有 accept()
结果:
Process1 醒了
Process2 醒了
Process3 醒了
...
最后:
只有一个拿到连接
其它全部:
accept()
↓
EAGAIN
↓
再次睡眠
一次连接:
唤醒100个进程
↓
99个白醒
↓
CPU白白调度99次
这就是 Linux 早期著名的 **Accept Thundering Herd(惊群)**。
epoll 中也可能出现
例如:
100 个 worker
都 epoll_wait()
监听同一个 fd
有一个事件到来:
内核:
把100个worker全部唤醒
最后:
只有一个成功 read()
其它99个:
read()
↓
EAGAIN
CPU 做了大量无效上下文切换。
Cache Stampede(缓存惊群)
互联网最常见的是:缓存过期
例如:
商品详情
TTL = 5分钟
5分钟到了:
10000 个请求同时访问
↓
缓存全部 Miss
↓
10000 个请求全部查数据库
数据库压力骤增。
解决方案:
- 热点数据永不过期(逻辑过期)
- 单飞(SingleFlight):只有一个请求回源,其它请求等待
- 请求合并(Request Coalescing)
- TTL 加随机抖动(Jitter),避免大量键同时过期
- 互斥锁(如 Redis 分布式锁)
Kubernetes 中的惊群
例如:
Deployment
1000 Pods
同时:
收到 ConfigMap 更新
于是:
1000 Pods
↓
同时 Reload
↓
同时连接数据库
↓
数据库瞬间建立1000连接
或者:
1000 Pods
↓
同时访问 Apollo
↓
Apollo 瞬间压力暴涨
Docker Swarm 中
例如:
docker service update
如果:
--update-parallelism = 0
表示:所有容器一起更新
于是:
100 个容器
↓
同时启动
↓
同时初始化
↓
同时连 Redis
↓
同时连 MySQL
↓
惊群
因此通常会设置:
--update-parallelism 2
--update-delay 10s
实现滚动更新,降低瞬时负载。
惊群常见解决方案
| 场景 | 解决方案 |
|---|---|
| accept 惊群 | SO_REUSEPORT、现代 Linux 内核优化、Nginx 的 accept_mutex(较早版本) |
| epoll 惊群 | EPOLLEXCLUSIVE(Linux 4.5+) |
| 缓存惊群 | SingleFlight、互斥锁、逻辑过期、TTL 随机化 |
| 数据库惊群 | 连接池、限流、请求合并 |
| 微服务惊群 | 熔断、限流、指数退避(Exponential Backoff)、随机退避(Jitter) |
| 容器更新惊群 | 滚动发布、分批重启、预热(Warm-up) |
本质
惊群问题的根源可以归纳为三个阶段:
多个等待者
│
▼
同一事件发生
│
▼
全部同时苏醒
│
▼
竞争同一资源
│
▼
只有极少数成功
│
▼
大量线程/进程再次休眠
因此,惊群的核心不是“请求很多”,而是大量等待者因同一个事件被同时唤醒,造成无效竞争、频繁上下文切换以及资源瞬时争抢。现代系统通常通过减少无效唤醒(如 EPOLLEXCLUSIVE)、让请求串行化或合并(如 SingleFlight)、错峰(随机退避、TTL 抖动、滚动更新)等方式来缓解这一问题。