Twisted中使用select/poll与epoll反应器的注意事项

101

根据我所阅读的和经历过的(基于Tornado的应用),我相信ePoll是选择和轮询网络的自然替代品,尤其是在Twisted中。这让我有点担心,更好的技术或方法很少不附带代价。

阅读了几十个epoll与其他替代方案之间的比较后可以发现,epoll在速度和可扩展性上显然是最佳选择,尤其是它具有线性扩展的特点,这非常出色。话虽如此,那处理器和内存利用率方面呢?epoll是否仍是最佳选择呢?

2个回答

200

对于非常少量的套接字(当然,这取决于您的硬件,但我们说的是大约10个或更少的数量),select 可以在内存使用和运行速度方面胜过 epoll 。 当然,对于这么少量的套接字,两种机制都非常快,所以在绝大多数情况下,您并不关心这种差异。

不过,需要澄清的是,selectepoll 都呈线性扩展。 不同之处在于,用户空间面向 API 的复杂性基于不同的内容。 在 select 调用的成本大致随着您传递给它的最高编号文件描述符的值而变化。 如果您选择单个 fd,100,则该成本大约是选择单个 fd,50 的两倍。将更低的 fd 添加到最高 fd 以下并非完全免费,因此在实践中要比这更复杂一些,但对于大多数实现来说,这是一个很好的第一近似。

epoll 的成本接近于实际上存在事件的文件描述符数。 如果您监视200个文件描述符,但只有其中的100个存在事件,则(非常粗略地)您只支付那100个活动文件描述符的成本。 这就是 epoll 倾向于在 select 上提供其主要优势之一的地方。 如果有一千个客户端大多处于空闲状态,那么当您使用 select 时,您仍然需要支付它们全部的成本。 然而,在使用 epoll 时,就像只有几个客户端- 您只支付每次活动的客户端成本。

所有这些都意味着对于大多数工作负载,epoll会导致更少的CPU使用率。 至于内存使用情况,则有点难以抉择。select确实设法以高度紧凑的方式表示所有必要信息(每个文件描述符一个比特)。而select中FD_SETSIZE(通常为1024)对可以使用多少文件描述符的限制意味着你使用select时永远不会超过每个fd集所需的128字节(读、写、异常)。相较于那384字节的最大值,epoll有一点笨重。每个文件描述符由多字节结构表示。但是,就绝对值而言,它仍然不会使用太多内存。你可以在几十千字节内表示大量文件描述符(我认为每1000个文件描述符大约需要20k)。您还可以加入这样一个事实,即如果您只想监视一个文件描述符,但其值恰好为1024,则您必须花费select的全部384字节,而对于epoll,您只需花费20字节。尽管如此,这些数字都很小,因此并没有太大区别。另外,还有epoll的另一个好处,也许您已经知道了,它不限于FD_SETSIZE文件描述符。你可以使用它来监视你有的所有文件描述符。如果您只有一个文件描述符,但其值大于FD_SETSIZEepoll也可以处理,但是select不能。

最近我偶然发现了 epoll 相对于 select 或者 poll 的一个小缺陷。虽然这三个 API 都不支持普通文件(即文件系统上的文件),但是 selectpoll 将这种不支持表示为这些描述符一直可读且一直可写。这使它们无法用于任何有意义的非阻塞文件系统 I/O,即使用 selectpoll 的程序,如果遇到来自文件系统的文件描述符,它至少会继续运行(或者如果失败,也不会是因为 selectpoll),尽管它可能不会具有最佳性能。

另一方面,当 epoll 被要求监视这样的文件描述符时,将会快速失败并显示错误(显然是 EPERM)。严格来说,这几乎算不上错误。它只是以明确的方式表明了它不支持该文件描述符。正常情况下,我会赞扬明确的失败条件,但这个条件是未记录的(据我所知),导致应用程序完全崩溃,而不仅仅是可能性能下降的应用程序。

实际上,我只在与标准 I/O 交互时遇到过这个问题。用户可能会将 stdin 或 stdout 重定向到普通文件。此前,stdin 和 stdout 是一个管道(epoll 支持得很好),但现在它变成了普通文件,epoll 会大声失败并且导致应用程序崩溃。


