细说技术系统中的公平性问题-02-epoll公平性分析
1.前言
四年前,我们曾经探讨过技术系统的公平性问题,文章链接在此。最近得空又深入研究了epoll的实现,记录在这篇文章中供参考。
2.源码剖析
2.1 源码位置
epoll相关函数的头文件位于:
这个头文件位于glibc库中,具体位置如下:
在glibc库中的实现如下:
epoll_wait实际上是个系统调用,名称为:sys_epoll_wait。具体实现位于linux内核源码包中,位置如下:
sys_epoll_wait函数的核心是ep_poll函数:
ep_poll函数的定义如下:
ep_poll函数的核心是ep_send_event函数:
ep_send_event函数的实现如下:
理解整个epoll_wait函数,只需要着重理解ep_send_event这个函数即可。
上述信息总结如下图:
2.2 epoll_wait()逻辑分析
上文中说道,分析epoll_wait函数的关键是分析ep_send_events函数。
ep_send_events函数的核心是ep_scan_ready_list和ep_send_events_proc函数。
其中ep_scan_ready_list是主流程函数,ep_send_events_proc函数是回调函数。
ep_send_events_proc函数将在ep_scan_ready_list函数的某个步骤被调用。
我们先看下ep_scan_ready_list这个主流程函数:
这个函数经过简化后,核心逻辑如下:
回调函数ep_send_events_proc经过简化后,逻辑如下:
理解上述过程的关键,是理解events、rdllist、txlist这三个列表。
其中:
- events是用户提供的,用于接收套接字事件列表的数组,位于用户空间
- rdllist是内核用户存放所有就绪事件列表的数组,位于内核空间
- txlist是sys_epoll_wait系统调用用于处理就绪事件的临时列表,位于内核空间
几个list的数据流转过程如下:
- 一开始,epoll_wait系统调用的核心流程函数ep_send_event函数,将现有的就绪事件列表rdllist中的事件拷贝到临时的txlist数组,并清空rdllist
- 然后,ep_send_event函数调用核心处理函数ep_send_events_proc:
- 遍历txlist数组
- 从txlist中取出该元素(即直接pop出来了,从txlist中删除了)
- 检查是否有套接字事件
- 如果有事件
- 将当前事件拷贝到用户空间的events数组
- 如果总拷贝事件数量已经超过了用户指定的maxevents,跳出遍历循环
- 如果是LT模式,将套接字事件插入rdllist中,以备下次触发
- 将当前事件拷贝到用户空间的events数组
- 遍历txlist数组
- 最后,txlist循环处理后,还可能有剩余元素(因为有maxevents控制,可能没遍历完),将剩余元素列表插入到rdllist的头部(注意是头部,不是尾部!!!)
单看上述逻辑还是不太直观,我们举个例子就一目了然了。
2.3 epoll_wait()示例1
假设我们有4个客户端,连接到服务端,并向服务端各自发送1个字节的数据,我们看服务端发生了什么。
客户端发送完毕,服务端调用epoll_wait之前,三个数组的状态如下(1、2、3、4代表客户端TCP连接编号):
注意到,虽然用户没有调用epoll_wait,然而rdllist居然有元素!
这是因为rdllist还有另外一个写者,即操作系统软中断程序。
在服务端网卡收到数据后,软中断程序会被触发,它会通过某种方式找到用户进程的rdllist,然后将对应的套接字代表的list元素插入进去,方便用户进程遍历。
插入的顺序,其实就是客户端tcp连接发送的数据到达服务端网卡的顺序。
然后服务端调用epoll_wait,首先是将rdllist中的元素复制到临时列表txlist中,并清空rdllist。
然后调用ep_send_events_proc遍历txlist,检测套接字事件,并将有事件的套接字拷贝到用户空间的events数组中。又因为用户指定了LT模式(电平触发模式),所以这些套接字还会再次被加入到rdllist中。
处理完毕后,结果如下:
然后ep_send_events函数进行收尾处理,将txlist中剩余的元素插入到rdllist的头部,由于txlist已经为空,所以这一步并没有什么作用。
然后epoll_wait内核部分处理完毕,用户调用的epoll_wait函数返回,返回值是4,代表events数组中有4个元素,events数组中则存放了4个套接字的相关信息。
用户处理完上述4个套接字的可读事件后,这四个套接字的读缓冲区就变空了,此时用户再调用epoll_wait时,内核依然是先将rdllist中的四个元素移动到txlist,然后清空rdllist。
然后ep_send_events_proc函数检查txlist中各套接字的可读事件,发现没有新的事件后,就把相对应的元素都扔掉了,结果如下:
由于没有什么事件发生,epoll_wait就会进入休眠模式,等待新的IO事件或者超时事件将他唤醒。
2.4 epoll_wait()公平性分析1
通过上面的例子我们可以看出:
当io事件不繁忙时,即每轮epoll_wait总是能把现有套接字的可读事件处理完(在底层体现为就绪列表rdllist能保持为空)时,epoll_wait向用户层返回的套接字列表顺序,为客户端连接数据实际到达服务器网卡的顺序。客户端按照该顺序处理,即可达到公平接收客户数据的效果,无需设置其他公平性处理措施。
2.5 epoll_wait()示例2
前面的例子是比较理想的一种情况,可能不太符合实际应用场景。因此我们分析一个更为复杂的场景,这个场景是在第一个场景的基础上演进的,变化点有2:
- 让客户端持续向服务端发送数据,保证套接字上持续发生可读事件
- 将服务端epoll_wait的maxevents参数设置为2,即让内核每次只返回2个套接字事件
在客户端发送完毕,服务端调用epoll_wait之前,三个数组的状态如下(1、2、3、4代表客户端TCP连接编号):
然后服务端调用epoll_wait,首先将rdllist中的元素复制到临时列表txlist中,然后清空rdllist。
然后调用ep_send_events_proc遍历txlist,检测套接字事件,并将有事件的套接字拷贝到用户空间的events数组中。又因为用户指定了LT模式(电平触发模式),所以这些套接字还会再次被加入到rdllist中。
在本例中,由于用户指定的maxevents参数是2,所以只往用户空间拷贝了2个,txlist中会剩余2个,ep_send_events_proc函数结束后,各个列表的状态如下:
然后ep_send_events函数进行收尾处理,将txlist中剩余的元素插入到rdllist的头部,结果如下:
然后用户的epoll_wait函数返回,返回值是2,参数events数组中存放了两个元素,表明1、2号套接字上有套接字事件。
用户处理完1、2号套接字的事件后,再次调用epoll_wait函数,进入epoll_wait函数前的状态如下:
然后拷贝rdllist到txlist,清空rdllist:
然后遍历txlist数组,检测事件,结果如下:
最后内核函数收尾,将txlist插入rdllist,结果如下:
2.6 epoll_wait()公平性分析2
在上述过程中,可以得到如下结论:
当用户指定了maxevents,而实际有事件的套接字超过maxevents时,epoll_wait不会总是优先返回前几个套接字,而是会优先返回上轮epoll_wait中没有处理到的套接字,从而达到一种轮换处理的效果。
也就是说,在连接数量很多,且每个连接都很繁忙时,通过将maxevents设置得较小,通过epoll自身的轮转机制,就能实现一定的公平性效果,不会造成系统总是优先处理部分连接的问题。
但是这个机制是不完美的。
首先是当连接数量比较多,且每个连接都很繁忙时,不能保证绝对公平。例如我们将maxevents参数设置为128,那么我们遍历当前批次的128个连接的时候,总是以相同的顺序遍历的,这个顺序虽然跟建立tcp连接的顺序无关,但跟第一包数据到达的顺序有关,这个顺序,其实并不是绝对公平的。
要想实现绝对的公平,就要严格按照各个连接数据到达的顺序遍历,要实现这一点是比较困难的。因为所谓的数据到达顺序的概念,是按照业务包来区分的,一个连接的接收缓冲区中可能有很多业务数据包,每个业务数据包到达的顺序都不一样,epoll只能做到连接层面的区分,不可能做到业务包层面的区分。
要想实现业务包层面的区分,只能应用自己实现,比如以极快的速度将数据从网络中取出来,保证epoll按照数据到达顺序返回套接字事件的机制正常运行,这样应用从网络中接收数据的顺序就无限接近数据到达顺序了。
但是这样做也有很大的弊端,以极快的速度取出数据,会造成限速机制失效,如果某些客户度端连接出现问题疯狂发送数据,会极大占用网络带宽和服务器处理能力。
其次是当连接数量较小,且每个连接都很繁忙时,公平性问题会更突出。因为epoll的轮转机制无法发挥作用,服务器总是会以相同的顺序遍历各个连接接收数据。
3. epoll公平性总结
综上所述:epoll只有在各个连接的io事件不太繁忙时,才能达到一个比较理想的公平性效果,即按照数据实际到达服务端网卡的顺序处理各个客户端连接的io事件。当io事件比较繁忙,服务端无法或者不能及时处理时,epoll会优先返回上轮未处理的io事件,但这一设置并不能完美解决io繁忙场景下的公平性问题。要想实现完美的或者接近完美的公平,需要从应用程序那里下手,单靠epoll解决不了。
4.参考资料
- glibc源码:glibc-2.31.tar.gz
- linux内核源码:linux-3.10.1.tar.gz
- 参考博文:https://zhuanlan.zhihu.com/p/487497556