《构建高性能web站点》读书笔记:CPU/IO并发策略

时间:2024-01-05 10:38:08

服务器并发处理能力:单位时间内处理的请求数,吞吐率,reqs/s

apache的mod_status,显示的 requests/sec,从启动开始的平均计算值。
lighttpd的mod_status显示最近5S的吞吐率。

并发用户数:多少个用户同时向服务器发送请求
总请求数
请求资源描述

100个用户同时发送请求,服务器网卡缓冲区最多有100个等待处理的请求。

Httpwatch可以看到浏览器的并发连接。

实际并发用户数位Web服务器当前维护的代表不同用户的文件描述符总数,即并发连接数。Web会限制同时服务的最多用户数,如apache的MaxClients参数。
多出来的用户请求,会在服务器内核的数据接收缓冲区中等待处理。

最大并发用户数稍稍大于最大并发连接数,如果请求的性质决定了处理每个请求花费的时间少,如1KB的静态网页,那么每个请求都可以快速处理然后释放文件描述符。这时,最大并发用户数可以大于最大并发连接数。

WEB服务器的本质是:以最快的速度将内核缓冲区中的用户请求数据拿回来,然后最快的处理完这些请求,将响应数据放到内核的发送数据缓冲区。

请求等待时间,用户等待时间:
1 用户平均请求等待时间:在一定并发用户数的情况下,对于单个用户的服务质量;
2 服务器平均请求处理时间:服务器的整体服务器质量,是吞吐率的倒数。

压力测试:
ab -n1000 -c10 http://localhost/test.html
-n1000 总请求数为1000
-c10 并发用户数为10

Document Length:响应数据的正文长度。
Time taken for tests:总请求数被处理完所花费的总时间。
Failed requests:失败的请求数,在连接服务器、发送数据、接收数据等环节发生异常,以及无响应后超时的情况。
Total transferred:所有请求的响应数据长度总和,代表从WEB流向用户的数据总长度。
HTML transferred:所有请求的响应数据中正文数据的总和。
Requests per second:吞吐率 Complete requests/Time taken for tests
Time per request:用户平均请求等待时间 Time taken for test/(Complete requests/Concurrency Level)
Time per request(across all concurrent requests):服务器平均请求处理时间 Time taken for tests/Complete requests
Transfer rate:这些请求在单位时间内从服务器获取的数据长度=Total transferred/Time taken for tests
最后部分的数据:描述每个请求处理时间的分布情况。处理时间是指单个用户,平均每个请求处理的时间。

并发策略:服务器同时处理较多请求的时候,如果合理协调并充分利用CPU计算和I/O操作,在较大并发用户数的情况下提供较高的吞吐率。

可以同时处理多个请求,在于操作系统通过多执行流体系设计使得多个任务可以轮流使用系统资源。

CPU并发计算
进程
磁盘I/O,网络I/O花费大部分时间。DMA技术让CPU不参与I/O操作的全过程,CPU发出I/O指令,进程挂起,释放CPU资源,I/O设备通过
总断来通知进程重新就绪。进程担当分配系统资源的实体,也是记录程序实例当前运行到什么程度的一组数据,进程通过进程描述符与这些数据关联。
进程有自己独立的内存地址空间和生命周期。频繁的进程创建和销毁,成为影响性能的主要因素。
子进程会将父进程地址空间的所有数据复制到自己的地址空间,完全继承父进程的上下文信息。父子进程间只能进行通信。

轻量级进程
进程间无法低成本共享数据,采用大量进程的WEB服务器(apache的prefork模型)在处理大量并发请求时,内存会大量消耗。
进程的相互独立带来稳定性和健壮性。
轻量级进程有独立性,但允许共享一些资源(地址空间,打开的文件),但上下文切换的开销还是难免。

线程
线程的接口“pthread”,对内核来说,多线程只是一个进程,由用户态模拟实现的多执行流,线程切换的开销要少,
但是在多处理器的服务器(SMP)中表现较差,内核的进程调度器才有权利分配多个CPU时间。
另一种linuxThreads,是内核级的线程库,原理:线程是一个轻量级进程。

