【Nginx】进程模型

转自:网易博客

服务器的并发模型设计是网络编程中很关键的一个部分,服务器的并发量取决于两个因素,一个是提供服务的进程数量,另外一个是每个进程可同时处理的并发连接数量。相应的,服务器的并发模型也由两个部分构成:进程模型和连接处理机制。进程模型主要有以下3种模型:

1单进程模式:这种模式的服务器称为迭代服务器,实现最简单,也没有进程控制的开销,cpu利用率最高,但是所有的客户连接请求排队等待处理,如果有一条连接时长过大,则其他请求就会被阻塞甚至被丢弃,这种模型也很容易被攻击,一般很少使用这种模型;
2多进程并发模式:这种模式由master进程启动监听并接受连接,然后为每个客户连接请求fork一个worker子进程处理客户请求(worker子进程的数量不受限制),这种模式也比较简单,但是为每个客户连接请求fork一个子进程比较耗费cpu时间,而且子进程过多的情况下可能会用尽内存,导致开始对换,整体性能急降,这种模型在小并发的情况下比较常用,比如每天处理几千个客户请求的情况;
3prefork模式:
master进程监听客户端连接请求并持续监视可用子进程数量,低于阀值则fork额外的子进程,高于阀值则kill掉一些过剩的子进程。这种模式根据accept的具体情形又可以分为三种变体:

*  1.master 负责listen,每个worker子进程独自acceptaccept无上锁。所有worker阻塞于同一个监听套接字上,当有新的客户连接请求时,内核会唤醒所有等待该事件的睡眠worker子进程,最先执行的worker将获得连接套接字并处理请求,这种模型会导致惊群问题,尽管只有一个子进程将获得连接,但是所有子进程都会被唤醒,子进程越多,惊群问题对于性能的影响就越大。

*  2.master 负责listen,每个worker子进程独自accpetaccept有上锁。这种模型解决了惊群问题,只有一个worker阻塞于accpet(只有获取了mutex才能接受socket连接),其余worker都阻塞于获取锁资源,上锁可以使用文件上锁或者使用共享内存的互斥锁,这种模型的cpu耗时略高于第一种模型。这两种模型都是由内核负责把客户端连接请求交由某个worker,客户连接请求的处理比较均匀,因为内核使用了公平的进程切换方式;

*  3.master负责listen accpet,通过某种方式把获得的连接套接字交给一个空闲worker这种模型下的master必须管理所有worker子进程的状态,并且要使用某种方式的进程间通信方式传递套接字给子进程,比如采用socketpair创建字节流管道用于传递。相对于上面两种模型而言,这种模型复杂度高一些,cpu耗时也更高,并且子进程的分配也由master负责,是否均匀取决于master。 

以上的进程模型都假定了两个条件,即套接字是阻塞的,并且每个客户连接请求对应一个子进程。这也就意味着如果同时并发量很高的时候,比如超过1万的并发量,就要有1万个worker子进程同时服务,内存耗光后,服务器性能急剧下降。这些模型基本上只能服务于并发量很低的情况,一般在1千以内勉强过得去(还依赖于每个处理的消耗)。

一个自然的解决办法就是把进程与连接的比例从1:1变成m:n。m=1、n>1的情况下,一个worker进程可以处理多个连接请求,这样对于每个客户端连接的处理就不能是全程阻塞的了。可以把每个客户端连接的处理分为若干过程,每个过程都是一个状态,这样就可以把对一个客户的连接请求处理分解成若干步骤。如果把每个客户请求的处理分开为不同的阶段,就可以在一个子进程内或者一批子进程间并发的处理更多的连接请求了,并且可以更好的控制资源的分配和管理,将资源的消耗降到一定的低水平,这样也就等于提高了服务器的整体并发能力。

请求的多阶段异步处理

把一个请求的处理过程按照事件的触发方式分为多个阶段,每个阶段都可以由事件收集、分发器来触发。

关键在于划分请求的阶段,一般是找到请求处理流程中的阻塞方法(或者造成阻塞的代码段),在阻塞代码段上按照下面4种方式来划分阶段:

1.将阻塞进程的方法按照相关的触发事件分解为两个阶段

大部分情况下,一个阻塞进程的方法调用时可以划分为两个阶段:阻塞方法改为非阻塞方法调用,这个调用非阻塞方法并将进程归还给事件分发器的阶段就是第一阶段;增加新的处理阶段用于处理非阻塞方法最终返回的结果,这里的结果返回事件就是第二阶段的触发事件

2.将阻塞方法调用按照时间分解为多个阶段的方法调用

系统中的事件收集、分发者并非可以处理任何事件,如果按照前一种方式试图划分某个方法时,那么可能会发现找出的触发事件不能够被事件收集、分发器所处理,这时只能按照执行时间来拆分这个方法。

例如读取10MB的文件(ngx_epoll_module模块主要是针对网络事件),可以这样来分解读取文件调用:每次只读取10kb,这样该事件接收器占用进程的时间不会太久,整个进程可以及时地处理其他请求。

在读取0-10kb后,为了进入10kb-20kb阶段,可以用能被事件收集器处理的事件来触发,或者设置一个定时器。

3.在无所事事且必须等待系统的响应,从而导致进程空转时,使用定时器划分阶段

当定时器事件发生时就会检查标志,如果标志位不满足,就立刻归还进程控制权,同时继续加入期望的下一个定时器事件

4.如果阻塞方法完全无法继续划分,则必须使用独立的进程执行这个阻塞方法

 Nginx进程模型:

nginx采用的是大部分HTTP服务器的做法,即master-worker模型,一个master进程管理一个或者多个worker进程,基本的事件处理都是放在worker进程,master负责一些全局初始化,以及对worker进程的管理。

nginx中,master进程和worker进程的通信主要是通过socketpair来实现的,进程与外部之间是通过信号通信的。每当fork完一个子进程之后,就将这个子进程的socketpair句柄传递给前面已经存在的子进程,这样子进程之间也就可以通信了。Nginx中fork子进程的函数是ngx_spawn_process()。主要实现代码在ngx_process.h和ngx_process.c文件中。

 Nginx模块开发(12)—进程模型 - cjhust - 我一直在努力

ngx_spawn_process用于具体创建worker进程。

步骤:

1.在进程表中为将要创建的子进程分配一个表项,若分配失败,则出错,即有一个ngx_processes全局数组,包含了所有的存活的子进程,这里fork出来的子进程会放入到相应的位置

2.创建父子进程间通信的socket对

3.创建子进程:设置进程id,进程运行执行函数,设置好进程表项字段

后面fork的子进程如何来让前面已经fork的子进程得到自己的进程相关信息:

在nginx中是每次新的子进程fork完毕后,然后父进程此时将这个子进程id,以及流管道的句柄channel[0]传递给前面的子进程,这样子进程之间也可以通信了

在nginx中,worker和master的交互,是通过流管道,而master与外部的交互是通过信号来进行的。

 

进程结构,这个结构体表示了一个进程,包含了它的id状态,channel等等:

typedef struct {
///进程id
    ngx_pid_t           pid;
///进程的退出状态(主要在waitpid中进行处理).
    int                 status;
///进程channel(也就是通过socketpair创建的两个句柄)
    ngx_socket_t        channel[2];

///进程的执行函数(也就是每次spawn,子进程所要执行的那个函数).
    ngx_spawn_proc_pt   proc;
    void               *data;
    char               *name;
///进程的几个状态。
    unsigned            respawn:1;
    unsigned            just_respawn:1;
    unsigned            detached:1;
    unsigned            exiting:1;
    unsigned            exited:1;
} ngx_process_t;

 

//参数解释:
//cycle:nginx框架所围绕的核心结构体
//proc:子进程中将要执行的工作循环
//data:参数
//name:子进程名字
ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
    char *name, ngx_int_t respawn)
{
    u_long     on;
    ngx_pid_t  pid;
    ngx_int_t  s;//将要创建的子进程在进程表中的位置
    //替换进程ngx_processes[respawn],可安全重用该进程表项
    if (respawn >= 0) {
        s = respawn;

    } else {//先找到一个被回收的进程表项
        for (s = 0; s < ngx_last_process; s++) {
            if (ngx_processes[s].pid == -1) {
                break;
            }
        }
        //进程表已经满
        if (s == NGX_MAX_PROCESSES) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
                          "no more than %d processes can be spawned",
                          NGX_MAX_PROCESSES);
            return NGX_INVALID_PID;
        }
    }


    if (respawn != NGX_PROCESS_DETACHED) {

        /* Solaris 9 still has no AF_LOCAL */
        //创建父子进程间通信的套接字对(基于TCP)
        if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1)
        {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "socketpair() failed while spawning \"%s\"", name);
            return NGX_INVALID_PID;
        }

        ngx_log_debug2(NGX_LOG_DEBUG_CORE, cycle->log, 0,
                       "channel %d:%d",
                       ngx_processes[s].channel[0],
                       ngx_processes[s].channel[1]);

        //设置socket为非阻塞模式
        if (ngx_nonblocking(ngx_processes[s].channel[0]) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          ngx_nonblocking_n " failed while spawning \"%s\"",
                          name);
            ngx_close_channel(ngx_processes[s].channel, cycle->log);
            return NGX_INVALID_PID;
        }

        if (ngx_nonblocking(ngx_processes[s].channel[1]) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          ngx_nonblocking_n " failed while spawning \"%s\"",
                          name);
            ngx_close_channel(ngx_processes[s].channel, cycle->log);
            return NGX_INVALID_PID;
        }
        //开启channel[0]的消息驱动IO
        on = 1;
        if (ioctl(ngx_processes[s].channel[0], FIOASYNC, &on) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "ioctl(FIOASYNC) failed while spawning \"%s\"", name);
            ngx_close_channel(ngx_processes[s].channel, cycle->log);
            return NGX_INVALID_PID;
        }
        //设置channel[0]的宿主,控制channel[0]的SIGIO信号只发给这个进程
        if (fcntl(ngx_processes[s].channel[0], F_SETOWN, ngx_pid) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "fcntl(F_SETOWN) failed while spawning \"%s\"", name);
            ngx_close_channel(ngx_processes[s].channel, cycle->log);
            return NGX_INVALID_PID;
        }
        //若进程执行了exec后,关闭socket
        if (fcntl(ngx_processes[s].channel[0], F_SETFD, FD_CLOEXEC) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "fcntl(FD_CLOEXEC) failed while spawning \"%s\"",
                           name);
            ngx_close_channel(ngx_processes[s].channel, cycle->log);
            return NGX_INVALID_PID;
        }
        //同上
        if (fcntl(ngx_processes[s].channel[1], F_SETFD, FD_CLOEXEC) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "fcntl(FD_CLOEXEC) failed while spawning \"%s\"",
                           name);
            ngx_close_channel(ngx_processes[s].channel, cycle->log);
            return NGX_INVALID_PID;
        }

        ngx_channel = ngx_processes[s].channel[1];

    } else {
        ngx_processes[s].channel[0] = -1;
        ngx_processes[s].channel[1] = -1;
    }
    //设置当前子进程的进程表索引值
    ngx_process_slot = s;

    //创建子进程
    pid = fork();

    switch (pid) {

    case -1:
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "fork() failed while spawning \"%s\"", name);
        ngx_close_channel(ngx_processes[s].channel, cycle->log);
        return NGX_INVALID_PID;

    case 0:
        ngx_pid = ngx_getpid();//设置当前子进程的进程id
        proc(cycle, data);//子进程运行执行函数 
        break;

    default:
        break;
    }

    ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "start %s %P", name, pid);
    //设置一些进程表项字段
    ngx_processes[s].pid = pid;
    ngx_processes[s].exited = 0;
    //如果是重复创建,即为替换进程,不用设置其他进程表字段,直接返回。
    if (respawn >= 0) {
        return pid;
    }

    ngx_processes[s].proc = proc;
    ngx_processes[s].data = data;
    ngx_processes[s].name = name;
    ngx_processes[s].exiting = 0;
    //设置进程表项的一些状态字
    switch (respawn) {

    case NGX_PROCESS_NORESPAWN:
        ngx_processes[s].respawn = 0;
        ngx_processes[s].just_spawn = 0;
        ngx_processes[s].detached = 0;
        break;

    case NGX_PROCESS_JUST_SPAWN:
        ngx_processes[s].respawn = 0;
        ngx_processes[s].just_spawn = 1;
        ngx_processes[s].detached = 0;
        break;

    case NGX_PROCESS_RESPAWN:
        ngx_processes[s].respawn = 1;
        ngx_processes[s].just_spawn = 0;
        ngx_processes[s].detached = 0;
        break;

    case NGX_PROCESS_JUST_RESPAWN:
        ngx_processes[s].respawn = 1;
        ngx_processes[s].just_spawn = 1;
        ngx_processes[s].detached = 0;
        break;

    case NGX_PROCESS_DETACHED:
        ngx_processes[s].respawn = 0;
        ngx_processes[s].just_spawn = 0;
        ngx_processes[s].detached = 1;
        break;
    }

    if (s == ngx_last_process) {
        ngx_last_process++;
    }

    return pid;
}

 

与apache的比较:

Apache通过创建进程和线程来处理其他的连接。管理员可以通过设置来控制服务器所能允许的最大进程数量。这个配置因机器的可用内存而异。过多的进程会耗尽内存从而使得机器使用磁盘上的交换内存,这严重的降低了性能。而且,当达到进程的上限之后,Apache会拒绝新的连接。Apache可以通过设置来运行在pre-forked 模式或worker multi-process 模式(MPM)。当其他的用户连接时,两种方式都会创建新的进程。区别在于,pre-forked模式为每一个进程创建一个线程,用来处理一个用户的请求。worker模式也创建新的进程,但是每一个进程至少有一个线程,每一个线程用来处理单个用户的单个请求。所以,一个worker mode 的进程处理至少一个连接,而一个per-fork 模式的进程只处理一个连接。

prefork模式:一个单独的控制进程(父进程)负责产生子进程,这些子进程用于监听请求并作出应答,每个子进程只有一个线程,每个进程在某个确定的时间只能维持一个连接。在大多数平台上,Prefork MPM在效率上要比Worker MPM要高,但是内存使用大得多。prefork的无线程设计在某些情况下将比MPM更有优势:他能够使用那些没有处理好线程安全的第三方模块,并 且对于那些线程调试困难的平台而言,他也更容易调试一些。

MPM模式:每个进程能够拥有的线程数量是固定的,服务器会根据负载情况增加或减少进程数量,一个单独的控制进程(父进程)负责子进程的建 立,每个线程在某个确定的时间只能维持一个连接。缺点:假如一个线程崩溃,整个进程就会连同其任何线程一起”死掉”

Nginx并不会为每一个的web请求创建新的进程,相反,管理员可以配置Nginx主进程的工作进程的数量(一个常见的做法是为每一个CPU配置一个工作进程)。所有这些进程都是单线程的。每一个工作进程可以处理数千个并发的请求。它通过一个线程来异步的完成了这些工作,而没有使用多线程的编程模型(把客户端连接请求分成若干个阶段,异步处理连接请求)。

apache是同步多进程模型,一个进程对应固定数量的连接;nginx是异步的,多个连接(万级别)可以对应一个进程 

 

参考:

http://simohayha.iteye.com/blog/467940

http://www.lupaworld.com/portal.php?mod=view&aid=239198&page=all

http://www.server110.com/nginx/201402/6543.html

http://blog.chinaunix.net/uid-20441206-id-3360689.html

posted @ 2014-08-17 19:29  合唱团abc  阅读(382)  评论(0编辑  收藏  举报