Nginx 高并发原理:IO 模型与 backlog
Nginx 之所以支持高并发,核心在于它的事件驱动模型与针对性的 OS 原语使用(epoll + 边沿触发),以及合理的 backlog 处理。
1 select 模型
int select(int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select 把要监视的描述符分成 3 类:writefds、readfds、exceptfds。调用后 select 会阻塞,直到有描述符就绪(可读/可写/异常)或超时(立即返回可设置 timeout 为 null)。返回后通过遍历 fd_set 找到就绪的描述符。
- 优点:跨平台性好,几乎所有平台都支持。
- 缺点:单个进程能监视的描述符数量存在上限,Linux 下一般为 1024;即使可以通过修改宏或重编译内核提升,也会导致效率下降(每次都要对所有 fd 全量轮询)。
2 poll 模型
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
poll 用一个 pollfd 指针数组替代 select 的三个位图。pollfd 同时包含需要监视的事件和实际发生的事件,不再使用 select 的「参数-值」传递方式。poll 没有最大数量限制(但数量过大后性能同样下降)。与 select 一样,poll 返回后仍需轮询 pollfd 找出就绪的描述符。随着监视描述符数量增长,效率线性下降。
3 epoll 模型(Nginx 使用)
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
三个核心函数:
epoll_create:创建 epoll 文件描述符。参数size并非限制最大监听数,只是对内核初始分配的数据结构大小的一个建议。epoll_ctl:对指定描述符fd执行控制,event为关联的监听事件。op有三种:EPOLL_CTL_ADD(添加)、EPOLL_CTL_DEL(删除)、EPOLL_CTL_MOD(修改)。epoll_wait:等待epfd上的 IO 事件,最多返回maxevents个事件。
与 select/poll 的本质区别:select/poll 是进程被阻塞后,内核每次都将所有被监视的描述符扫描一遍;而 epoll 通过 epoll_ctl() 预先注册描述符,一旦某个描述符就绪,内核采用类似回调的机制迅速激活该文件描述符,进程调用 epoll_wait 时便能得到通知。
3.1 epoll 的优点
- 监视的描述符数量不受限制。理论上限为「可打开文件的最大数」(与内存相关,1GB 内存约 10 万左右,可用
cat /proc/sys/fs/file-max查看)。select 的数量限制对高连接数服务器是致命的。 - IO 效率不随监听 fd 数量增长而线性下降——只有就绪的 fd 才会执行回调函数,select/poll 则是全量轮询。
- 支持两种触发模式:
- 水平触发(LT):fd 状态变化后,若未处理会反复通知,编程简单,libevent 等大量使用;
- 边沿触发(ET):只告知哪些 fd 刚刚变为就绪,只说一遍,若未处理不再提示。理论上性能更高,但编码复杂——Nginx 使用边沿触发。
- 通过
mmap内存映射加速内核与用户空间的信息传递,避免无谓的内存拷贝。
epoll 文档:https://man.cx/epoll
4 backlog 指令含义
listen address[:port] [backlog=number];
backlog 限制了处于挂起状态的连接队列的最大长度,即:连接已完成三次握手、但尚未被 accept 处理的 socket 队列大小,默认值为 -1(使用内核默认值)。当一个连接请求到达而队列已满时,客户端会收到连接拒绝(Connection refused)。
参考:
- https://www.04007.cn/article/323.html
- https://www.jianshu.com/p/3ecc99ebf566
- https://www.cnxct.com/something-about-phpfpm-s-backlog/
5 生产配置建议
worker_processes auto; # 约等于 CPU 核心数
worker_rlimit_nofile 65535; # 提高 worker 可打开文件描述符上限
events {
use epoll; # 使用 epoll(Linux)
worker_connections 10240; # 每个 worker 的最大连接数
}
官方建议
worker_connections不高于worker_rlimit_nofile;总并发上限 ≈worker_processes × worker_connections(HTTP keepalive 会被复用)。