进程调度器
任何时刻只有一个进程处于运行状态,其他进程处于挂起状态并等待就绪,或就绪但等待CPU时间片,其他状态。
内核中的进程调度器(Scheduler)维护一个可运行进程的队列,“运行队列run quere”,及休眠进程和僵尸进程的列表。
调度器根据进程优先级来安排运行队列,调度器也能动态调整进程优先级。体现在进程的nice属性中。

使用top可以看出进程的属性:

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                          
32419 mallwatc  17   0  171m  31m 8300 R  6.6  0.1   0:00.20 php                              
32407 storesoc  17   0  165m  25m 8260 S  5.6  0.1   0:00.17 php                              
32408 storesoc  17   0  165m  25m 8260 S  5.6  0.1   0:00.17 php

进程优先级为Priority,PR列,而优先级动态调整值在top中用NI表示。

lighttp的进程优先级会在20~25间,apache的prefork在15~17间。
Linux进程调度器更加偏爱I/O操作密集型的进程,因为这些进程在发起I/O操作后会阻塞(除了异步I/O),不会占用太多CPU时间,其他进程可以额更好的
交错运行。
PR值是进程调度器分配给进程的时间片长度,单位是时钟个数。如Linux上CPU时钟个数一般为10ms,PR值为15的表示进程的时间片为150ms

在多处理器(SMP)中,给每个处理器分配一个运行队列,进程调度器负责将进程分配到适合的CPU。

系统负载:
可以通过次命令来了解系统负载

#cat /proc/loadavg
8.71 8.06 7.41 2/389 9057
2/389 中,2为此时运行队列中的进程个数,389为此时的进程总数。
最右的9057为最新创建的进程ID

8.71 8.06 7.41表示最近1,5,15分钟计算出的系统负载。在单位时间内运行队列中就绪等待(CPU时间)的进程数平均值。
要提高系统,可以编写一个占用长时间CPU的脚本,比如一个循环累加器。

进程切换
调度器挂起正在运行的进程,恢复以前挂起的某个进程。即“上下文切换”。上下文表示进程运行到何种程度。
挂起进程:将进程在CPU寄存器中的数据拿出来暂存在内核堆栈中;
进程恢复:将数据重新装入CPU寄存器。
这段装入和移出的数据称为“硬件上下文”,进程上下文还包含了进程运行需要的一切状态信息。
nmon工具可以监视服务器每秒上下文切换次数。
nmon -s 300 -c 288 -f -m /tmp

-s 300:表示每300秒采集一次数据,
-c 288 :表示采集288次,300*288=86400秒,刚好是1天的数据,这样运行一次这个程序就会生成一个一天的数据文件,
-m /tmp: 表示生成的数据文件的路径
-f :表示生成的数据文件名中有时间

RunQueue    1    Load    Average
ContextSwitch    28.4    1 mins    0.01
Forks        0.0    5 mins    0.03
Interrupts    253.0    15 mins    0.00

28.4次/秒 是系统不提供其WEB服务器的上下文切换。

apache的prefork通过一个父进程预先创建一定数量的子进程,所有子进程竞争accept用户请求,一旦某个子进程accept成功便开始处理这个请求。
每个子进程处理的请求数由MaxRequestsPerChild 参数控制。

RunQueue    3    Load    Average
ContextSwitch    18166.5    1 mins    1.34
Forks        0.5    5 mins    0.43
Interrupts    300.4    15 mins    0.15

apache为每个并发用户创建独立的子进程来处理请求,查看httpd进程数
#ps afx|grep httpd|wc -l
96
除去grep本身和apache父进程,有94个子进程。用mod_status可以看:
95 requests currently being processed,0 idle workers
95个请求包括mod_status自身的一个。没有空闲子进程。
使用top查看子进程占用的内存

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  SWAP COMMAND                     
30370 wwwaolb2  16   0  282m 142m 8548 R 76.2  0.6   0:19.57 140m php                         
14994 mysql     15   0 4974m 4.5g 5376 S 39.1 19.1   9848:41 358m mysqld                      
30899 soccercl  18   0  192m  52m 8440 R 30.5  0.2   0:01.53 140m php                         
30902 wheretob  18   0  189m  49m 8376 R 19.8  0.2   0:00.99 139m php  
RES为占用的物理内存空间大小;
SWAP为使用的虚拟空间大小;
VIRT为RES+SWAP
可以通过编译参数和httpd.conf的配置减少加载的模块和多余的处理来减少内存开销。

