跳转至

小心使用 epoll 负载均衡

记录一个epoll使用问题。在一个新项目写了一个协议转换的小功能。把收到的RTP包封装客户的头再转发出去,用的epoll来收RTP。在公司环境测试正常。可是到了客户现场出现奇怪的现象,每次RTP流只能转发前面几秒的流,后面就不转发了。关闭/打开监听,又是只转发前面几秒。

解决

通过加日志和打印堆栈,确定是卡在了epoll_wait那里。即使有包过来,也没有往下走。 在关闭和打开监听时,会执行EPOLL_CTL_DEL和EPOLL_CTL_ADD,重新创建了socket fd。后来修改了Epoll事件触发方式,从边缘触发+阻塞,改成水平+非阻塞后解决。在另一个项目,水平触发+阻塞也正常工作。所以水平模式是关键。

探究

参考了epoll相关帖子后总结,epoll起初是为多路复用设计的,而不是为了负载均衡,也就是单线程用epoll是比较好的。多线程监听同一个epoll fd时,会出现惊群, 多个线程的epoll_wait被唤醒,这不仅浪费时间片,而且ET模式下可能导致所有线程饿死。后来内核优化epoll, 加了EPOLLEXCLUSIVE和EPOLLONESHOT,作用分别是 * EPOLLEXCLUSIVE 只唤醒一个线程 * EPOLLONESHOT 只提醒一次,在处理完事件后,调用epoll_ctl MOD来重新启用时间提醒。注意这个是针对socket fd的,只有触发事件的fd会被阻止触发新事件。 而其他添加到epoll fd的socket fd不受影响。

accept 扩容

但是这两个新标志导致永远只有一个线程调用accept, 所以效率低。 不过accept也有新标志SO_REUSEPORT,可以创建多个socket fd, 这样就能添加多个listen fd到epoll fd里。即使一个listen fd被屏蔽触事件,其他线程也能收到事件并处理连接。

read 扩容

这个收银员的例子的很有意思youtube,超市的收银台的客户排在不同队列,一旦队列第一个人东西很多,那这个队列后面的人都要等。而医院人工挂号,银行叫号系统,所有人一个队列,即使一个人卡住了,其他窗口还能继续工作。作为消费者,谁都不愿意被卡住,但超市设置多队列时,比如N个队列,而自己只能选择一个,那么大概率自己会觉得比别人慢,因为比别人快的几率只有1/N。

而处理网络任务时,如果每个线程只服务一个连接或者固定的几个连接时(超市排队), 一旦某个线程卡住,则这个线程负责的所有连接都会卡住。其他线程即使很闲也帮不上忙,因此提高IO吞吐量的最好的方式是将所有连接合并成一个队列(银行叫号)。 即Epoll只负责将所有事件添加到一个队列,工作线程负责总队列里取出事件,并处理。

那读一次还是读完?水平触发可以只读一次,但加了EPOLLONESHOT时,最好读完。另外ET模式必须要读完。

注意这里讨论的TCP,UDP就更简单些。

Reference

https://idea.popcount.org/2017-02-20-epoll-is-fundamentally-broken-12/

评论