1. 引言
本文来自 Marek’s 博客中 I/O multiplexing part 系列之三和四,原文一共有四篇,主要讲 Linux 上 IO 多路复用的一些问题,本文加入了我的一些个人理解,如有不对之处敬请指出。原文链接如下:
- The history of the Select(2) syscall
- Select(2) is fundamentally broken
- Epoll is fundamentally broken 1/2
- Epoll is fundamentally broken 2/2
2. 脉络
系列三和系列四分别讲 epoll(2) 存在的两个不同的问题:
- 系列三主要讲 epoll 的多线程扩展性的问题
- 系列四主要讲 epoll 所注册的 fd (file descriptor) 和实际内核中控制的结构 file description 拥有不同的生命周期
我们在此也按照该顺序进行阐述。
3 epoll 多线程扩展性
epoll 的多线程扩展性的问题主要体现在做多核之间负载均衡上,有两个典型的场景:
- 一个 TCP 服务器,对同一个 listen fd 在多个 CPU 上调用
accept(2)
系统调用 - 大量 TCP 连接调用
read(2)
系统调用上
3.1 特定 TCP listen fd 的 accept(2) 的问题
一个典型的场景是一个需要处理大量短连接的 HTTP 1.0 服务器,由于需要 accept() 大量的 TCP 建连请求,所以希望把这些 accept() 分发到不同的 CPU 上来处理,以充分利用多 CPU 的能力。
这在实际生产环境是存在的, Tom Herbert 报告有应用需要处理每秒 4 万个建连请求;当有这么多请求的时候,很显然,将其分散到不同的 CPU 上是合理的。
然后实际上,事情并没有这么简单,直到 Linux 4.5 内核,都无法通过 epoll(2) 把这些请求水平扩展到其他 CPU 上。下面我们来看看 epoll 的两种模式 LT(level trigger, 水平触发) 和 ET(edge trigger, 边缘触发) 在处理这种情况下的问题。
3.1.1 水平触发的问题:不必要的唤醒
一个愚蠢的做法是是将同一个 epoll fd 放到不同的线程上来 epoll_wait(),这样做显然行不通,同样,将同一个用于 accept 的 fd 加到不同的线程中的 epoll fd 中也行不通。
这是因为 epoll 的水平触发模式和 select(2)
一样存在 “惊群效应”,在不加特殊标志的水平触发模式下,当一个新建连接请求过来时,所有的 worker 线程都都会被唤醒,下面是一个这种 case 的例子:
1. 内核:收到一个新建连接的请求
2. 内核:由于 "惊群效应&#