1.1 吞吐率
Web服务器的吞吐率是指其单位时间内所能处理的请求数。更关心的是服务器并发处理能力的上限即最大吞吐率。
Web服务器在实际工作中,其处理的Http请求包括对很多不同资源的请求即请求的url不一样。正因为这种请求性质的不同,Web服务器并发能力的强弱关键在于如何针对不同的请求性质设计不同的并发策略。有时候一台Web服务器要同时处理许多不同性质的请求,在一程度上使得Web服务器性能无法发挥。
并发用户数为某一时刻同时向服务器发送请求的用户数。注意,100个用户同时向服务器各发10个请求与1个用户同时向服务器发1000个请求对服务器造成的压力是不一样的,显然是前者造成的压力更大,原因是此时服务器网卡接收缓冲区中的请求同时有100个等待处理。
最大并发数是有一定利益前提的,是用户和服务器各自期望利益的一个衡量点。一般是服务器保持了比较高的吞吐率同时用户对等待时间比较满意时的并发数即可定为最大并发数。
在并发用户数较大的情况下,服务器采用什么样的并发策略是影响最大并发数的关键。
用户访问web站点通常是使用浏览器,而浏览器在下载一个网页及网页中的组件是采用多线程下载的。但其对同一域名下的URL并发下载数是有限制的,具体限制因浏览器及其版本和http版本不同。
服务器支持的最大并发数具体到真实用户并不是一对一的关系。一个真实的用户可能给服务器带来两个或更多的并发用户数的压力。
从web服务器的角度看,实际并发用户数可理解为服务器维护不同用户的文件描述符总数即并发连接数。不是同时有多少用户,服务器就为其建立多少连接,服务器一般会限制同时服务的最多用户数。
web服务器工作的本质是以最快的速度将内核缓冲区中的用户请求数据拿回来并尽量尽快处理完这些请求,并将响应数据放到发送数据的缓冲区中,再去处理下一拨请求,如此反复。
用户平均请求等待时间用于衡量服务器在一定并发用户数下对单个用户的服务质量。而服务器平均请求处理时间用于衡量服务器的整体服务质量,它是吞吐率的倒数。如果并发策略得当,每个请求的平均处理时间可以减少。
并发策略的设计就是在服务器同时处理较多请求的时候合理协调并充分利用CPU和IO计算 ,使其在较大并发用户数下保持较高的吞吐率。但并不存在一个对所有请求性质都较高的并发策略。
1.2 CPU并发计算
服务器之所以可以同时处理多个请求,在于操作系统通过多执行流体系设计多个任务可以轮流使用系统资源,包括CPU、内存、IO等。
多执行流的一般实现便是进程,多进程的好处可以对CPU时间的轮流使用,对CPU计算和IO操作重叠利用。这里的IO主要是指磁盘IO和网络IO,对CPU而言,它们慢的可怜。大多数进程的时间主要耗在IO上。
进程的调度由内核执行,进程的目的是担当分配资源的实体。每个进程都有自己的内存地址空间和生命周期。子进程被父进程创建后便把父进程地址空间的所有数据复制到自己的内存地址空间。完全继承父进程的上下文信息,它们之间可以互相通信,但不互相依赖,无权干涉。
进程的创建使用fork()系统调用,服务器频繁地创建进程会引起不小的性能开销。Linux 2.6对fork()进行了优化,减少了一些多余的内存复制。
进程的优越性体现在其稳定性和健壮性,其中一个进程崩溃不会影响到另一个进程。但采用大量进程的web服务器(如:Apache prefork模型)在处理大量并发请求时其内存开销将成为性能的瓶颈。
轻量级进程由系统调用clone()来创建,由内核管理,独立存在,允许这些进程共享数据,轻量级进程减少了内存开销,为多进程应用提供了数据共享,但其上下文切换开销还是避免不了。
一般多线程的管理在用户态完成,线程切换的开销比轻量级进程切换开销要小,但它在多CPU服务器中表现较差。
进程调度器维护着一个可运行队列以及一个包括所有休眠和僵尸进程的列表。进程调度器的工作就是决定下一个运行的进程。如果队列中有多个可运行的进程,此时进程调度器可根据进程的优先级及其它策略进行选择。
CPU时间片的长度要具体权衡,时间片太短,那么CPU在进程切换上的时间浪费就比较大,如果时间片太长,那么多任务实时性和交互性就无法做到保证。
系统负载越高代表CPU越忙,也就越无法很好地满足所有进程的需要。系统负载的计算是根据单位时间内运行队列中就绪等待的进程数平均值。当运行队列中的就绪进程不需要等待就可以立即得到CPU说明系统负载比较低,系统响应速度也就快。
查看系统负载可以通过cat /proc/loadavg、top、w等命令工具查看。
进程的切换就是进程调度器挂起正在运行的进程,恢复之前挂起的某个进程。
每个进程只能共享CPU寄存器,一个进程被挂起的本质就是将其在CPU寄存器中的数据取出来暂存到内核堆栈中,恢复一个进程的本质就是将其数据重新载入到CPU寄存器中,其实这种硬件上下文切换的开销也是挺大的。
要服务器支持较大的并发数,就要减少上下文切换的次数,最简单地做法是减少进程数目,尽量使用线程配合其它IO模型来设计并发策略。
除了关注CPU使用率外,还要关注IOWait,它是指CPU空闲并等待IO操作完成的时间比例。IOWait不能真实地代表IO操作的性能或工作量,它是衡量CPU性能的。即使IOWait为100%也不代表IO出现性能瓶颈,IOWait为0时IO也可能很忙。此时,最好是测试磁盘IO和查看网络IO的流量。
1.3 系统调用
进程有用户态和内核态两种运行模式。进程可以在这两种模式中切换,存在一定的开销。进程通常运行在用户态,当进程需要对硬件操作的时候就要切换到内核态。这两种模式的分离是为了底层操作的安全性和简化开发模型。所有进程都必须通过内核提供的系统调用来操作硬件。进程从用户态到内核态存在一定的内存空间切换,这种开销是比较昂贵的,应尽量减少不必要的系统调用。
1.4 内存分配
Web服务器在工作的过程中需要大量的内存,这使得内存的分配和释放很重要。服务器处理成千上万的http请求,其内存堆栈的分配和复制次数变得更加频繁。
Apache在运行时内存使用量非常惊人,它一开始就申请大量内存作内存池,为防止以后频繁的内存再分配带来的性能开销,内存池的使用使用Apache管理更安全,但内存池的使用也没有弥补其性能,其内存池的释放是在Apache关闭的时候。
Lighttpd使用单进程模型,其内存使用量比较小,同样是使用单进程的Nginx其内存使用量更小,Nginx使用多线程处理请求,这些多线程可以共享内存资源,它使用分阶段按需分配内存、及时释放策略。
1.5 持久连接
持久连接是指一次TCP连接中持续处理多个请求而不断开连接。建立TCP连接操作的开销可不小,在允许的情况下,连接次数越小越有利于性能提升。
长连接对于密集型的图片或网页等小数据量的请求有明显的加速作用。
Http长连接的实施需要浏览器和服务器的配合,缺一不可。
浏览器要支持http长连接可以在http请求头中加入:Connection: Keep-Alive,目前主流web服务器都默认使用长连接,除非显式关闭。
对于长连接的使用要注意长连接的有效时间多长,即什么时候关闭长连接,浏览器和服务器都有默认的有效时间,也都可以设置有效时间,都可以主动关闭,若两者设置的时间长度不一致,以短的为准。例如:
请求:Connection:Keep-Alive
响应:Connection:Keep-Alive
Keep-Alive:timeout=5,max=100
持久连接的目的就是减少连接次数,重用已有的连接通道,减少连接开销。
1.6 IO模型
IO有内存IO、网络IO和磁盘IO等。
可以使用RAID磁盘阵列来加速对磁盘IO的访问,使用独立网络带宽和高带宽网络适配器可以搞网络IO速度,但IO操作都要由内核系统调用完成,系统调用需要CPU调用,无疑存在CPU快和IO慢的不协调。
我们所关注的IO操作主要是网络数据的发送、接收和磁盘文件的访问。不同IO模型的本质在于CPU参与的方式。
DMA:直接内存访问。即不需要通过CPU即可以进行内存到磁盘的数据交换。这样就可降低对CPU的占有率,节省系统资源。
IO等待是不可避免的,既然有等待,就会有阻塞。这里的阻塞是指当前发起请求的进程IO被阻塞,并不是CPU被阻塞,CPU是没有阻塞的,它只有拼命地计算。
同步阻塞IO是指当前进程调用某些IO操作的系统调用或库函数时,进程便暂停下来,等待IO操作完成后再继续进行,这种模型可以和多进程结合起来有效利用CPU资源,但其代价就是多进程的大内存开销。这种模型的等待时间包括等待数据的就绪和等待数据的复制。
同步非阻塞IO是指调用不会等待数据的就绪,当没数据可读或可写时立即告诉进程,让其函数及时返回。通过反复轮询来尝试数据是否就绪,防止进程被阻塞,最大的一个好处就是可以在一个进程内同时处理多个IO操作。但是反复轮询会大量占用CPU时间,使得进程处于忙碌等待状态。非阻塞IO只对网络IO有效,对磁盘IO无效。
多路IO就绪通知允许进程通过一种方法同时监视所有文件描述符,并可以快速获得所有就绪的文件描述符,然后只针对这些文件描述符进行数据访问。当然,要注意,这种模型在数据访问时仍然要采用阻塞或非阻塞方式进行。
select:通过一个select()系统调用来监视并返回就绪的文件描述符,从而对这些文件描述符进行后续的读写。几乎所有的平台都支持这种方式,可以跨平台,但它的缺点是单个进程可监视的文件描述符数量有最大限制,Linux上一般为1024,它对所有socket进行一次性扫描也存在开销。
poll:与select没有本质区别,只是poll没有最大文件描述符数量限制。它的缺点也是将大理文件描述符的数组在用户态和内核态来回复制,而不管文件描述符是否就绪,开销会成线性增长。
epoll:Linux2.6才出现,具有其它方式的一切优点,是Linux2.6下性能最好的多路IO就绪通知方法。它基于事件的就绪通知方式。
kqueue:性能和epoll差不多,它是FreeBSD下的,但它的API在许多平台下不支持。
内存映射是指将内存中某块地址空间和我们指定的磁盘文件相关联,从而把对这块内存的访问转换为对磁盘文件的访问。内存映射可以提高磁盘IO性能,像访问内存一样地访问磁盘文件。有两种内存映射,共享型和私有型。共享型是指对任何内存的写操作都同步到磁盘文件,而所有映射同一个文件的进程都共享任意一个进程对映射内存的修改。私有型是指映射的文件只能是只读文件,不可以将内存的写同步到文件,多个进程不共享修改。显然,共享型的内存映射效率偏低。
直接IO就是指绕过内核缓冲区,打开的文件可直接访问,避免CPU和内存的多余时间开销。
sendfile系统调用可将磁盘文件的特定部分直接送到客户端的Socket的描述符,加快静态文件的请求速度,减少CPU和内存的开销。
阻塞和非阻塞是指当进程访问的数据尚未就绪,进程是否等待即是立即返回还是继续等待。同步是指主动请求并等待IO操作完成,当数据就绪后读写时必须阻塞。异步是指主动请求数据后可以继续处理其它任务,随便等待IO操作完成的通知,即读写时进程不阻塞。
1.7 服务器并发策略
设计并发策略的目的就是就是让IO操作和CPU计算尽量重叠进行。一方面要让CPU在IO等待不要空闲,另一方面要让CPU在IO调度上尽量花最少的时间。
(1)一个进程处理一个连接,非阻塞IO
这样会存在多个并发请求同时到达时,服务器必然要准备多个进程来处理请求。这种策略典型的例子就是Apache的fork和prefork模式。对于并发数不高的站点同时依赖Apache其它功能时的应用选择Apache还是可以的。
(2)一个线程处理一个连接,非阻塞IO
这种方式允许在一个进程中通过多个线程来处理多个连接,一个线程处理一个连接。Apache的worker模式就是这种典型例子,使其可支持更多的并发连接。不过这种模式的总体性能还不如prefork,所以一般不选用worker模式。
(3)一个进程处理多个连接,非阻塞IO
适用的前提条件就是多路IO就绪通知的应用。这种情况下,将处理多个连接的进程叫做worker进程或服务进程。worker的数量可以配置,如Nginx中的worker_processes 4
(4)一个线程处理多个连接,异步IO
即使有高性能的多路IO就绪通知,但磁盘IO的等待还是无法避免的。更加高效的方法是对磁盘文件使用异步IO,目前很少有Web服务器真正意义上支持这种异步IO。