accpet惊群和epoll惊群现象

 http://www.citi.umich.edu/projects/linux-scalability/reports/accept.html accpet惊群现象的解决(独占)
 https://github.com/torvalds/linux/commit/df0108c5da561c66c333bb46bfe3c1fc65905898 epoll惊群现象的解决


主要参考上面的两篇文章


在做负载均衡器项目中自己曾将想过多个线程进行accept。并没有出现有的线程因为accpet的惊群的事情,但在网上看过nginx的惊群处理。


首先摘抄自网上的一段accpet的一些问题?


Linux社区提出了一个解决这个问题的建议“雷鸣牧群”问题引起了他们的注意。其思想是在内核的任务结构中添加一个标志,并在__wake_up()和add_wait_queue_exclusive()函数中更改等待队列的处理。任务结构的状态变量中有一点保留为“专用”标记,accept()系统调用将负责设置此“独占”标志,并将任务添加到等待队列中。
在处理等待队列时,__wake_up()将遍历等待队列,在它运行到它的第一个“独占”任务之前,唤醒任务。它将唤醒这个任务,然后退出,剩下的队列等待。为了确保所有未被标记为独占的任务被唤醒,add_wait_queue()将由add_wait_queue_exclusive()补充,它将在等待队列的末尾添加一个独占任务,在所有非专有的服务人员之后,确保所有的“正常”任务都是先完成的。程序员将负责确保所有的独占任务都添加到add_wait_queue_exclusive()的等待队列中。
另一个解决方案是,当任务被放在等待队列中,而不是在被唤醒的时候,在花旗银行开发的时候,就不应该出现这样的想法,即决定一个任务是否应该是排他的。在等待队列中唤醒任务的进程或中断可以更好地确定它是否想要唤醒一个任务或所有任务。因此,我们在任务结构中删除了标记*,并没有在add_wait_queue()或add_wait_queue_exclusive()中处理任何特殊处理。关于上面的指导方针,我们认为实现解决方案的最简单方法是添加新的调用来补充wake_up()和wake_up_interruptible。这些新调用是wake_one()和wake_one_interruptible()。它们是#定义的宏,就像wake_up()和wake_up_interruptible()一样,并采用完全相同的参数。唯一的区别是,额外的标记被这些宏发送到__wake_up(),指示“唤醒一个”而不是默认的“唤醒所有”。通过这种方式,它取决于waker是否想要唤醒一个(例如,接受一个连接)或唤醒所有(例如,告诉每个人这个套接字是关闭的)。
对于这个“wake one”解决方案,我们检查了四种最常用的TCP套接字方法,并决定应该调用wake_up_interruptible(),它应该调用wake_one_interruptible()。在我们选择使用wake_one_interruptible()的地方,方法是所有套接字的默认方法,我们为TCP创建了一个小的函数,而不是默认的。我们这样做了,所以更改只会影响TCP代码,不影响任何其他工作套接字协议。如果在某个时候,决定了wake_one_interruptible()应该是套接字的默认值,那么新的TCP特定的方法就可以被消除。基于我们对每个套接字方法如何使用的解释,以下是我们提出的:




 state_change ..............(tcp_wakeup指针)wake_one_interruptible()


 data_ready ..........(tcp_data_ready指针)wake_one_interruptible()


 write_space .........(tcp_write_space指针)wake_one_interruptible()


 error_report(指针sock_def_error_report)…wake_up_interruptible()


请注意,accept()中使用的所有三种方法都调用wake_one_interruptible()而不是wake_up_interruptible(),当这个补丁被应用时。
尽管,有一组标记传递给__wake_up(),它在任务结构中模拟状态变量,即。,这些标志与用于任务结构的标志具有相同的位掩码。尽管任务结构中没有使用TASK_EXCLUSIVE,但它仍然被定义为__wake_up()的标志。


为什么内核2.6没有在处理accept惊群时立即处理epoll惊群,因为accept的惊群很单一,
 一个新套接字来了,本来就只能被一个进程所连接,因为socket是一对一的。 
 而epoll不是,epoll除了可以添加accept套接字意外,还可以关注io事件,
 而这个io事件是可以给多个进程同时处理的,所以如果直接将epoll也改成想accept一样
 只唤醒一个进程的话,是不合理的。  但是在linux4.5之后添加了一个EPOLLEXCLUSIVE这个标志,
 可以解决这个问题。具体查看epollwait部分代码进行理解。

epoll的惊群现象解决。想一想nginx解决的应该时epoll的惊群问题具体代码网上有就不贴出。得到锁的可以将accpet放进自己的epoll中然后。没有得到的移出去;
在思考这个问题之前,我们应该以前对前面所讲几点有所了解,即先弄清楚问题的背景,并能自己复现出来,而不仅仅只是看书或博客,然后再来看看 Nginx 的解决之道。这个顺序不应该颠倒。


首先,我们先大概梳理一下 Nginx 的网络架构,几个关键步骤为:


