Nignx如何解决“惊群”问题
当客户端发起连接后,由于所有的worker子进程都监听着同一个端口,内核协议栈在检测到客户端连接后,会激活所有休眠的worker子进程,最终只会有一个子进程成功建立新连接,其他子进程都会accept失败。
Accept失败的子进程是不应该被内核唤醒的,因为它们被唤醒的操作是多余的,占用本不应该被占用的系统资源,引起不必要的进程上下文切换,增加了系统开销,同时也影响了客户端连接的时延。
“惊群”问题是多个子进程同时监听同一个端口引起的,因此解决的方法是同一时刻只让一个子进程监听服务器端口,这样新连接事件只会唤醒唯一正在监听端口的子进程。
因此“惊群”问题通过非阻塞的accept锁来实现进程互斥accept(),其原理是:在worker进程主循环中非阻塞trylock获取accept锁,如果trylock成功,则此进程把监听端口对应的fd通过epoll_ctl()加入到本进程自由的epoll事件集;如果trylock失败,则把监听fd从本进程对应的epoll事件集中清除。
Nginx实现了两套互斥锁:基于原子操作和信号量实现的互斥锁、基于文件锁封装的互斥锁。考虑到锁的平台可移植性和通用性,改造twemproxy选择时,选择文件锁实现。
如果获取accept锁成功的进程占用锁时间过长,那么其他空闲进程在这段时间内无法获取到锁,从而无法接受新的连接。最终造成客户端连接相应时间变长,qps低,同时引起负载严重不均衡。为了解决该问题,选择通过post事件队列方式来提高性能,trylock获取到accept锁成功的进程,其工作流程如下:
-
trylock获取accept锁成功
-
通过epoll_wait获取所有的事件信息,把监听到的所有accept事件信息加入accept_post列表,把已有连接触发的读写事件信息加入read_write_post列表。
-
执行accept_post列表中的所有事件
-
Unlock锁
-
执行read_write_post列表中的事件。
Worker进程主循环工作流程图如下:
image从上图可以看出,worker进程借助epoll来实现网络异步收发,客户端连接twemproxy的时候,worker进程循环检测客户端的各种网络事件和后端memcached的网络事件,并进行相应的处理。