epoll惊群问题与解决
Epoll工作方式1.水平触发(LT)我觉得这里用生活中的概念来理解是很好的。假设你妈喊你吃饭你现在在打游戏先喊第一声吃饭啦儿子你没理她然后她就会喊第二声、第三声…直到你回应她为止不过后果嘛哼哼~。这就是水平触发在操作系统中或者说I/O多路转接中考虑这样一个例子我们已经把一个tcp socket添加到epoll描述符这个时候socket的另一端被写入2KB数据。此时我们调用epoll_wait,并且它会返回就绪的文件描述符说明它已经准备好进行读取操作然后调用read只读取了1KB的数据继续调用epoll_wait。epoll默认工作模式就是LT水平工作模式,当epoll检测到socket上事件就绪可以不立刻进行处理或者只处理一部分。如上⾯的例⼦,由于只读了1K数据,缓冲区中还剩1K数据,在第⼆次调⽤epoll_wait 时,epoll_wait 仍然会⽴刻返回并通知socket读事件就绪。直到缓冲区上所有的数据都被处理完,epoll_wait 才不会⽴刻返回,而是会等待数据到来。⽀持阻塞读写和⾮阻塞读写。2.边缘触发(ET)如果你妈喊你⼀次, 你没动, 你妈就不管你了。这就是边缘触发,如果我们在第一步将socket添加到epoll描述符的时候使用了EPOLLET标志epoll进行ET工作模式。当epoll检测到socket上事件就绪时必须立刻处理。如上面的例子虽然只读了1KB的数据缓冲区还剩1KB数据但是在第二次调用epoll_wait的时候就再也不会返回已经就绪的文件描述符了因此一次性处理缓冲区的所有的数据这个工作就交给了用户自己去进行处理保证一次提醒全部接受。ET的性能比LT更高(epoll_wait返回的次数少了很多)。Nginx默认采用ET模式使用。ET只支持非阻塞读写指的是当调用read或者write的时候如果没有数据则会立刻返回而不是等在这里阻塞。ET模式下需要使用非阻塞轮询的方式以保证把所有的数据读出来~EPOLL的使用场景epoll的⾼性能,是有⼀定的特定场景的.如果场景选择的不适宜,epoll的性能可能适得其反。对于多连接且多连接中只有一部分连接比较活跃时比较适合用epoll。为什么这个主要是因为epoll核心用红黑树管理连接就绪链表返回活跃的连接时间复杂度是O(1)不像select/poll需要全量扫描O(N),比如1万连接仅仅100活跃,epoll只需要处理这100个在就绪链表中的而不是要遍历10000次去寻找哪个就绪。相反如果连接少且都活跃epoll维护红黑树的开销可能比数组要高因此这种情况下不如选择select/poll要效率高。EPOLL的惊群问题1.什么是惊群问题在多进程/线程条件下当多个进程/线程阻塞在同一个 epoll 实例上等待事件时如果这个事件发生如新连接到达所有等待的进程/线程都会被唤醒但最终只有一个能成功处理该事件其他被唤醒的进程/线程会发现自己无事可做重新进入睡眠状态。在早期的Linux内核设计中当事件触发时内核会遍历所有注册到epoll实例上的进程全部唤醒这是因为当时内核并没有做精细化的进程筛选默认“宁滥勿缺”避免漏掉可能需要处理事件的进程。虽然这种设计简单但在多进程高并发场景下就造成了大量无效唤醒和资源浪费锁竞争。后来才引入EPOLLEXCLUSIVE标志来优化这个问题。2.怎么解决Linux 内核 4.5 的font stylecolor:rgb(15, 17, 21);EPOLLEXCLUSIVE/font标志可以解决这个问题。它的原理是让 epoll 在唤醒时只唤醒等待队列中的一个进程/线程而不是全部。structepoll_eventev;ev.eventsEPOLLIN;ev.data.fdlisten_fd;// 关键添加 EPOLLEXCLUSIVE 标志ev.events|EPOLLEXCLUSIVE;epoll_ctl(epfd,EPOLL_CTL_ADD,listen_fd,ev);内核级解决效率最高无需应用层额外处理自动负载均衡内核选择唤醒哪个需要 Linux 4.5 内核仅支持共享同一个 epoll 实例的场景3.有其他解决方法吗3.1各进程单独 epoll 文件描述符共享传统方案// 父进程创建 listen_fdintlisten_fdsocket(...);bind(...);listen(...);for(inti0;i4;i){if(fork()0){// 子进程创建自己的 epollintepfdepoll_create(1);epoll_ctl(epfd,EPOLL_CTL_ADD,listen_fd,ev);while(1){intnepoll_wait(epfd,events,MAX_EVENTS,-1);// 新连接到来时只有一个进程被唤醒// 不一定取决于内核实现// 实际上早期内核仍然可能惊群}}}内核会遍历所有关联了目标文件描述符的epoll实例对每个实例唤醒一个进程。比如有两个epoll实例实例A关联了进程1进程2等待队列。实例B关联了进程3和进程4。当事件触发时内核会从实例A的等待队列选择一个进程唤醒再从实例B的等待队列选择一个进程唤醒最终只有两个进程唤醒这两个进程再通过竞争锁决定哪个进程来处理这个到来的连接。这种方式通过分摊压力来减少惊群程度在一定程度上缓解但是还是不能完全解决。3.2 应用层加锁不推荐// 使用文件锁或 mutexpthread_mutex_t lock;while(1){pthread_mutex_lock(lock);intnepoll_wait(epfd,events,MAX_EVENTS,-1);// 处理事件pthread_mutex_unlock(lock);}缺点锁竞争严重完全破坏了并发性性能极差3.3 SO_REUSEPORT 端口重用多个进程可以绑定到同一个端口内核自动负载均衡新连接。intopt1;setsockopt(listen_fd,SOL_SOCKET,SO_REUSEPORT,opt,sizeof(opt));// 每个进程独立 bind、listenbind(listen_fd,...);listen(listen_fd,128);// 每个进程在自己的 epoll 中监听自己的 listen_fd// 内核直接将新连接分发到某个进程优点彻底避免惊群内核级负载均衡支持哈希、轮询等策略每个进程独立互不影响缺点需要内核 3.9 支持负载均衡策略不可自定义内核决定4.为什么选择这个解决方法这里比较推荐的就是EPOLLEXCLUSIVE和SO_REUSEPORT这两个方法。一方面是内核帮我们处理好了用户层使用比较简单另一方面是惊群问题相比于其他两种解决的要彻底。