Nginx使用多进程的方法进行任务处理,每个worker进程只有一个线程,单线程循环处理全部监听的事件。本文重点分析一下多进程间的负载均衡问题以及Nginx多进程事件处理流程,方便大家自己写程序的时候借鉴。

一、监听建立流程

整个建立监听socket到accept的过程如下图:

说明:

1.main里面调用ngx_init_cycle(src/core/ngx_cycle.c),ngx_init_cycle里面完成很多基本的配置,如文件,共享内存,socket等。

2.上图左上角是ngx_init_cycle里面调用的ngx_open_listening_sockets(src/core/ngx_connection.c)主要完成的工作,包括基本的创建socket,setsockopt,bind和listen等。

3.然后是正常的子进程生成过程。在每个子worker进程的ngx_worker_process_cycle中,在调用ngx_worker_process_init里面调用各模块的初始化操作init_process。一epoll module为例,这里调用ngx_event_process_init,里面初始化多个NGX_EVENT_MODULE类型的module.NGX_EVENT_MODULE类型的只有ngx_event_core_module和ngx_epoll_module。前一个module的actions部分为空。ngx_epoll_module里面的init函数就是ngx_epoll_init。ngx_epoll_init函数主要完成epoll部分相关的初始化,包括epoll_create,设置ngx_event_actions等。

4.初始化完ngx_epoll_module,继续ngx_event_process_init,然后循环设置每个listening socket的read handler为ngx_event_accept.最后将每个listening socket的READ事件添加到epoll进行等待。

5.ngx_event_process_init初始化完成后,每个worker process开始循环处理events&timers。最终调用的是epoll_wait。由于之前listening socket以及加入到epoll,所以如果监听字有read消息,那么久调用rev->handler进行处理,监听字的handler之前已经设置为ngx_event_accept。ngx_event_accept主要是调用accept函数来接受新的客户端套接字client socket。

下面是监听字的处理函数ngx_event_accept流程图:

说明:

1.前半部分主要是通过accept接受新连接字,生成并设置相关结构,然后添加到epoll中。

2.后半部分调用connection中的listening对应的handler,即ngx_xxx_init_connection,其中xxx可以是mail,http和stream。顾名思义,该函数主要是做新的accepted连接字的初始化工作。上图以http module为例,初始化设置了连接字的read handler等。

二、负载均衡问题

Nginx里面通过一个变量ngx_accept_disabled来实施进程间获取客户端连接请求的负载均衡策略。ngx_accept_disabled使用流程图:

说明:

1.ngx_process_events_and_timers函数中,通过ngx_accept_disabled的正负判断当前进程负载高低(大于0,高负载;小于0,低负载)。如果低负载时,不做处理,进程去申请accept锁,监听并接受新的连接。

2.如果是高负载时,ngx_accept_disabled就发挥作用了。这时,不去申请accept锁,让出监听和接受新连接的机会。同时ngx_accept_disabled减1,表示通过让出一次accept申请的机会,该进程的负载将会稍微减轻,直到ngx_accept_disabled最后小于0,重新进入低负载的状态,开始新的accept锁竞争。

参考链接:http://www.jb51.net/article/52177.htm

三、“惊群”问题

“惊群”问题:多个进程同时监听一个套接字,当有新连接到来时,会同时唤醒全部进程,但只能有一个进程与客户端连接成功,造成资源的浪费。

Nginx通过进程间共享互斥锁ngx_accept_mutex来控制多个worker进程对公共监听套接字的互斥访问,获取锁后调用accept取出与客户端已经建立的连接加入epoll,然后释放互斥锁。

Nginx处理流程示意图:

说明:

1.ngx_accept_disabled作为单个进程负载较高(最大允许连接数的7/8)的标记,计算公式:

ngx_accept_disabled = ngx_cycle->connection_n/8 - ngx_cycle->free_connection_n;

即进程可用连接数free_connection_n小于总连接数connection_n的1/8时ngx_accept_disabled大于0;否则小于0.或者说ngx_accept_disabled小于0时,表示可用连接数较多,负载较低;ngx_accept_disabled大于0时,说明可用连接数较少,负载较高。

2.如果进程负载较低时,即ngx_accept_disabled 小于0,进程允许竞争accept锁。

3.如果进程负载较高时,放弃竞争accept锁,同时ngx_accept_disabled 减1,即认为由于让出一次竞争accept锁的机会,负载稍微减轻(ngx_accept_disabled 小于0可用)。由于负载较高时(ngx_accept_disabled >0)只是将ngx_accept_disabled 减1,这里不申请accept锁,所以后续的accept函数会遭遇“惊群”问题,返回错误errno=EAGAIN,直接返回(个人觉得这里有改进的空间,见补充部分)。

