epoll事件驱动框架使用注意事项

栏目: 后端 · 发布时间: 6年前

内容简介:epoll事件驱动框架使用注意事项

自己一直订阅云风大哥的blog,今天看到期博文《 epoll 的一个设计问题 》,再追踪其连接看下去,着实让自己惊出一阵冷汗。真可谓不知者无畏,epoll在多线程、多进程环境下想要用好,需要避过的坑点还是挺多的。

这篇博文主要是根据Marek的博客内容进行翻译整理的。

epoll事件驱动框架使用注意事项

epoll的坑点主要是其最初设计和实现的时候,没有对多线程、多进程这种scale-up和load-balance问题进行考虑,所以随着互联网并发和流量越来越大,越来越多的epoll flag和kernel flag被引入来修补相关问题;而来epoll的用户态空间操作接口是file descriptor,内核态管理接口是file descripton,有些情况下两者不是对应关系,会导致程序的行为很奇怪。

一、多线程环境scale-up和load-balance

1.1 多线程accept

在比如HTTP/1.0的类似应用中,TCP都采用短连接的方式工作,那么accept()工作将会很重甚至成为整个系统的瓶颈所在,为了能够利用多核的并行处理,通常需要在多个执行单元(此处考虑多线程)上面并行运行accept()服务。但是:

(1) 简单的电平触发模式

这是最简单的方式,在多个线程中共享同一个bound socket file descriptor,然后各个线程在自己的服务中将其和一个epoll-event相关联,启动accept()服务。

epoll默认情况下是和select一样执行Level-Triggle,多线程模式下使用电平触发模式会有“惊群”效应,所有侦听这个套接字的线程都会被唤醒,而多个线程同时执行accept()只会有一个线程成功,其他线程全部返回EAGAIN。

(2) 边缘触发模式

虽然epoll的边缘触发工作状态下内核保证只会通知一次(一个线程),但是在边缘触发模式下,根据epoll的手册告知我们,无论是读还是写操作都要直到底层返回EAGAIN的时候本轮操作才可以结束,否则可能事件的数据没有操作完,但是在边缘触发情况下有不会继续发送信号,那么待处理数据会一直滞留下去。

所以在多线程即使使用边缘触发也会有竞争问题:线程A的epoll_wait返回后,线程A不断的调用accept()处理连接请求,当内核的accept queue队列中的请求恰好处理完时候,内核会重新将该socket置为不可读状态,以便可以重新被触发;此时如果新来了一个连接,那么另外一个线程B可能被唤醒,然后执行accept()操作,不过此时之前的线程A还需要重新再执行一次accept()以确认accept queue已经被处理完了,此时如果线程A成功accept的话,线程B就被惊醒了。

而且情况更为严重的是在考虑负载均衡的情况下,其他线程有可能被饿死,绝大多数情况的连接都被之前唤醒线程的最后一次确认性accept()给实际消费了。

(3) 新内核的解决方式

在4.5+内核版本上,epoll提供了EPOLLEXCLUSIVE标识,该标识会保证一个事件发生时候只有一个线程会被唤醒,以避免多侦听下的“惊群”问题。

如果不支持该标识,还可以使用Edge-Triggle + EPOLLONESHOT方式,启用该标识的时候epoll会在epoll_wait返回的时候自动禁止该描述符对应的事件通知,然后调用accept()消费事件,然后用户需要手动执行epoll_ctl(EPOLL_CTL_MOD)再次手动使能,等于会有一次额外epoll_ctl系统调用的开销。这种方式会让负载在多个执行单元上均衡的分布,不过任一时候只能有一个工作线程调用accept(),限制了真正并行的吞吐量。

根本性的解决方式是使用新内核的SO_REUSEPORT,可以使得应用程序在同一个端口上面创建多个socket,从而避免上面共享socket带来的种种问题。

1.2 多线程read

如果为每个线程维持一个自己的工作队列,在自己的队列中独自侦听自己的socket,那么数据的有序和完整性是很容保证的,毕竟socket只会在一个工作线程中出现并使用。不过这种做法的缺点是每个线程的工作负载可能是不均衡的,这种事先划分队列的情况可能导致某些线程的事件处理延迟,而某些线程空闲原地打转。

