细说技术系统中的公平性问题-02-epoll公平性分析

细说技术系统中的公平性问题-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中,以备下次触发
  • 最后,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

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注