ngx_process_events_and_timers函数部分代码如下:

 if (ngx_use_accept_mutex) {
if (ngx_accept_disabled > ) {
ngx_accept_disabled--; } else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
} if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS; } else {
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
}

4.如果竞争加锁失败(6-7行),直接返回,返回到ngx_worker_process_cycle的for循环里面,此次不参与事件处理,进行下一次循环。

5.如果竞争加锁成功,设置NGX_POST_EVENTS标记,表示将事件先放入队列中,稍后处理,优先释放ngx_accept_mutex,防止单个进程过多占用锁时间,影响事件处理效率。ngx_epoll_process_events函数有如下部分(写事件wev部分也一样):

 if (flags & NGX_POST_EVENTS) {
queue = rev->accept ? &ngx_posted_accept_events
: &ngx_posted_events; ngx_post_event(rev, queue);//先将event放入队列,稍后处理 } else {
rev->handler(rev);
}

6.从ngx_epoll_process_events返回ngx_process_events_and_timers,然后是处理accept事件(下面代码10行);处理完accept事件,马上释放锁(下面代码13-15行),给其他进程机会去监听连接事件。最后处理一般的连接事件。

 delta = ngx_current_msec;

 (void) ngx_process_events(cycle, timer, flags);

 delta = ngx_current_msec - delta;

 ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, ,
"timer delta: %M", delta); ngx_event_process_posted(cycle, &ngx_posted_accept_events);//这里处理ngx_process_events 里面post的accept事件 //处理完accept事件,马上释放锁
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
} //在处理一般的connection事件之前,先处理超时。
if (delta) {
ngx_event_expire_timers();
} //处理普通的connection事件请求
ngx_event_process_posted(cycle, &ngx_posted_events);

7.在处理accept事件时,handler是ngx_event_accept(src/event/ngx_event_accept.c),在这个函数里面,每accept一个新的连接,就更新ngx_accept_disabled。

 do {
...
//接受新连接
accept();
...
//更新ngx_accept_disabled
ngx_accept_disabled = ngx_cycle->connection_n /
- ngx_cycle->free_connection_n; ... }while(ev->available)

补充:

ngx_accept_disabled 减1这条路径很明显没有申请accept锁,所以后面的epoll_wait和accept函数会出现“惊群”问题。建议按如下图改进:

说明:

添加红色框步骤,在负载过高时,ngx_accept_disabled 减1进行均衡操作同时,将accept事件从当前进程epoll中清除。这样epoll当前循环只处理自己的普通connection事件。当然,左侧路径可能执行多次,ngx_disable_accept_events操作只需要执行一次即可。

如果过了一段时间,该进程负载降低,进入右侧路径,在申请accept锁的函数中ngx_trylock_accept_mutex中,申请加锁成功后,会调用ngx_enable_accept_events将accept事件再次加入到epoll中,这样就可以监听accept事件和普通connection事件了。

以上补充部分为个人理解,有错误之处,欢迎指正。

四、多进程(每个进程单线程)高效的原因

一点思考:

1.master/worker多进程模式,保证了系统的稳定。master对多个worker子进程和其他子进程的管理比较方便。由于一般worker进程数与cpu内核数一致,所以不存在大量的子进程生成和管理任务,避免了大量子进程的数据IPC共享开销和切换竞争开销。各worker进程之间也只是重复拷贝了监听字,除了父子进程间传递控制消息,基本没有IPC需求。

2.每个worker单线程,不存在大量线程的生成和同步开销。

以上两个方面都使Nginx避免了过多的同步、竞争、切换和IPC数据传递,即尽可能把cpu从不必要的计算开销中解放出来,只专注于业务计算和流程处理。

解放了CPU之后,就是内存的高效操作了。像cache_manager_process,内存池ngx_pool_t等等。还有可以设置进程的affinity来绑定cpu单个内核等。

这样的模型更简单,大连接量扩展性更好。

“伟大的东西,总是简单的”,此言不虚。

注:引用本人文章请注明出处,谢谢。