非常好的回答。考虑为完整性明确 poll 的行为? - quark
7
关于从普通文件中读取数据的行为,我的看法是:我通常更喜欢直接失败而非性能下降。原因是这种情况在开发过程中更容易被检测到,因此可以正确地解决它(例如通过采用其他实际文件的I/O方法)。当然,你的情况可能不同:如果没有明显的减速,则失败并不更好。但只发生在特定情况下的剧烈减速,在开发过程中很难捕捉到,这会在实际部署时成为一颗定时炸弹。 - quark
1
刚刚完全阅读了您的编辑。在某种程度上,我同意 epoll 不模仿其前辈可能是不正确的,但我可以想象实现 EPERM 错误的开发人员认为:“仅仅因为它一直存在问题,并不意味着破坏我的代码也是正确的。” 另一个反驳的观点是,我是一个防御性程序员,超过 1+1 的任何东西都是可疑的,我以这样的方式编码,以允许优雅的失败。让内核触发一个出乎意料的错误并不好或体贴。 - David
1
@Jean-Paul,你能否也加上一些关于kqueue的解释呢? - Good Person
除了性能问题,从man select中是否存在问题?Linux内核没有固定的限制,但glibc实现将fd_set定义为固定大小类型,FD_SETSIZE定义为1024,并且FD_*()宏根据该限制操作。要监视大于1023的文件描述符,请改用poll(2)。在CentOS 7上,我已经看到过自己的代码因为内核返回>1023的文件句柄而失败了select(),我目前正在研究一个类似的问题,这可能是Twisted遇到的同样问题。 - Paul D Smith
也许?我不确定我完全理解这个问题。确实,select()在高编号的文件描述符上会失败。这可能是使用其他机制的原因之一。如果这不是一个充分的答案,那么提出一个新问题可能是一个好主意。 - Jean-Paul Calderone

4
在我们公司的测试中,epoll()相对于select()有一个问题:单个成本比较高。当尝试使用超时从网络读取时,创建epoll_fd(而不是FD_SET),并将fd添加到epoll_fd中,比创建FD_SET更昂贵(后者是一种简单的malloc)。
如同前面的答案所述,在进程中的FD数变得更多时,select()的成本会更高,但在我们的测试中,即使fd的值在10,000左右,select仍然表现优异。这些情况下,线程只等待一个fd,并试图克服使用阻塞线程模型时网络读取和网络写入不会超时的事实。当然,与非阻塞反应器系统相比,阻塞线程模型的性能较低,但有时需要与特定的遗留代码库集成。
在高性能应用程序中,这种用例很少出现,因为反应器模型不需要每次创建新的epoll_fd。对于epoll_fd长时间存在的模型(这显然是任何高性能服务器设计的首选),epoll在所有方面都是明确的赢家。

5
如果您使用的文件描述符值在10k+范围内,甚至不能使用select()函数 - 除非您重新编译系统的一半以更改FD_SETSIZE的值 - 因此我想知道这种策略到底起作用了吗?对于您描述的情况,我可能会考虑使用poll()函数,它更像select()函数,而不像epoll()函数,并且可以避免FD_SETSIZE的限制。 - Jean-Paul Calderone
如果您的文件描述符值在10K范围内,可以使用select(),因为您可以malloc()一个FD_SET。实际上,由于FD_SETSIZE是编译时确定的,而实际fd限制是在运行时确定的,所以FD_SET的唯一安全用法是检查文件描述符的数量是否超过FD_SET的大小,并在FD_SET太小的情况下执行malloc(或道德等效物)。当我在客户生产环境中看到这个问题时,我感到震惊。在编写套接字方面经过20年的编程后,我所编写的所有代码 - 以及网络上的大多数教程 - 都是不安全的。 - Brian Bulkowski
5
据我所知,在任何流行的平台上,这种说法都是不正确的。 FD_SETSIZE 是在编译 C 库时设置的编译时常量。如果在构建应用程序时将其定义为不同的值,则您的应用程序和 C 库将发生不一致,从而导致问题。如果您有参考资料声称重新定义 FD_SETSIZE 是安全的,我很想看看它们。 - Jean-Paul Calderone

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接