服务器并发处理能力 - 2
一台Web服务器在单位时间内能处理的请求越多越好,这也成了Web服务器的能力高低所在,它体现了我们常说的"服务器并发处理能力"。
吞吐量
Web服务器的并发处理能力,一般使用单位时间内服务器处理的请求数来描述其并发能力,习惯称其为吞吐率(Throughput),单位是"reqs/s"。
并发用户数
人们常常把并发用户数和吞吐率混淆,实际上,它们并不是一回事,吞吐率是指在一定并发用户数的情况下,服务器处理请求能力的量化体现;并发用户数就是指在某一时刻同时向服务器发送请求的用户总数。
采用多线程的并发对于同一个域名下的URL的并发下载数是有最大限制的,具体限制视浏览器的不同而不同,比如,在HTTP/1.1下,IE7支持两个并发连接,IE8支持6个并发连接,Firefox3支持4个并发连接。另一方面,Web服务器一般也会限制同时服务的最多用户数,比如Apache的MaxClients参数。
CPU并发计算
服务器之所以可以同时处理个请求,在于操作系统通过多执行流体系设计使得多个任务可以轮流使用系统资源,这些资源包括CPU、内存以及I/O等。
进程
多执行流的一般实现便是进程。
进程的调度由内核来进行,从内核的观点来看,进程的目的就是担当分配系统资源的实体。同时,进程也可以理解为记录程序实例当前运行到什么程度的一组数据,多个进程通过不同的进程描述符与这些数据进程关联。
每个进程都有自己独立的内存地址和生命周期。进程的创建使用fork()系统调用。
轻量级进程
由于进程之间相对独立,无法共享数据。为此,在Linux2.0之后,提供了对轻量级进程的支持,它由一个新的系统调用clone()来创建,并由内核直接管理,像普通的进程一样独立存在,各自拥有进程的描述符,但是这些进程已经允许共享一些资源。
轻量级进程减少了内存的开销,并为多进程应用程序的数据共享提供了直接支持。
线程
POSIX 1003.1c为Linux定义了现成的接口"pthread"。从内核角度来看,多线程只是一个普通的进程,它是由用户态通过一些库函数模拟实现的多执行流,所以多线程的管理完全在用户态完成。这种实现方式下线程切换的开销相比于进程和轻量级进程要少,但它在多处理器的服务器(SMP)表现较差,因为只有内核的进程调度器才有权分配多个CPU的时间。
POSIX线程的另一种是现实LinuxThreads,它可以说是内核级线程(Kernel-Level Threads),因为它通过clone()来创建线程,也就是说,它的实现原理是将线程和轻量级进程一对一关联,每个线程实际上就是一个轻量级进程,这样使得线程完全由内核的进程调度器来管理,所以他对于SMP的支持较好,但线程切换的开销要比用户态线程多一些。
LinuxThreads已经加入了glibc和libc的目前版本。
进程调度器
内核中的进程调度器(Scheduler)维护着各种状态的进程队列。在Linux中,进程调度器维护者一个包含所有可运行进程的队列,称为"运行队列",以及一个包括所有休眠进程和僵尸进程的列表。
进程调度器的一项重要工作就是决定下一个运行的进程,这个执行先后顺序通过进程优先级进行处理,进程优先级由进程自己决定,但在进程运行时进程调度器会动态的调整它们的优先级。
Linux中进程的优先级属性为Priority,在top结果中用PR表示,而对于进程的动态调整体现在进程的nice属性,在top结果中用NI表示。
PR所代表的是进程调度器分配给进程的时间片长度,单位是时钟个数,一个时钟需要多长时间跟CPU的主频以及操作系统平台有关,比如Linux上一般为10ms,那么PR值为20则表示这个进程的时间为200ms。
Linxu2.6的进程调度器偏爱I/O操作密集型的进程,因为这些进程在发起I/O操作后通常都会阻塞(除异步I/O),不会占用太多CPU时间。
系统负载
在任何时刻通过 /proc/loadavg,可以查看系统运行队列的情况。
其中6/1564,6表示测试运行队列中的进程个数,而1564则表示此时的进程总数。最右边的25689表示到此时为止,最后创建的一个进程ID。左边的0.24、0.52、0.58三个数值分别表示最近1分钟、5分钟、15分钟的系统负载。
系统负载是在单位时间内运行队列中就绪等待的进程的平均值,系统负载越高,代表CPU越繁忙。
进程切换
为了让所有的进程可以轮流使用系统资源,进程调度器在必要的时候挂起正在运行的进程,同时恢复以前挂起的某个进程,这种行为称为进程切换,也就是"上下文"。
一个进程被挂起的本质就是将它在CPU寄存器中的数据拿出来暂存在内核态堆栈中,而一个进程恢复工作的本质就是将它的数据重新载入CPU寄存器。
Nmon是一个Linux监视工具,它可以提供基于服务器终端命令的监视页面。利用Nmon对服务器某个时刻的抽样结果如下:
ContextSwitch表示这时上下文切换平均每秒12614.9次,这是操作系统正常运转所进行的必要工作。
IOWait
IOWait是指CPU空闲并且等待I/O操作完成的时间比例。IOWait往往不能代表I/O操作的性能,它的设计出发点是用来衡量CPU性能的。
IOWait很高的时候,说明当前任务的CPU时间比I/O操作时间比较少,通过Nmon得到CPU监控数据如下图:
此时IOWait为0%,系统不繁忙。
转载请并标注: “本文转载自 linkedkeeper.com (文/张松然)”
系统调用
进程有两种运行模式:用户态和内核态。进程通常在用户态,这时可以使用CPU和内存,而当进程需要对硬件外设进行操作的时候(如读取磁盘文件、发送网络数据),就必须切换到内核态,当在内核态的任务完成后,进程又切回到用户态。
由于系统调用涉及进程从用户态到内核态的切换,导致一定的内存空间交换,这也是一定程度上的上下文切换,所以系统调用的开销通常是比较昂贵的。
减少不必要的系统调用,也是Web服务器性能优化的一个方面。
我们使用 strace 来跟踪 Nginx 的一个子进程,获得某次请求处理的一系列系统调用,如下所示:
内存分配
Apache在运行时的内存使用量是非常惊人的,这主要归咎于它的多进程模型,该模型使得Apache在运行开始便一次性申请大片的内存作为内存池。而Nginx的内存分配策略,它使用多线程来处理请求,这使得多线程之间可以共享内存资源,从而令它的内存总体使用大大减少,Nginx维持10000个非活跃HTTP持久连接只需要2.5MB内存。
持久连接
持久连接(Keep-Alive)也称为长连接。HTTP/1.1对长连接有了完整的定义,HTTP请求数据投中包含关于长连接的生命:
1
| Connection : Keep-Alive |
长连接的有效使用可以减少大量重新建立连接的开销,有效的加速性能。对于Apache这样的多进程模型来说,如果长连接超时时间过长,那么即便是浏览器没有任何请求,而Apache仍然维持着连接的子进程,一旦并发用户数较多,那么Apache将维持着大量空闲进程,严重影响了服务器性能。
I/O模型
有人说,比特天生就是用来别复制的,数据的生命意义便在于输入输出。
事实上,如何让高速的CPU和慢速的I/O设备更好的协调工作,这是从现代计算机诞生到现在一直探索的话题。
PIO与DMA
很早以前,磁盘和内存之间的数据传输是需要CPU控制的,也就是说如何读取磁盘文件到内存,数据要经过CPU存储转发,这种方式称为PIO。
后来,DMA(直接内存访问,Direct Memory Access)取代了PIO,它可以不经过CPU而直接进行磁盘和内存的数据交换。在DMA模式下,CPU只需要向DMA下达指令,由DMA来处理数据的传送即可,DMA通过系统总线来传输数据,传送完毕通知CPU,这样降低了CPU占有率。
同步阻塞I/O - BIO
阻塞是指当前发起I/O操作的进程被阻塞,而不是CPU被阻塞。
举个例子,比如你去逛街,饿了,你看到小吃城,就在一家面馆买了一碗面,交了钱,可面条做起来需要时间,你知不知道什么时候可以做好,只好坐在那里等,等面条做好吃完再继续逛街。—— 这里吃面条便是I/O操作。
同步非阻塞I/O - NIO
在同步阻塞I/O中,进程实际上等待的时间包括两部分,一个是等待数据的就绪,另一个是等待数据的复制(copy data from kenrel to user)。
同步非阻塞I/O的调用不会等待数据的就绪(wait for data),如果数据不可读或者不可写,它会立刻告诉进程。
回到买面的故事,假如你不甘心等面条做好就想去逛街,可又担心面条做好了没有及时领取,所以你逛一会便跑回去看看面条是否做好,往返了很多次,最后虽然即使吃上了面条,但是却累得气喘吁吁。
多路I/O就绪通知
多路I/O就绪通知的出现,提供了对大量文件描述符就绪检查的高性能方案,它允许进程通过一种方法来同时监视所有文件描述符,并可以快速获得所哟就绪的文件描述符,然后只针对这些文件描述符进行数据访问。
回到买面的故事,加入你不止买了一份面,还在其他小吃店买了饺子、粥、馅饼等,这些东西都需要时间来制作。在同步非阻塞I/O模型中,你要轮流不停地去各个小吃店询问进度。现在引入多路I/O就绪通知后,小吃城在大厅里安装了一块电子屏幕,以后所有小吃店的食物做好后,都会显示在屏幕上,这样你只需要间隔性地看看大屏幕就可以了也许你还可以同时逛逛附近的商店。
需要注意的是,I/O就绪通知只是帮助我们快速获取就绪的文件描述符,当得知就绪后,就访问数据本身而言,让然需要选择阻塞或非阻塞的方式,一般我么你选择非阻塞方式。
多路I/O就绪有很多不同的实现:
select
select 最早于1983年出现在4.3BSD中,它通过一个select()系统调用来监视包含多个文件描述符的数组,当select()放回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前在所有平台上都支持,但select的一个缺点在于单个进程能够监视文件描述符数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但是调用select()会对所有socket进行一次线性扫描,所以这也会浪费一定的开销。
poll
poll在1986年诞生于System V Release3(UNIX),它和select本质上没有多大差异,除了没有监视文件数量的限制,select 缺点同样适用于 poll。
另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行I/O操作,那么下次调用的时候将再次报告这些文件描述符,所以一般不会丢失就绪通知的消息,这种方式称为水平触发(Level Triggered)。
SIGIO
Linxu2.4提供SIGIO,它通过实时信号(Real Time Signal)来实现select/poll的通知方式,但是它们的不同在于,select/poll告诉我们哪些文件描述符是就绪的,一直到我们读写之前,每次select/poll都会告诉我们;而SIGIO则是告诉我们哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它就不会再告诉我们,这种方式称为边缘触发(Edge Triggered)。
/dev/poll
Sun在Solaris中提供了新的实现,它使用虚拟的/dev/poll设备,你可以将要监视的文件描述符数组写入这个设备,然后通过ioctl()来等待时间通知,当ioctl()放回就绪的文件描述符后,你可以从/dev/poll中读取所有就绪的文件描述符数组,这点类似于SIGIO。
在Linux下有很多方法可以实现类似/dev/poll的设备,但是都没有 提供直接的内核支持,这些方法在服务器负载较大时性能不稳定。
/dev/epoll
随后,名为/dev/epoll的设备以不定的形式出现在Linux2.4上,它提供了类似/dev/poll的功能,而且增加了内存映射(mmap)技术,在一定程度上提高了性能。
但是,/dev/epoll仍然只是一个补丁,Linux2.4并没有将它的实现加入内核。
epoll
直到Linux2.6才出现了有内核直接支持的实现方法,那就是epoll,它被公认为Linxu2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发和边缘出发,在默认情况下,epoll采用水平触发,如果要使用边缘出发,需要在事件注册时增加EPOLLET选项。
在Nginx的epoll模型代买(src/event/modules/ngx_epoll_module.c)中,可以看到它采用了边缘触发:
1
| ee.events = EPOLLIN | EPOLLOUT | EPOLLET; |
另外一个本质的改进在于epoll采用基于事件的就绪通知方法。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通知epoll_ctl()来注册每一个文件描述符,一旦某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
回到买面的故事,虽然有了电子屏幕,但是显示的内容是所有食品的状态,包括正在制作和已经做好的,这显然给你造成阅读上的麻烦,就好像select/poll每次返回所有监视的文件描述符一样,如果能够只显示做好的食品,随后小吃城进行了改进,就像/dev/poll一样只告知就绪的文件描述符。在显示做好的食品时,如果只显示一次,而不管你有没有看到,这就相当于边缘出发,而如果在你领取之前,每次都显示,就相当于水平触发。
但尽管如此,一旦你走远了,还得回到小吃城去看电子屏幕,能不能让你更加轻松地获得通知呢?小吃城采取了手机短信通知的方式,你只需要到小吃城管理处注册后, 便可以在餐点就绪时及时收到短信通知,这类似于epoll的事件机制。
内存映射
Linux内核提供一种访问磁盘文件的特殊方式,它可以将内存中某块地址空间和指定的磁盘文件相关联,从而把这块内存的访问转换为对磁盘文件的访问,这种技术称为内存映射(Memory Mapping)。
使用内存映射可以提高磁盘I/O性能,它无需使用read()或write()等系统调用来访问文件,而是通过mmap()系统调用来建立内存和磁盘文件的关联,然后想访问内存一样访问磁盘。
直接I/O
在Linux2.6中,内存映射和直接访问磁盘文件没有本质上差异,因为数据从进程用户态内存空间到磁盘都要经过两次复制,即"磁盘-->内核缓冲区"和"内核缓冲区-->用户态内存空间"。
引入内核缓冲区的目的在于提高磁盘文件的访问性能,因为当进程需要写磁盘文件时,实际上只是到了内核缓冲区便告诉进程已经成功。然而,对于一些复杂应用,如数据库服务器,它们为了充分提高性能,希望绕过内核缓冲区,由自己在用户态空间实现并管理I/O缓冲区。
Linxu提供了对这种需求的支持,即在open()系统调用中增加了参数选项O_DIRECT,用它打开的文件便可以绕过内核缓冲区的直接访问,这样便避免了CPU和内存的多余开销。
在MySQL中,对于Innodb存储引擎,其自身可以进行数据和索引的缓存管理,所以对于内核缓冲区的依赖不是那么重要。
sendfile
在向Web服务器请求静态文件的过程中,磁盘文件的数据要先经过内核缓冲区,然后到用户态内存空间,因为是不需要处理的静态数据,所以它们又被送到网卡对应的内核缓冲区,接着在被送入网卡进行发送。
数据从内核出去,又回到内核,没有任何变化。在Linux2.4的内核中,引入了一个称为khttpd的内核级Web服务器程序,它只处理静态文件的请求。引入的目的便在于内核希望请求的处理尽量在内核完成,减少内核态的切换以及用户态数据复制的开销。
Linux通过系统调用将这种机制提供给开发者,那就是sendfile()系统调用。它可以将磁盘文件的特定部分直接传送到代表客户端的socket描述符。
异步I/O
阻塞和非阻塞是指当进程访问的数据如果尚未就绪,进程是否需要等待。
同步和异步是指访问数据的机制,同步指请求并等待I/O操作完毕方式,当数据就绪后在读写的时候必须阻塞;异步是指请求数据后便可以继续处理其他任务,随后等待I/O操作完毕的通知,这使进程在数据读写时不发生阻塞。
转载请并标注: “本文转载自 linkedkeeper.com (文/张松然)”