Nginx 多进程连接请求/事件分发流程分析的更多相关文章

  1. Kafka处理请求的全流程分析

    大家好,我是 yes. 这是我的第三篇Kafka源码分析文章,前两篇讲了日志段的读写和二分算法在kafka索引上的应用 今天来讲讲 Kafka Broker端处理请求的全流程,剖析下底层的网络通信是如 ...

  2. Android事件分发流程总结

    Action_Down 当按下一个控件,调用流程是Activity.dispatchTouchEvent -> ViewGroup.dispatchTouchEvent , 1.ViewGrou ...

  3. javascript事件执行流程分析

    我一直想搞清楚事件在DOM中的传播方式,今天经高人指点终于明白一二.首先扒了一张图: 事件捕获过程:当我们点击TEXT时,首先是window->document->body->div ...

  4. android touch事件分发流程

    韩梦飞沙  韩亚飞  313134555@qq.com  yue31313  han_meng_fei_sha 三个方法:分发触摸事件dispatchTouchEvent.在触摸事件的时候onTouc ...

  5. 从源码的角度分析ViewGruop的事件分发

    从源码的角度分析ViewGruop的事件分发. 首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View ...

  6. Android查缺补漏(View篇)--事件分发机制源码分析

    在上一篇博文中分析了事件分发的流程及规则,本篇会从源码的角度更进一步理解事件分发机制的原理,如果对事件分发规则还不太清楚的童鞋,建议先看一下上一篇博文 <Android查缺补漏(View篇)-- ...

  7. Android事件分发机制源码分析

    Android事件分发机制源码分析 Android事件分发机制源码分析 Part1事件来源以及传递顺序 Activity分发事件源码 PhoneWindow分发事件源码 小结 Part2ViewGro ...

  8. Android事件分发机制三:事件分发工作流程

    前言 很高兴遇见你~ 本文是事件分发系列的第三篇. 在前两篇文章中,Android事件分发机制一:事件是如何到达activity的? 分析了事件分发的真正起点:viewRootImpl,Activit ...

  9. Android事件分发与责任链模式

    一.责任链模式 责任链模式是一种行为模式,为请求创建一个接收者的对象链.这样就避免,一个请求链接多个接收者的情况.进行外部解耦.类似于单向链表结构. 优点: 1. 降低耦合度.它将请求的发送者和接收者 ...

随机推荐

  1. html 标准属性不要用 setAttribute 方法

    html 中有些属性,譬如 checked , autofocus 只要存在就会有效,即使值是 false ,譬如: <input autofocus=false id='test'> & ...

  2. jconsole远程连接超时问题解决方法

    根据oracle网站上的文档,本地使用jconsole没有问题.但当我从windows连接到linux时(centos5.4)时,老是连接不上). 原因是Linux上JVM给jconsole的RMI配 ...

  3. 删除 Linux /tmp 目录下的临时文件

    使用 tmpreaper How to install and use tmpreaper How to Remove Files Older than N days using tmpreaper ...

  4. Oracle数据类型char与varchar的对比

    使用scott用户连接数据库 新建一个表 create table stu01(name char(32)); 插入一条数据 insert into stu01 values('liuyueming' ...

  5. Fast R-CNN论文详解 - CSDN博客

    废话不多说,上车吧,少年 paper链接:Fast R-CNN &创新点 规避R-CNN中冗余的特征提取操作,只对整张图像全区域进行一次特征提取: 用RoI pooling层取代最后一层max ...

  6. HDU Today---hdu2112(最短路-_-坑在是无向图)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2112 spfa或者迪杰斯特拉都可以 注意公交车是有来回的--- #include <iostre ...

  7. Python开发【笔记】:concurrent.futures 平行运算

    平行运算 前言: 编写Python程序时,我们可能会遭遇性能问题,即使优化了代码,程序也依然有可能运行的很慢,从而无法满足我们对执行速度的要求,目前的计算机,其cpu核心数越来越多,于是,我们可以考虑 ...

  8. 【Loadrunner】性能测试:通过服务器日志获取性能需求

    性能测试:通过服务器日志获取性能需求          接触过性能测试的童鞋都知道,想要做好一个项目的性能测试,性能需求的获取至关重要~!如果公司有做过性能测试还好,大家可以拿之前的性能测试数据作为参 ...

  9. centos7 安装Mariadb

    MariaDB 数据库管理系统是 MySQL 的一个分支,主要由开源社区在维护,采用 GPL 授权许可.开发这个分支的原因之一是:甲骨文公司收购了 MySQL 后,有将 MySQL 闭源的潜在风险,因 ...

  10. UIView动画补充

    我自己的总结: // 第一种: Duration 时间 animations:动画体 /* [UIView animateWithDuration:4 animations:^{ CGRect rec ...