面试时被问到:是否了解 Nginx,它使用的 epoll 模式和其他的相比有什么优势?
直接被问住,实际生产中配过不少的 Nginx,各种 rewrite、regex、正反向代理、php、fast-cgi、限流、证书、jwt、cors;epoll 只大概有印象是下面这行:
1events {
2 use epoll;
3 worker_connections 1024;
4}
实在是汗颜啊,所以得仔细研究一下这个 epoll。
首先扔概念
IO多路复用:
多路是指网络连接,复用指的是同一个线程。
IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件描述符;
一旦某个描述符就绪(一般是读就绪或者写就绪),就能够通知应用程序进行相应的读写操作;
没有文件描述符就绪时会阻塞应用程序,交出cpu。
那么IO多路复用的实现方法有三种: select、poll、epoll
select、poll、epoll本质上都是同步I/O,用户进程(这里就是Nginx)负责读写(从内核空间拷贝到用户空间),读写过程中,用户进程是阻塞的。
select:
-
查询 fd_set 中,是否有就绪的 fd,可以设定一个超时时间,当有 fd (File descripter) 就绪或超时返回;
-
fd_set 是一个位集合,大小是在编译内核时的常量,默认大小为 1024
-
特点:
-
连接数限制,fd_set 可表示的 fd 数量太小了;
-
线性扫描:判断 fd 是否就绪,需要遍历一边 fd_set;
-
数据复制:从内核空间拷贝到用户空间,复制连接就绪状态信息
poll:
-
解决了连接数限制:
-
poll 中将 select 中的 fd_set 替换成了一个 pollfd 数组
-
解决 fd 数量过小的问题
-
数据复制:从内核空间拷贝到用户空间,复制连接就绪状态信息
epoll:event 事件驱动
-
事件机制:避免线性扫描
-
为每个 fd,注册一个监听事件
-
fd 变更为就绪时,将 fd 添加到就绪链表
-
fd 数量:无限制(OS 级别的限制,单个进程能打开多少个 fd)
区别在于:
epoll较灵活,如果有一百万个链接状态同时保持,但是在某个时刻,只有几百个链接是活跃的。epoll的处理是通过epoll_create()创建对象,epoll_ctl()收集所有的套接字添加到epoll对象,epoll_wait()收集所有发生事件也就是所谓的活跃的链接,并收集到一个List链表中,这样只需要遍历这些List链表里的数据,而不用遍历一百万个链接。 而后者select poll则是每次收集事件时,将这一百万个链接都传给操作系统,再由操作系统内核上判断某些链接产生了事件,造成了巨大的资源浪费(大批量的不同态内存复制)。
为什么epoll效率比较高:
epoll 在epoll_create 时,就已经建立好了一个文件描述符对应 epoll 对象,同时,在内核 cache 里建立了一个红黑树,用于存储后续epoll_ctl传来的socket连接。再同时,又建立了一个list链表,用于存储准备就绪的事件。 等到 epoll_wait() 调用的时候,只需要观察list链表里有没有数据就可以,有数据就返回,没数据就sleep。等到timeout后,即使没数据,也返回了。所以 epoll_wait 会非常高效
三者的比较:
select | poll | epoll | |
---|---|---|---|
数据结构 | bitmap | 数组 | 红黑树 |
最大连接数 | 1024 | 无上限 | 无上限 |
fd拷贝 | 每次调用select拷贝 | 每次调用poll拷贝 | fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝 |
工作效率 | 轮询:O(n) | 轮询:O(n) | 回调:O(1) |
Nginx 的并发处理能力
关于 Nginx 的并发处理能力:
- 并发连接数,一般优化后,峰值能保持在 1~3w 左右。(内存和 CPU 核心数不同,会有进一步优化空间)
- Nginx 的最大连接数:Worker 进程数量 x 单个 Worker 进程的最大连接数