ab测试的结果:
Requests per second:    5479.30[#/sec](mean)
apache平均处理5524.8个/秒请求,平均上下文切换次数为18166.5次/秒

lightted对于静态页,用单进程单线程模型来处理多个请求,对于PHP,用数量较少的fastcgi进程来处理。
其上下文切换只有61.3次/秒,平均处理13323.3次/秒请求,内存开销也小。
如果希望服务器支持较大的并发数,就要尽量减少上下文切换次数,最简单的做法是减少进程数,使用线程并配合其他I/O模型来设计并发策略。

IOWAIT
指cpu空闲并且等待I/O操作完成的时间比例。比如一个任务花费10ms的I/O操作时间和10ms的CPU时间,IOWAI为10/20,等于50%。实际的计算更复杂。
IOWait很高,说明CPU时间开销相对于I/O操作时间来说比较少。

锁竞争
大量并发请求时,存在资源抢占竞争,用“锁”机制控制资源的占用,当一个任务占用资源,锁住资源,等待锁的释放。
尽量减少并发请求对于共享资源的竞争。

系统调用
进程可以在用户态和内核态之间切换,需要一定的开销。运行在用户态,可以使用CPU和内存来完成一些任务,而需要对硬件进行操作时
(读取磁盘,发送网络数据等),就切换到内核态。当内核态任务完成后,切换回用户态。
减少不必要的系统调用,是WEB性能优化的一环。如apache支持.htaccess来为hdocs下各个目录进行局部的参数配置。
<Directoy />
    options FollowSymLinks
    AllowOveride all
    Order deny,allow
    Allow from all
</Directory>
用trace来跟踪apache的一个子进程,获得某次请求处理的一系列系统调用。
一共有30次系统调用,用于检查被访问的文件路径中各级目录下是否存在.htaccess文件,共涉及6次open()系统调用。
gettimeofday()和times()调用是mod_status发起的。关闭.htaccess和mod_status,apache的并发处理能力大幅增加。

内存分配
WEB服务器需要用到大量内存,是的内存的分配和释放尤为重要。
传统的应用,各类表达式锁消耗的最大开销在于中间临时变量的内存分配及数据复制时间。
WEB的大量请求,使得内存堆栈的分配和复制次数更加频繁。可以通过数据结构和算法复杂度来减少数据复制时间。

apache使用多进程模型,内存使用量很大。内存管理方案:内存池策略。一次性申请大片的内存作为内存池,随后需要时只要在内存池中获取,
而不需再次分配。减少频繁的内存分配和释放。也使得内存管理更安全。内存池在apache关闭时会彻底释放。但对性能的弥补没用。

lighttpd使用单进程模型,内存使用小很多。
nginx使用单进程模型,内存使用量更小。内存分配策略:使用多线程来处理请求,可以共享内存资源,减少内存使用;使用分阶段的内存分配策略,
按需分配,及时释放,使得内存使用量保持在很小的数量范围。
nginx的初衷就是支持大并发连接数,而内存的大小是实现的关键。

持久连接
Keep-alive,长连接,一次TCP连接中持续发送多份数据而不断开连接。
短连接:发送一份数据便断开。
长连接对密集型的图片或网页等小数据请求处理有加速作用。
需浏览器和服务器双方支持,浏览器的请求头包含:Connection:Keep-Alive.服务器端开启长连接支持。
长连接的使用可以减少大量重新建立连接的开销,加速性能。

长连接超时时间的设置,如果过长,apache扔维持着连接子进程,一旦并发数过多,就会维持大量空闲进程。

I/O 模型
网络I/O,磁盘I/O。
网络I/O的由带宽最低的交换节点决定。
I/O操作需要内核系统调用来完成,系统调用需要CPU来调度。快速的CPU要等待慢速I/O操作。
让CPU花费足够少的时间在I/O操作的调度上。

PIO:读取磁盘文件到内存,数据需要经过CPU存储转发。
DMA:CPU向DMA控制器下达指令,让DMA控制器来处理数据的传送,DMA控制器通过系统总线来传输数据,传送完毕再通知CPU。

同步阻塞I/O
I/O等待:WEB服务器等待访问,等待建立连接,等待传输数据,等待接受缓冲区,数据复制进进程地址空间等等。
    等待前一个I/O操作完成,等待磁头复位等等。
阻塞:当前发起I/O操作额进程被阻塞。

在通常,对磁盘文件调用read()将阻塞进程,一直到数据被复制到进程用户态内存空进,而对磁盘文件调用write(),
进程会在数据被复制到内核缓冲区后立即返回。
开启O_SYNC,使得write()必须等待数据真正写入磁盘文件才返回。

同步阻塞I/O是指当进程调用某些涉及I/O操作的系统调用或库函数时:accept(),send(),rev()等,进程暂停下来,等待I/O操作完成后再继续运行。
可以和多进程结合起来有效利用CPU资源,代价是多进程的大量内存开销。
比如apache-prefork模型,当某个子进程等待请求时,进程阻塞在accept()调用,用strace -p 进程ID 来查看。
进程实际等待时间包括:数据的就绪和数据的复制。

同步非阻塞I/O
如果数据不可读或不可写(数据未就绪),同步非阻塞I/O会立即告诉进程数据不可用。结合反复轮询来尝试数据是否就绪,防止进程被阻塞,好处是
一个进程可以同时处理多个I/O操作。反复轮询会消耗CPU时间,使进程处于忙碌等待状态。
只针对网络I/O有效。在socket设置O_NONBLOCK即可。
网络的是send()或recv()系统调用。

多路I/O就绪通知
WEB服务器处理大量的文件描述符。
多路I/O就绪通知:允许进程通过一种方法来同时监听所有文件描述符,并可以快速获得所有就绪的文件描述符,然后只针对这些文件描述符进行数据访问。
I/O就绪通知只是快速获得就绪的文件描述符,访问数据扔需选择阻塞或非阻塞的访问方式,一般选择非阻塞方式,防止意外的等待阻塞进程。就绪通知有时
只代表一个内核的提示,而此时的文件描述可能被客户端关闭了。

UNXI系类中:
select()系统调用监视包含多个文件描述符的数组。数组个数为1024。假如已经维持了1024个连接,新的连接会被拒绝。
poll()和select()区别是没有最大文件描述符限制。但一样会将包含大量文件描述符的数组整体复制到用户态和内核的地址空间,不论文件描述符是否就绪。
当二者将就绪的文件描述符告诉进程后,如果进程没有进行I/O操作,再调用时,二者都会再次报告。不会丢失就绪的消息,称水平触发(level tiggered)

SIGIO告诉进程哪些文件描述符刚刚变为就绪状态,如果没有采取行动,将不会再次告知,称边缘触发(Edge Triggered)。
通过实时信号(real time signal)实现通知,由内核中的事件队列来维持。可能造成信号到达时事件已过期。队列长度有限,易发生事件丢失。

/dev/poll:将文件描述符数组写入虚拟设备/dev/poll,进程通过ioctl()来等待事件通知,返回就绪的文件描述符后,
进程可以从/dev/poll读取所有就绪的文件描述符数组。节省了扫描所有文件描述符的开销。

/dev/epoll:补丁,提供/dev/poll的功能,增加了内存映射(mmap)技术。
epoll: 2.6内核公认最好的多路I/O就绪通知方法。
同时支持水平触发和边缘触发。理论上边缘触发性能更好,代码实现复杂。使用边缘触发,需修改源码重新编译,增加EPOLLET选项。
epoll返回一个代表就绪描述符数量的值,进程只需去epoll指定的数组中一次取得相应数量的文件描述符即可,也使用了内存映射技术,省掉了文件描述符
在系统调用时复制的开销。
epoll采用事件的就绪通知方式,一旦某个文件描述符就绪时,内核会迅速激活这个文件描述符以供进程使用。

内存映射
将内存中某块和要指定的磁盘文件相关联,对这块内存的访问转换为对磁盘文件的访问。不使用read()或write()调用,使用mmap()调用建立内存和磁盘文件
的关联,像访问内存一样访问文件。减少I/O操作。
共享型内存映射:将任何对内存的写操作都同步到磁盘文件,所有映射同一个文件的进程都共享任意进程对映射内存的修改;
私有型:    映射的是只读文件。不能将对内存的写同步到文件。
共享性映射效率偏低,同步花费。

apache对于较小的静态文件使用内存映射来读取。用srace跟踪子进程:
---------------------------------------------------------------
open("/data/www/site/htdocs/test.htm",o_rdonly|o_largefile)=20
mmap2(NULL,151,PROT_READ,MAP_SHARED,20,0)=0Xb7f8f000
writev(19,[{"HTTP/1.1 200 OK\r\nDate:HU,12 F"...,278},{"<html>\n<head>\n<title>Wlecome to"...,151}],2)=429
munmap(0Xb7f8f000,151)
---------------------------------------------------------------
访问的是/data/www/site/htdocs/test.htm静态文件,151字节。

用open()打开这个文件,获得文件描述符为20,通过mmap2()系统调用完成了共享型内存映射的关联。
随后,apache读取文件中的内容,这个操作没有使用系统调用,只是进程读取地址空间数据的用户态行为。
使用writev()系统调用将http相应的头信息和151字节的正文数据合并后发送
nummap()撤销映射。

直接I/O
内存映射和直接访问文件没有区别。数据从进程用户态内存到磁盘要进过两次复制:磁盘与内核缓冲区之间,内核缓冲区与用户态内存空间。
内核缓冲区的目的是为了提高磁盘文件的访问性能,当进程要读取磁盘文件时,如果在内核缓冲区中有,就不访问磁盘。当进程写文件时,
实际是写在内核缓冲区,便返回写成功,真正写入磁盘是通过一定的策略延迟写。

而数据库,为了实现自己的I/O管理策略,要绕过内核缓冲区,系统提供了直接I/O,open()系统调用增加参数O_DIRECT,用它打开的文件可以绕过
内核缓冲区。
mysql的innodb引擎对内核缓冲区的依赖小。实现直接I/O的方法,是使用raw分区跳过内核缓冲区。
在my.cnf中
innodb_data_file_path=/dev/sda5:100Gnewraw
/dev/sda5是raw分区。raw分区用raw设备管理程序来加载。
或者通过:
innodb_flush_method=O_DIRECT
O_SYNC 只对写数据有效,将写入内核缓冲区的数据立即写入磁盘,将数据丢失概率减少,仍需经过内核缓冲区。

sendfile
在向web请求静态文件的过程中,磁盘文件数据先经过内核缓冲区,然后到达进程用户态内存空间,静态数据直接被送到网卡对应的内核缓冲区,接着被
送入网卡进行发送。

静态数据从内核缓冲区绕一圈又回到内核缓冲区,浪费了CPU和内存的开销。系统提供sendfile(),将磁盘文件直接传送到代表客户端的socket描述符,
加快静态文件的请求速度。

apache使用mmap()系统调用来处理小文件,而处理大文件是,用sendfile64()。用strace跟踪进程。
-----------------------------------
open("../test.mp4",O_RDONLY|O_LARGEFILE)=20
setsockopt(19,SOL_TCP,TCP_CORK,[1],4)=0
writev(19,[{....}],1)=287
sendfile64(19,20,[0],1186854)=48865
setsockopt(19,SOL_TCP,TCP_CORK,[0],4)=0
poll([{fd=19,events=POLLOUT,revents=POLLOUT}],1,300000)=1
sendfile64(19,20,[48865],1137989)=98304
poll([{fd=19,events=POLLOUT,revents=POLLOUT}],1,300000)=1
......
-----------------------------------
apache用open()打开请求的1.2M的文件,获得文件描述符为20,通过writev()发送出HTTP响应头,接着对1.2M的正文数据使用sendfile64()进行分段发送
(98304字节),其调用参数包含了两个文件描述符,代表磁盘文件的20和代表客户端的19
apache可以关掉sendfile: EnableSendfile off

对于请求较小的静态文件,sendfile发挥的作用很小。
处理小文件请求时,发送数据的环节在整个过程中所占的时间很小。

ab测试中,connect指TC连接的时间,processing指发送文件的时间。

异步I/O
同步和异步是指访问数据的机制。
同步指进程主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞进程;
异步指进程主动请求数据后便可以继续处理其他任务,随后等待I/O操作完毕的通知,使进程在数据读写时也不阻塞。

服务器并发策略
请求封装在IP包中,位于网卡的接收缓冲区。WEB服务不断地读取这些请求,然后进行处理,并将结果写到发送缓冲区。
设计并发策略的目的,是让I/O操作和CPU计算尽量重叠进行:让CPU在I/O等待时不要空闲;让CPU在I/O调度上尽量花费最少时间。

一个进程处理一个连接,非阻塞I/O
在并发请求时,WEB服务要准备多个进程来处理请求。
fork模式,主进程负责accept()连接,fork()新的worker进程处理新连接,处理结束后,进程便被销毁。fork()开销影响性能的关键。
prefork模式,主进程预先创建一定数量的子进程,每个请求由一个子进程来处理,切每个子进程可以处理多个请求。主进程根据负载来调整子进程的数量。
对于accetp(),有两种策略:
1 主进程用非阻塞accetp()来接收连接,当建立连接后,主进程将任务分配给空闲的子进程来处理;
2 所有子进程使用阻塞accetp()来竞争连接,一旦一个子进程建立连接后,它将继续进行处理。

阻塞和非阻塞时指当进程访问的数据如果尚未就绪,进程是否需要等待。
apache采用第2种策略。
按大多数TCP栈的实现方式,当一个请求连接到达时,内核会激活所有阻塞在accetp()的子进程,但只有一个能够成功获得连接并返回到用户空间,而其余
的子进程得不到连接而继续回到休眠状态,“抖动”造成一定的开销。

对于接收请求数据的I/O操作,apache采用了非阻塞read(),可用用strace跟踪来查看
apache通过fcntl64()系统调用将accetp()获得的文件描述符19设置为O_NONBLOCK(非阻塞)模式。
处理完请求后,apache接着使用poll()检查当前的socket连接是否有新的请求数据到达。
当客户端关闭socket连接后,poll()的POLLHUP事件会被触发,但有时会触发POLLIN事件,所以apache再次调用read()来接收,如果发现获得
0字节,便认为客户端关闭,调用close()关闭连接。

使用长连接可以减少accept()和poll()调用。

apache的多进程模型限制了并发连接数,但是独立的子进程保证了稳定性和兼容性。子进程的奔溃不会影响到apache本身。对并发数要求不高(150以内)。

一个线程处理一个连接,非阻塞I/O
一个进程建立多个线程,一个线程处理一个连接。apache中的worker多路处理模块,主进程会创建很少的子进程,每个子进程又拥有一定数量的线程。
apache的线程是由内核进程调度器管理的轻量级进程,它们的上下文切换开销依然存在。

一个进程处理多个连接,非阻塞I/O
一个进程处理多个连接,存在一潜在的条件,就是多路I/O就绪通知的应用。通常将处理多个连接的进程称为worker进程,或服务进程。
nginx的参数:worker_processes 设置worker进程数量。

这种模型,有时会设计独立的Listener进程,专门负责接收新的连接,然后分配任务给各个worker,可以根据worker的负载来平衡调度任务,但是
会增加开销。所以一般都是由worker进程来接收。

设置多路I/O就绪通知方法。epoll只关注活跃连接,不在死连接上浪费时间。
图片服务器,为用户提供网页中大量图片的快速下载,采用长连接方式,会处于空闲状态,用epoll仍能很好的工作。
大量的worker进程可以维护更多的活跃连接数,但带来更多的上下文切换和内存开销,整体上使所有连接的响应时间变长。

一个线程处理多个连接,异步I/O
对磁盘文件调用read()或sendfile()发送数据,如果数据不再磁盘缓冲区,磁盘便开始用物理设备来读取数据,整个进程的其他工作都必须等待。
对磁盘文件使用异步I/O,目前WEB服务较少支持。对于大量小文件的并发请求,文件传送可能不是关键(PDF是大文件)