Nginx 主进程解析配置文件,根据 listen 指令,将监听套接字初始化到全局变量 ngx_cycle 的 listening 数组之中。此时,监听套接字的创建、绑定工作早已完成。
Nginx 主进程 fork 出多个子进程。
每个子进程在 ngx_worker_process_init 方法里依次调用各个 Nginx 模块的 init_process 钩子,其中当然也包括 NGX_EVENT_MODULE 类型的 ngx_event_core_module 模块,其 init_process 钩子为 ngx_event_process_init。
ngx_event_process_init 函数会初始化 Nginx 内部的连接池,并把 ngx_cycle 里的监听套接字数组通过连接池来获得相应的表示连接的 ngx_connection_t 数据结构,这里关于 Nginx 的连接池先略过。我们主要看 ngx_event_process_init 函数所做的另一个工作:如果在配置文件里没有开启accept_mutex锁,就通过 ngx_add_event 将所有的监听套接字添加到 epoll 中。
每一个 Nginx 子进程在执行完 ngx_worker_process_init 后,会在一个死循环中执行 ngx_process_events_and_timers,这就进入到事件处理的核心逻辑了。
在 ngx_process_events_and_timers 中,如果在配置文件里开启了 accept_mutext 锁,子进程就会去获取 accet_mutext 锁。如果获取成功,则通过 ngx_enable_accept_events 将监听套接字添加到 epoll 中,否则,不会将监听套接字添加到 epoll 中,甚至有可能会调用 ngx_disable_accept_events 将监听套接字从 epoll 中删除(如果在之前的连接中,本worker子进程已经获得过accept_mutex锁)。
ngx_process_events_and_timers 继续调用 ngx_process_events,在这个函数里面阻塞调用 epoll_wait。
至此,关于 Nginx 如何处理 fork 后的监听套接字,我们已经差不多理清楚了,当然还有一些细节略过了,比如在每个 Nginx 在获取 accept_mutex 锁前,还会根据当前负载来判断是否参与 accept_mutex 锁的争夺。


把这个过程理清了之后,Nginx 解决惊群问题的方法也就出来了,就是利用 accept_mutex 这把锁。


如果配置文件中没有开启 accept_mutex,则所有的监听套接字不管三七二十一,都加入到每子个进程的 epoll中,这样当一个新的连接来到时,所有的 worker 子进程都会惊醒。


如果配置文件中开启了 accept_mutex,则只有一个子进程会将监听套接字添加到 epoll 中,这样当一个新的连接来到时,当然就只有一个 worker 子进程会被唤醒了。


accept 不会有惊群,epoll_wait 才会。
Nginx 的 accept_mutex,并不是解决 accept 惊群问题,而是解决 epoll_wait 惊群问题。
说Nginx 解决了 epoll_wait 惊群问题,也是不对的,它只是控制是否将监听套接字加入到epoll 中。监听套接字只在一个子进程的 epoll 中,当新的连接来到时,其他子进程当然不会惊醒了。具体epoll惊群现象的解决参考上面的。是在linux后面的版本上进行的解决。具体查看上面的博文链接。

epoll:添加EPOLLEXCLUSIVE标志

目前,epoll文件描述符或epfds(从fd返回
添加到共享唤醒源的epoll_create [1]())始终是
以非排他性方式添加。这意味着当我们有多个时
epfds附加到一个共享的fd源,他们都被唤醒了。这创建
雷鸣般的群体行为。

引入一个新的'EPOLLEXCLUSIVE'标志,可以作为其中的一部分传递
'epoll_ctl()EPOLL_CTL_ADD操作期间的'event'参数。这个新的
当连接多个epfds时,标志允许独家唤醒
到一个共享的fd事件源。

这个实现遍历独占服务员的列表,并排队等候
事件发送给每个epfd,直到找到有线程的第一个服务器
通过epoll_wait()阻止它。这个想法是搜索哪些线程
空闲并准备好处理唤醒事件。因此,我们排队一个事件
到至少1个epfd,但仍可能将事件排队到所有epfds
附加到共享fd源文件。

性能测试由Madars Vitolins使用修改后的版本完成
Enduro / X。使用'EPOLLEXCLUSIVE'标志减少了长度
这个特殊的工作量从860秒降低到24秒。

示例epoll_clt文本:

EPOLLEXCLUSIVE

  为epfd文件描述符设置独占唤醒模式
  被附加到目标文件描述符fd。因此,当一个事件
  发生并且多个epfd文件描述符被附加到相同
  使用EPOLLEXCLUSIVE的目标文件,一个或多个epfds将收到一个
  带有epoll_wait(2)的事件。在这种情况下的默认值(何时
  EPOLLEXCLUSIVE未设置)适用于所有epfds接收事件。
  EPOLLEXCLUSIVE只能用EPOLL_CTL_ADD指定。






 

你可能感兴趣的:(linux学习)