多个线程使用同一个队列自然是在负载均衡、处理效率上是最理想的,可以称之为”combined queue”模型,他们共享同一个epoll set,然后大家都尝试去获取active socket,不过也可能产生问题:

电平触发的惊群自然不必多说。而即使使用了EPOLLEXCLUSIVE保证每次只有一个线程获得对应读事件,但是如果第一个线程没有读完数据,那么下一次被唤醒的就可能是其他的工作线程,这种情况处理起数据就很糟糕了,毕竟在电平触发的情况下,我们不知道有多少数据可以读。

边缘触发也会导致数据完整性的竞争条件,其竞争的位置在于缓冲队列读完了,在内核将该事件置为可触发状态,此时内核又收到数据了,那么内核可能会唤醒其他线程来接收这个数据,而原本线程接收的数据就不完整了。

唯一解决的方式,就是采用EPOLLONESHOT的方式,在确认数据接收完全,本线程不需要再次使用socket的时候,再次手动触发事件使能。

二、epoll中用户态和内核态管理不一致的问题

这个问题被诟病了很久,在使用的时候必须特别的小心才可以。在通常情况下,用户态调用close()关闭file descriptor的时候,内核的file description的引用计数会递减,而当内核发现其没有再被引用的时候,会清理该file description上面的epoll事件侦听。

在通常的使用情况下,这确实没有什么问题,但是在遇到dup、fork、IO重定向等非常规操作的时候,file descriptor的生命周期和file description的生命周期就会不同,从而很容易发生问题:假设通过dup用户态就有两个fd共享同一个底层的file description,此时关闭原先注册epoll event事件的fd而不调用epoll_ctl取消事件侦听,那么底层的epoll event事件订阅就没有真正被取消;此时上层的应用程序看来现象就是即便关闭了fd,但是epoll_wait()还是会不断返回关闭了的fd的事件信息,更糟糕的是 fd已经关闭,我们无法通过epoll_ctl再次取消这个事件侦听了 ,因为fd是epoll控制底层事件的唯一入口,即便相同引用底层的其他fd也不行,对此你无能为力。

作者的伪代码很容易说明问题:

rfd, wfd = pipe()
write(wfd, "a")             # Make the "rfd" readable
epfd = epoll_create()
epoll_ctl(efpd, EPOLL_CTL_ADD, rfd, (EPOLLIN, rfd))

rfd2 = dup(rfd)
close(rfd)

r = epoll_wait(epfd, -1ms)  # still recv event!!!

所以,当你在epoll中关闭一个fd的时候,也定要事先调用epoll_ctl(EPOLL_CTL_DEL)取消事件侦听,否则你唯一能补救的就是:通过epfd强制将整个epoll_set的事件都废除掉,然后再从头重新建立事件侦听机制。

针对上面的epoll种种问题,Marek给出的建议就是:

如果可以不要在多线程中使用epoll的时候做负载均衡,因为往往实际得到的效率收获甚微;不要在多线程中共享、同时操作socket。避免fork,如果必须的话:在execv之前请关闭所有注册了epoll事件侦听的file descriptor。在调用dup/dup2/dup3和close之前,必须使用epoll_ctl(EPOLL_CTL_DEL)取消事件侦听!

关于上面的内容,前半部分其实已经在Nginx中有所涉及了,比如Nginx的accept_mutex、SO_REUSEPORT机制等,后半部分在以后使用epoll时候必须时刻提醒自己脑袋清醒,要么就使用成熟的基于epoll封装的事件库!

本文完!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

OKR工作法

OKR工作法

克里斯蒂娜•沃特克 (Christina Wodtke) / 明道团队 / 中信出版社 / 2017-9-1 / CNY 42.00

《OKR工作法》讲述了一种风靡硅谷科技企业的全新工作模式。 如何激励不同的团队一起工作,全力以赴去实现一个有挑战性的目标? 硅谷的两个年轻人汉娜和杰克,像很多人一样,在萌生了一个创意后,就走上创业之路。但是,很快他们发现好的想法远远不够,必须还有一套适合的管理方法确保梦想能实现。为了让创业团队生存下来,汉娜和杰克遭受了内心的苦苦挣扎和煎熬。他们患上“新奇事物综合症”,什么都想做,导致无......一起来看看 《OKR工作法》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试