dccmx 于 2011年 二月 13日 发表 | 最后修改于 2011年 二月 15日 from http://blog.dccmx.com/2011/02/nginx-conn-handling/
你知道的,并发连接是任何服务端程序都逃不掉的重要的性能指标。如何处理大量并发的连接无疑是服务端程序设计时所要考虑的第一个问题。这里简单的看看Nginx是如何处理并发的http连接的。
总体结构如下图所示:
对于服务端来讲,处理并发连接无疑要达到的效果是:高并发,快响应。Nginx架构采用的是Master-Worker的多进程协作模式。所以如何让每个worker进程都平均的处理连接也是一个要考虑的问题。
就上图,listen套接字是Master进程初始化的时候创建的,然后fork子进程的时候自然的继承给子进程的。上代码。
在src/core/nginx.c的main函数里依次有如下两行调用:
|
cycle = ngx_init_cycle(&init_cycle);
ngx_master_process_cycle(cycle);
|
在nginx代码中,一个cycle代表一个进程,所有进程相关变量(包括连接)都在这个结构体里。main函数里先调用ngx_init_cycle来初始化了一个主进程实例,80端口的监听套接字也是在这个函数里创建的:
|
if
(ngx_open_listening_sockets(cycle) != NGX_OK) {
goto
failed;
}
|
在ngx_open_listening_sockets函数的代码中可以看到bind、listen等套接字函数的调用。最终创建完的监听套接字就在cycle结构体的listening域里。相关代码如下:
|
ls = cycle->listening.elts;
s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0);
if
(setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
(
const
void
*) &reuseaddr,
sizeof
(
int
))
== -1)
{...
if
(bind(s, ls[i].sockaddr, ls[i].socklen) == -1) {...
if
(listen(s, ls[i].backlog) == -1) {...
ls[i].listen = 1;
ls[i].fd = s;
|
你懂的。
main函数里面调用的ngx_master_process_cycle就是创建worker进程的地方了。
在ngx_master_process_cycle函数里调用了ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_RESPAWN);函数,在ngx_start_worker_processes函数里我们有能看到
|
for
(i = 0; i < n; i++) {
cpu_affinity = ngx_get_cpu_affinity(i);
ngx_spawn_process(cycle, ngx_worker_process_cycle, NULL,
"worker process"
, type);
ch.pid = ngx_processes[ngx_process_slot].pid;
ch.slot = ngx_process_slot;
ch.fd = ngx_processes[ngx_process_slot].channel[0];
ngx_pass_open_channel(cycle, &ch);
}
|
好了,自此Master的活就干得差不多了,之后Master进程和worker进程纷纷进入自己的事件循环。Master的事件循环就是收收信号,管理管理worker进程,而worker进程的事件循环就是监听网络事件并处理(如新建连接,断开连接,处理请求发送响应等等),所以真正的连接最终是连到了worker进程上的,各个worker进程之间又是怎么接受(调用accept()函数)的呢。所有的worker进程都有监听套接字,都能够accept一个连接,所以,nginx准备了一个accept锁,如图,所有的子进程在走到处理新连接这一步的时候都要争一下这个锁,争到锁的worker进程可以调用accept接受新连接。这样做的目的就是为了防止多个进程同时accept,当一个连接来的时候多个进程同时被唤起——所谓惊群(BTW:据说新版本内核已经没有惊群了,待考证)。相关代码在src/event/event.c的ngx_process_events_and_timers中:
|
if
(ngx_use_accept_mutex) {
if
(ngx_accept_disabled > 0) {
ngx_accept_disabled--;
//此处ngx_accept_disabled其实是进程的最大连接数(配置文件中指定)的1/8减去剩余连接数的差
//在src/event/nginx_event_accept.c中计算:ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
//当剩余连接数小于最大连接数的1/8的时候为正,表示连接有点多了,于是放弃一次争锁定机会
}
else
{
if
(ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
//这里ngx_trylock_accept_mutex函数就是争锁定函数,成功争得了锁则将全局变量ngx_accept_mutex_held置为1,否则置0
return
;
}
if
(ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
//占用了accept锁的进程在处理事件的时候是先将事件放入队列,后续慢慢处理,以便尽快走到下面释放锁。
}
else
{
//没争得锁的进程不需要分两步处理事件,但是把处理事件的timer更新为ngx_accept_mutex_delay
if
(timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
}
delta = ngx_current_msec;
//下面这个函数就是处理事件的函数(包括新连接建立事件),网络IO事件等等
(
void
) ngx_process_events(cycle, timer, flags);
delta = ngx_current_msec - delta;
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->
log
, 0,
"timer delta: %M"
, delta);
if
(ngx_posted_accept_events) {
//这里处理队列中的accept事件,这里面将会调用ngx_event_accept来建立新连接
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
}
if
(ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
//好了,释放accept锁
}
|
好了,就是这样子的,这样Nginx就将并发的连接较平均的分发给了worker进程(其实是各个worker进程较平均的抢)。具体的accept调用和连接的初始化都在src/event/nginx_event_accept.c中的void ngx_event_accept(ngx_event_t *ev)函数中。
介绍的很好, 语言风趣幽默。
能进一步把线程模型也分析下就好。 呵呵
nginx里的线程是浮云啊,默认都不开的,开了也没什么好处的。如果要看多线程服务器就看memcached的线程模型吧
能否推荐下,介绍模型的一些资料。
为什么是浮云? 是不是多线程原子锁的操作太多了对性能没什么提升?
现在,每个worker通过异步的方式运转性能已经极限了,再开线程线程线程间切换什么的反而性能下降。
还是看经典开源软件的代码来的实在。
哦,有相关的测试数据说明已经极限了吗?
恩,还需多看多学习呀。
C10k
-------------------------------------------------------------------------------
Nginx进程管理之master进程
Nginx分为Single和Master两种进程模型,Single模型即为单进程方式工作,具有较差的容错能力,不适合生产之用。Master模型即为一个master进程+N个worker进程的工作方式。生产环境都是用master-worker模型来工作。本文着重分析Nginx的master进程做了哪些事情,它是如何管理好各个worker进程的。在具体分析代码之前,先附上一张master进程的全貌图:
![](http://www.tbdata.org/wp-content/uploads/2010/11/master1.jpg)
我们知道在main函数中完成了Nginx启动初始化过程,启动初始化过程中的一个重要环节就是解析配置文件,回调各个配置指令的回调函数,因此完成了各个模块的配置及相互关联。在所有的这些重要及不重要的初始化完成后,main函数就开始为我们打开进程的“大门”――调用ngx_master_process_cycle(cycle); 接下来的文字里面,我们就重点来看看这个函数里做了一些什么事情。
迈入这扇大门之后,迎面而来的就是屏蔽一系列的信号,以防干事的时候,被打扰嘛。你懂的。
这里好像要开始创建子进程了哦,没错,master进程就是通过依次调用这两个函数来创建子进程。第一个调用的函数创建的子进程我们称为worker进程,第二调用的函数创建的是有关cache的子进程。接收请求,完成响应的就是worker进程。光光是调用这个函数好像没什么看头,我们深入“虎穴”窥探一下究竟。
其实吧,ngx_start_worker_processes函数挺短小精干的,再截取主体就剩下这么一个for循环了。此处就是循环创建起n个worker进程,fork新进程的具体工作在ngx_spawn_process函数中完成。这里涉及到了一个全局数组ngx_processes(定义在src/os/unix/ngx_process.c文件中),这个数组的长度为NGX_MAX_PROCESSES(默认1024),存储的元素类型是ngx_process_t(定义在src/os/unix/ngx_process.h文件中)。全局数组ngx_processes就是用来存储每个子进程的相关信息,如:pid,channel,进程做具体事情的接口指针等等,这些信息就是用结构体ngx_process_t来描述的。在ngx_spawn_process创建好一个worker进程返回后,master进程就将worker进程的pid、worker进程在ngx_processes数组中的位置及channel[0]传递给前面已经创建好的worker进程,然后继续循环开始创建下一个worker进程。刚提到一个channel[0],这里简单说明一下:channel就是一个能够存储2个整型元素的数组而已,这个channel数组就是用于socketpair函数创建一个进程间通道之用的。master和worker进程以及worker进程之间都可以通过这样的一个通道进行通信,这个通道就是在ngx_spawn_process函数中fork之前调用socketpair创建的。有兴趣的自己读读ngx_spawn_process吧。
至于ngx_start_cache_manager_processes函数,和start_worker的工作相差无几,这里暂时就不纠结了。至此,master进程就完成了worker进程的创建工作了,此时此刻系统中就有一个master进程+N个worker进程在工作了哦,接下来master进程将“陷入”死循环中守护着worker进程,担当起伟大的幕后工作。在master cycle中调用了sigsuspend(),因而将master进程挂起,等待信号的产生。master cycle所做的事情虽然不算复杂,但却比较多;主要过程就是:【收到信号】,【调用信号处理函数(在初始化过程中注册了)】,【设置对应的全局变量】,【sigsuspend函数返回,判断各个全局变量的值并采取相应的动作】。在这里,我们不对每个信号的处理情况进行分析,随便看看两个信号就好了。
这段位于master cycle中的代码是对SIGQUIT信号进行的处理动作。ngx_quit就那个全局变量之一,当master进程收到这个信号的时候,就调用ngx_signal_handler(定义在src/os/unix/ngx_process.c文件中)设置ngx_quit为1。因此master从sigsuspend返回后,检测到ngx_quit为1,就调用ngx_signal_worker_processes函数向每个worker进程递送SIGQUIT信号,通知worker进程们开始退出工作。然后就关闭所有的监听套接字。最后居然来了一个continue就又回到了cycle中,不是退出吗?为什么是continue而不是exit呢。前面已经提过了,master进程是幕后者,需要守护着worker进程们,既然是守护哪能worker进程没撤退,自己就先撤退了呢。由于,worker进程是master的子进程,所以worker退出后,将发送SIGCHLD信号给master进程,好让master进程为其善后(否则将出现“僵尸”进程)。在master进程收到SIGCHLD信号,就会设置全局变量ngx_reap为1了。
此时,ngx_reap为1了,master进程调用ngx_reap_children处理所有的worker子进程。这个ngx_reap_children函数不光担任起为worker进程善后的工作(子进程的收尸处理是在信号处理函数直接完成的),还担任了重启worker进程的任务。当然,这个重启worker进程是在一些异常情况下导致worker进程退出后的重启,并不是在“君要臣死、臣不得不死”的时候的顽强抵抗。Nginx具有高度的模块化优势,每个人都可以开发自己需要的模块程序,难免会出现一些bug引起worker进程的崩溃,因此master进程就肩负起了容错任务,这样才能够保证24小时的提供服务。
至此,master进程也就差不多了,剩下的一些信号处理动作,有兴趣的自行研究吧,其实master进程在处理热代码替换方面也是值得一读的。下一篇博文理所当然的应该是worker进程了。
Nginx进程管理之worker进程
上一篇博文分析了master进程,本文着手分析一下worker进程的情况。首先找到worker进程的入口地方――ngx_worker_process_cycle。这个函数不光是worker进程的入口函数,同时也是worker进程循环工作的主体函数,看函数名含有一个cycle嘛。进入这个cycle函数,第一件事就是调用ngx_worker_process_init(cycle, 1);对worker进程进行初始化操作。先看看这个worker进程的初始化过程。
进入初始化就将全局变量ngx_process设置为worker进程的标志,由于这个变量是从master进程复制过来的,所以没设置前就是master进程的标志。然后设置相应的环境变量。接下去就是设置了一些列的资源限制,id等玩意,这里就忽略代码了。
此处循环调用每个模块的init_process,完成每个模块自定义的进程初始化操作,一般在模块定义的时候设置这个回调指针的值,即注册一个函数给它。做模块开发的时候,貌似使用得挺少的,遇到的时候好好关注下。
针对这段代码采用直接注释代码的方式进行分析,感觉挺自然的,还不错,以后碰到比较长的代码段都采用这种方式进行了。
ngx_channel就是worker进程channel的读端,这里调用ngx_add_channel_event将channel放入Nginx关心的集合中,同时关注起这个channel上的读事件,也即这个channel上有数据到来后,就立马采取读channel操作。此处的添加一个channel的读事件是worker进程初始化的关键之处。到此,初始化过程就结束了,回到worker循环主体看看吧。
通过上述分析发现worker进程的cycle比master简单不少啊,到此,worker进程的大体框架就差不多了。下一篇计划分析master和worker间的channel通信。
Nginx的master和worker进程间的通信
前面单独分析了master进程和worker的工作情况,本文就大概看一下master进程和worker进程之间是如何使用channel来完成通信的。这部分实现的源码主要分布于src/os/unix/channel.h和channel.c两个文件中。实现极其简单,没有什么复杂的逻辑。下面,我绘制了一个简单的master进程和worker进程间的关系,图中的箭头符号指出数据是由master进程传给worker进程,而没有从worker到master;这是因为channel不是一个普通的数据传输管道,在Nginx中它仅仅是用着master发送指令给worker的一个管道,master借此channel来告诉worker进程该做什么了,worker却不需要告诉master该做什么,所以是一个单向的通道。
![](http://www.tbdata.org/wp-content/uploads/2010/12/m-w.jpg)
master进程每次发送给worker进程的指令用如下一个结构来完成封装:
这个结构中的4个字段分别是发送的指令、worker进程的pid、worker进程的slot(在ngx_proecsses中的索引)及一个文件描述符。master进程可能会将一个打开的文件描述符发送给worker进程进行读写操作,那么此时就需要填写fd这个字段了。worker进程在收到一个这样的结构数据后,通过判断command的值来采取相应的动作;command就是master给worker下达的命令。
master进程用于处理SIGCHLD信号的函数ngx_reap_children中就有向worker进程发送关闭channel的指令,我们看看这个例子是怎么做的。
这几行代码是我从ngx_reap_children函数中拼凑起来的,所以看上去好像有点奇怪,不那么顺畅;但却清晰的给我们展现了master进程怎么给一个worker进程发送指令,此处发送的指令时NGX_CMD_CLOSE_CHANNEL。发送指令的函数ngx_write_channel是利用sendmsg来完成,《Unix网络编程》可以详细了解sendmsg。
worker进程在调用ngx_worker_process_init进行初始化的时候,使用了如下两行代码将channel放到epoll等事件处理模块中。
当master进程发来指令后,就调用ngx_channel_handler函数进行事件的响应。下面浓缩的代码给出了ngx_channel_handler所做的事情。
Nginx中关于整个channel的实现就这么简单,没有什么多余的事情。
http://blogread.cn/it/article.php?id=5008&f=sa
不知不觉网站PV就爆发了。nginx压力越来越大,一些默认参数就显得不够用了。
我们的主服务器硬件配置非常健壮(双路至强5620 + 48GB内存 + SSD),理论上可以承受每天过500万的PV,当然,前提是优化得够好。
简单罗列一下优化过的几个参数:
默认是1024,意思是最多打开的文件个数。1024怎么够,至少开到8192,网上很多文章都直接开到了65535。
worker数量,位于nginx.conf头部,一般来说有几个cpu核心开几个,不算超线程。
位于nginx.conf头部,也是文件数量限制,直接开大吧。
位于nginx.conf中,默认是1024,也不够。
location /status/ {
stub_status on;
}
小小炫耀一下服务器的status参数,想当年刚用nginx的时候,Writing参数不是0就是1,现在已经这么大了: