nginx源码分析(16)-模块分析(2)

对一个高性能服务器来说,一般都要求处理速度快和资源占用小,尤其是当服务器遇到C10K问题的时候。要做到处理速度足够快,其并发模型的设计相当关键;而要做到资源尤其是内存资源的占用少,就要依赖于其资源分配和资源管理的方案设计。

网 上有一些比较apache、lighttpd和nginx的文章,一般认为apache的功能完善,但是并发能力较弱,资源占用较多,一般并发量达到一千 以上,服务已经比较吃力,进程切换的开销相当大。lighttpd和nginx都是相对轻量很多的webserver,功能比较丰富,采用了高级的IO模 型,并发能力强,资源分配和管理的方案比较精细,会根据请求情况预分配少量的资源,资源用完不立即释放可重用,所以资源的消耗保持在一个很低的水平。 lighttpd用libevent处理IO事件,nginx则针对不同的操作系统定制了IO处理机制,两个webserver的处理能力差不多。不过 lighttpd有内存泄露的问题,一般认为nginx更稳定,而且nginx有些关键功能实现的更好,比如rewrite和proxy,目前nginx 已经超越了lighttpd,成为最多使用的轻量级webserver。

服务器的并发模型设计是网络编程中很关键的一个部分,下面简单介绍几种典型的并发模型设计方案,大部分的高性能服务器的并发模型都是这些模型的变种,基本原理类似。

服务器的并发量取消于两个因素,一个是提供服务的进程数量,另外一个是每个进程可同时处理的并发连接数量。相应的,服务器的并发模型也由两个部分构成:进程模型和连接处理机制。

进程模型可以分为以下几种:

1、单进程模式,这种模式的服务器称为迭代服务器,实现最简单,也没有进程控制的开销,cpu利用率最高,但是所有的客户连接请求排队等待处理,如果有一条连接时长过大,则其他请求就会被阻塞甚至被丢弃,这种模型也很容易被攻击,一般很少使用这种模型;

2、 多进程并发模式,这种模式由master进程启动监听并accept连接,然后为每个客户连接请求现场fork一个worker子进程处理客户请求,这种 模式也比较简单,但是为每个客户连接请求fork一个子进程比较耗费cpu时间,而且子进程过多的情况下可能会用尽内存,导致开始对换,整体性能急降,这 种模型在小并发的情况下比较常用,比如每天处理几千个客户请求的情况;

3、prefork模式,master监听客户连接请求并持续监视可用子进程数量,低于阀值则fork额外的子进程,高于阀值则kill掉一些过剩的子进程。这种模式根据accept的具体情形又可以分为三种变体:

1)master 负责listen,每个worker子进程独自accept,accept无上锁。所有worker阻塞于同一个监听套接字上睡眠,当有新的客户连接请求 时,内核会唤醒所有等待该事件的睡眠worker子进程,最先执行的worker将获得连接套接字并处理请求,这种模型会导致惊群问题,尽管只有一个子进 程将获得连接,但是所有子进程却都会被唤醒,子进程越多,惊群问题对于性能的影响就越大。另一方面,如果每个worker不是阻塞于accept而是阻塞 于select,则很容易造成select冲突问题,这种情况的性能损耗更大,所以这种模型一般都是直接阻塞于accept,不阻塞于select;

2)master 负责listen,每个worker子进程独自accpet,accept有上锁。这种模型解决了惊群问题,只有一个worker阻塞于accpet,其 余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进程可以处理多个连接请求,这样对于每个客 户连接的处理就不能是全程阻塞的了。可以把每个客户连接的处理分为若干过程,每个过程都是一个状态,这样就可以把对一个客户的连接请求处理分解为若干步 骤。我们知道,类似于webserver这样的服务器,其客户处于不同的网络环境,并且server所处的网络环境也随着网络状况的改变而改变,所以每个 客户连接请求耗费的时长不等,有些可能很长(tcp拥塞控制等),有些可能很短,如果进程阻塞于一条时长大的客户请求上面,对于那些时长短的客户请求是不 公平的。如果把每个客户请求的处理分开为不同的阶段,就可以在一个子进程内或者一批子进程间并发的处理更多的连接请求了,并且可以更好的控制资源的分配和 管理,将资源的消耗降到一定的低水平,这样也就等于提高了服务器的整体并发能力。

如何把连接请求处理分解为不同的步骤呢?这就要介绍一下并发模型的连接处理机制了,这个机制的关键是IO模型。一般有五种典型的IO模型,下面我们以网络套接口上的输入操作(In)为例,分别介绍五种模型。

对 于一个套接口上的输入操作,第一步通常是等待数据从网络中到达(或者等待连接请求建立),当所等待分组到达时(或者等待的连接请求已经建立),它被拷贝到 内核中的某个缓冲区(或者listen的监听队列中相应连接请求套接字置位);第二步就是把数据从内核缓冲区拷贝到应用进程的缓冲区(或者accept获 得已经建立的连接请求的连接套接字)。

1、阻塞IO模型:当套接口是阻塞的,所有的输入操作(调用connect,accept,read,recvfrom,recv,recvmsg等输入函数)发起后会阻塞到两个步骤完成才会返回;

2、非阻塞IO模型:当套接口是非阻塞的,所有的输入操作在第一个步骤立即返回,这个时候一般需要轮询检查(循环调用输入函数),当数据准备好或者连接已经建立进入第二步的情况下,调用的输入函数将阻塞到第二步完成为止;

以后两种模型都只能处理一个单独的套接口,如果要同时处理多个套接口,就可以使用:

3、 IO复用模型:当在等待多个套接口的输入时,可以调用select、poll等IO复用函数监听这些套接口的输入事件,进程会阻塞在这些调用上,直到有一 个或者多个套接口的输入事件发生,也即完成了第一步,IO复用函数会返回这些套接口,接着进程可以调用输入函数完成这些套接口的第二步;

4、信号驱动IO模型:创建套接口的时候,开启套接口的信号驱动IO功能,并安装一个信号处理函数,处理SIGIO信号。当套接口完成了第一步时,会发送SIGIO信号通知进程处理,进程在信号处理函数中完成第二步;

5、异步IO模型:告诉内核启动某个输入操作,并让内核在完成了输入操作之后(两个步骤都完成)通知进程。

前三种模型在所有的操作系统都支持,而后两种模型很少操作系统实现。前四种IO模型都导致进程阻塞,直到IO操作完成,属于同步IO操作;只有异步IO模型不导致进程阻塞,是异步IO操作。

IO 复用模型中,select和poll一般所有的操作系统都会支持,但是每次等待都要设置需要等待的套接口,并且内部的实现不够高效,很难支持监听高并发量 的套接口集。不同的操作系统使用了不同的高级轮询技术来支持高性能的监听,一般这些方式都不是可移植的,比如freebsd上实现了 kqueue,solaris实现了/dev/poll,linux实现了epoll等等。

nginx针对不同的操作系统,定制了不同的IO处理机制,一般都会采用操作系统的高性能接口。我们会在下一章详细探讨nginx究竟是如何支撑高并发服务的,其并发模型的设计是怎样的?

你可能感兴趣的:(nginx源码分析(16)-模块分析(2))