nginx的多线程模型分析,很有参考价值
原文地址: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)函数中。