Linux 惊群效应之 Nginx 解决方案34
发表时间:2018-12-22 12:54 前言因为项目涉及到 Nginx 一些公共模块的使用,而且也想对惊群效应有个深入的了解,在整理了网上资料以及实践后,记录成文章以便大家复习巩固。 结论
惊群效应是什么惊群效应(thundering herd)是指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应。 惊群效应消耗了什么
Linux 解决方案之 AcceptLinux 2.6 版本之前,监听同一个 socket 的进程会挂在同一个等待队列上,当请求到来时,会唤醒所有等待的进程。 Linux 2.6 版本之后,通过引入一个标记位 WQ_FLAG_EXCLUSIVE,解决掉了 accept 惊群效应。 具体分析会在代码注释里面,accept代码实现片段如下: 唤醒阻塞的 accept 代码片段如下: Linux 解决方案之 Epoll在使用 select、poll、epoll、kqueue 等 IO 复用时,多进程(线程)处理链接更加复杂。
epoll_create 在 fork 之前创建与 accept 惊群的原因类似,当有事件发生时,等待同一个文件描述符的所有进程(线程)都将被唤醒,而且解决思路和 accept 一致。 为什么需要全部唤醒?因为内核不知道,你是否在等待文件描述符来调用 accept() 函数,还是做其他事情(信号处理,定时事件)。 此种情况惊群效应已经被解决。 epoll_create 在 fork 之后创建epoll_create 在 fork 之前创建的话,所有进程共享一个 epoll 红黑数。 当连接到来时,我们需要选择一个进程来 accept,这个时候,任何一个 accept 都是可以的。当连接建立以后,后续的读写事件,却与进程有了关联。一个请求与 a 进程建立连接后,后续的读写也应该由 a 进程来做。 当读写事件发生时,应该通知哪个进程呢?Epoll 并不知道,因此,事件有可能错误通知另一个进程,这是不对的。所以一般在每个进程(线程)里面会再次创建一个 epoll 事件循环机制,每个进程的读写事件只注册在自己进程的 epoll 种。 我们知道 epoll 对惊群效应的修复,是建立在共享在同一个 epoll 结构上的。epoll_create 在 fork 之后执行,每个进程有单独的 epoll 红黑树,等待队列,ready 事件列表。因此,惊群效应再次出现了。有时候唤醒所有进程,有时候唤醒部分进程,可能是因为事件已经被某些进程处理掉了,因此不用在通知另外还未通知到的进程了。 Nginx 解决方案之锁的设计首先我们要知道在用户空间进程间锁实现的原理,起始原理很简单,就是能弄一个让所有进程共享的东西,比如 mmap 的内存,比如文件,然后通过这个东西来控制进程的互斥。 Nginx 中使用的锁是自己来实现的,这里锁的实现分为两种情况,一种是支持原子操作的情况,也就是由 NGX_HAVE_ATOMIC_OPS 这个宏来进行控制的,一种是不支持原子操作,这是是使用文件锁来实现。 锁结构体如果支持原子操作,则我们可以直接使用 mmap,然后 lock 就保存 mmap 的内存区域的地址 原子锁创建原子锁获取TryLock,它是非阻塞的,也就是说它会尝试的获得锁,如果没有获得的话,它会直接返回错误。 原子锁实现 如果系统库不支持这个指令的话,Nginx 自己还用汇编实现了一个。 原子锁释放Unlock 比较简单,和当前进程 id 比较,如果相等,就把 lock 改为 0,说明放弃这个锁。
Nginx 解决方案之惊群效应变量分析ngx_use_accept_mutex 这个变量,如果有这个变量,说明 Nginx 有必要使用 accept 互斥体,这个变量的初始化在 ngx_event_process_init 中。
ngx_accept_disabled,这个变量是一个阈值,如果大于 0,说明当前的进程处理的连接过多。 是否使用锁NGX_POST_EVENTS 标记,设置了这个标记就说明当 socket 有数据被唤醒时,我们并不会马上 accept 或者说读取,而是将这个事件保存起来,然后当我们释放锁之后,才会进行 accept 或者读取这个句柄。 如果没有设置 NGX_POST_EVENTS 标记的话,Nginx 会立即 Accept 或者读取句柄 定时器,这里如果 Nginx 没有获得锁,并不会马上再去获得锁,而是设置定时器,然后在 epoll 休眠(如果没有其他的东西唤醒)。此时如果有连接到达,当前休眠进程会被提前唤醒,然后立即 accept。否则,休眠 ngx_accept_mutex_delay时间,然后继续 tryLock。 获取锁来解决惊群如上代码,当一个连接来的时候,此时每个进程的 epoll 事件列表里面都是有该 fd 的。抢到该连接的进程先释放锁,在 accept。没有抢到的进程把该 fd 从事件列表里面移除,不必再调用 accept,造成资源浪费。 同时由于锁的控制(以及获得锁的定时器),每个进程都能相对公平的 accept 句柄,也就是比较好的解决了子进程负载均衡。 上一篇45岁以后的人生
|
|