构建高性能站web站点第三章服务器并发处理能力(1)

人们总是希望少花钱多办事, 同样的,一台Web服务器在单位时间内能处理的请求越多越好,这也成了Web服务的能力高低所在,它体现了我们常说的“服务器并发处理能力”。值得一提的是,本章所说的服务器,主要指用于提供HTTP服务的服务器,但是本章所涉及的一些关于操作系统和内核内容,并不局限于Web服务器。

3.1 吞吐率

说到Web服务器的并发能力,那就一定得有一个量化的描述,我们一般使用单位时间内服务器的请求数来描述其并发能力,但是听起来有点长,我们习惯称其为吞吐率(Throuthput),单位是“reqs/s”。需要注意的是,吞吐率这个概念有时候还用于描述其他指标,比如单位时间内的通信数据量等,但是在本书中,吞吐率指Web服务器单位时间内处理的请求数。

在一些常见的Web服务器软件中,通常会提供当前服务器运行状况能及吞吐率的查看方法,帮助我们对服务器的吞吐率随时进行了解和监控,比如Apache的mod_status模块提供了如下统计:

Current Time: Tuesday, 04-Sep-2018 10:29:05 Öйú±ê׼ʱ¼ä

Restart Time: Monday, 03-Sep-2018 09:44:27 Öйú±ê׼ʱ¼ä

Parent Server Config. Generation: 1

Parent Server MPM Generation: 1

Server uptime: 1 day 44 minutes 38 seconds

Server load: -1.00 -1.00 -1.00

Total accesses: 80616 - Total Traffic: 1.7 GB

扫描二维码关注公众号,回复: 3223520 查看本文章

.905 requests/sec - 20.0 kB/second - 22.1 kB/request

7 requests currently being processed, 1913 idle workers

这里的.905 requests/sec便是此时的吞吐率,它是从Apache启动到目前时刻平均计算值。而在Lighttpd的mod_status模块中,可以更好地展示最近5s的吞吐率,统计数据如表3-1所示。

表3-1 通过mod_stauts查看lighhttpd的实时吞吐率

它分别展示了从Lighttpd启动到目前时刻的吞吐率以及最近5s的吞吐率,还包括单位时间的流出数据量。

吞吐率和压力测试

单从定义来看,吞吐率描述了服务器在实际运行期间单位时间内处理的请求数,然而,我们更关心的是服务器并发处理能力的上限,也就是单位时间内服务器能够处理的最大请求数,最最大吞吐率。所以我们普遍使用“压力测试”的方法,通过模拟足够数目的并发用户数,分别持续发送一定的HTTP请求,并统计测试持续的总时间,计算出基于这种“压力”下的吞吐率,即为一个平均计算值。

提示:

在后面的内容中,我们对吞吐率和最大吞吐率不做特意区分,请大家根据上下文来理解。

另一方面,在Web服务顺的实际工作中,其处理的HTTP请求通常包括对很多不同资源的请求,也就是请求不同的URL,比如这些请求有的是获取图片,有的是获取动态内容,显然服务器处理这些请求所花费的时间各不相同,而这些请求的不同组成比例又是不确定的,那么我们在压力测试的时候是否要模拟这种不同请求交错的情况呢?你也许会说这才是实际情况下的吞吐率,可以帮助我们了解服务器在实际情况下的并发处理能力。

没错,在有些时候,我们需要了解服务器在业务环境中实际的处理能力,但是这些不同性质的请求交错在一起,形成的资源需求模型过于复杂,也就是对于CPU处理和I/O操作需求涉及太多因素,给我们考察服务器并发处理能力带来了难以相像的困难。我们知道在一个含有多变量的数据模型中求解最优化结果非常困难的,所以我们一般都简化模型,以同一个特定的有代表性的请求进行压力测试,然后根据需要。对多个请求的吞吐率按照比例计算加权平均值。

往往也正是因为这些请求性质不同,Web服务器并发能力强弱的关键便在于如何针对不同的请求性质来设计最优并发策略,这在后续的章节中会有详细介绍。同是也是因为有时候一台服务器要同时处理诸多不同性质的请求,在一定程度上使得Web服务顺的性能无法充分发挥,这很容易理解,就像银行对不同型业务设立不同的窗口一样,这些窗口的服务员分别熟悉自己的窗口业务,可以为不同的客户分别快速办理业务,但是如果让这些窗口都可以办理所有业务,也就是客户可以去任何窗口办理任何业务,那会怎么样呢?我想没几个银行业务员会对所有业务都轻车熟路,这样势必会影响到整体业务办理速度。

压力测试的前提条件

这么一来,我们要统计吞吐率,便存在一些潜在的问题,那就是压力的描述和请求性质的描述。

压力的描述一般包含两部分,即并发用户数和总请求数,也就是模拟多少用户同时向服务器发送多少个请求,随后会有关于并发用户数的详细介绍。

请求性质则是对请求的URL所代表的资源的描述,比如1KB大小的静态文件,或者包含10次数据库查询的动态内容等。

所以,吞吐率的前提包括如下几个条件:

  • 并发用户数
  • 总请求数
  • 请求资源描述

并发用户数

前面提到的并发用户数,在我们开始进行压力测试之前,一定要弄明白它的含义。

简单地说,并发用户数就是指在某一时刻同是向服务器发送请求的用户总数。如此多的用户同时请求服务器,显然会给服务器带来不少的压力,这时你可能有一个实际的问题,假如100个用户同时向服务器分别进行10次请求,与1个用户向服务器连贯进行1000次请求,效果一样吧?也就是说的服务器带来的压力一样吗?

虽然看起来服务器都需要连贯处理1000个请求,其实关键的区别就在于,是否真的“连续”。首先有一点需要明白,对于压力测试中提到的每一个用户,连续发送请求实际上是批在发送一个请求。这样一来,从微观层面来看,1个用户向服务器连续进行1000次请求的过程,任何时刻服务器的网卡接收缓冲区中只有来自该用户的1个请求,而100个用户同时向服务器分别进行10次请求的过程中,服务器网卡接收缓冲中最多有100个等待处理的请求,显然这时候服务器的更大。

其实,并发用户数这个词你也许经常听到,很多时候它还有一些别名,比如并发、并发连接数等。经常会有人说某个Web服务器对支持多少并发数,除此之外没有任何上下文,这让很多人摸不着头脑,人们常常把并发用户数和吞吐率混淆,它们并不是一回事,通过前面的介绍,我们很清楚,吞吐率是指在一定并发用户数的情况下,服务器处理请求能力的量化体现。

那么,说一个服务器最多支持多少并发用户数,这个“最多”到底是什么意思?这里我们暂且抛开技术的因素,从广义的角度来看,举个生活中的例子。

比如某商城里有一个柜台,给顾客们办理业务。刚开始顾客稀少,一次中来一个顾客,柜台业务员很轻松就可以搞定,不久,有很多顾客去柜台办理业务,大家排成一个长队依次办理,每个顾客在办理业务的过程中,都需要花时间填写一些资料,这时候其他顾客就得等着,而且柜台业务员闲着无聊又不能干别的事情,所以他觉得很学浪费时间,就让大家排成两队,这样在等待的时候就给另一个队办理。可以填写资料的时间有点长,另一队的顾客开始填写资料时,前一个队的顾客还没填完,业务员还是得等。最后队伍增加到了10队,10个人同时办理业务,刚好业务员不用等待任何顾客了,而且每个顾客对办理速度也很满意。

可是随着前来办理业务的顾客越来越多,业务员想做点有挑战的事情,他白马书院队伍增加到了20队,同时给20个顾客办理业务,这下可和得了,20条队伍拥挤不堪,原本轻松的保安为了维持秩序累得气喘吁吁,这不不算,关键是业务员快疯了,他的办理速度已经赶不上大家填写资料的速度了,一些人填完资料后没搭理,便开始抱怨,业务员也因为同是要给很多很多人办理业务,脑子反应不过来,导致处理原来熟悉的业务时也变得手忙脚乱,效率下降并且时常伴随一些低级失误。

终于,在大家的一片声讨下,柜台崩溃了。注意,柜台的崩溃不是因为业务员无法继续工作了,虽说他忙得累死累活,但是活儿还得干啊,关键是顾客们等不耐烦了,大部分顾客都无法忍受长时间的等待以及办理过程中出现的错误,所以投诉到了商场的管理处,商城只好暂时关闭柜台业务,商讨对策。

很快,商场又恢复了柜台业务,将柜台的队伍数调整到10队,同时根据顾客流量,临时增高一定数量的柜台,这样一来,顾客们纷纷表示满意。

我们可以说,这个柜台支持的最大并发数为10,因为恰好在这个并发数下,柜台型业务开展得非常成功,顾客们都对服务时间非常满意,而且此时代表业务办理次数的柜台吞吐率也比较高,商城和顾客实现双赢。当并发数少于10的时候柜台业务员的时间得不到充分利用,浪费了柜台的宝贵资源,这时候的吞吐率要低一些。而当并发数大于10的时候,事实证明,顾客们不乐意了。这样看来,问题的本质变得非常清晰,似乎就是商家和顾客的博弈,而且是合作型博弈,最大并发数便是博弈的结果,也是最大程序的共赢。

可见,通常所讲的最大并发数是有一定利益前提的,那就是服务器和用户双方所期待的最大收益,服务器希望支持高并发数及高吞吐率,而用户不管那么多,只希望等待较少的时间,或者得到更快的下载速度,显然,双方不可能都彻底满足,所以便存在讨价还价的余地。同时双方也都有能够忍受的最低尺度。从经济学的角度看,就这么简单,氢找到双方利益的平衡点,便是我们所希望的最大并发用户数。这种现象在后结章节中介绍下载服务的时候还会提到。

当然,经过权衡后,我乌日娜希望的最大并发用户数,还存在一定的技术制约。这也是狭义层面的最大并发数定义。柜台的故事只是一个简单的模型,而在我们访问实际的web站点时,每个请求的处理过程可并不像柜台业务员给顾客办理业务那么简单,尤其是在并发用户数较大的情况下,Web服务器使用什么样的并发策略,是影响最大并发数的关键。

另外值得说明的是,即使我们通过压力测试得出服务器的最大并发数,但这与实际并发用户却是两回事,因为有多少用户同时发来的请求并不是服务器所能决定的,该来的总是要来,那是客观存在的。一旦实际并发用户数大于服务器能支持的最大并发数,那必然造成一部分用户需要 等待超过预期的时间,影响了站点的服务质量。所以,得出最大并发数意义,在于了解服务器的承载能力,并且结合用户规模考虑适当的扩展方案。从站点的某些商业角度来看,最大并发用户数的支持程度往往比吞吐率更加容易理解。

在考虑实际用户规模的时候,我们还需要了解一点。用户访问Web站点通常使用浏览器,而浏览器在下载一个网页以及网页中的多个组件时,采用多线程的并发下载方式,但是对于同一域名下URL的并发下载数是有最大限制的,具体限制视浏览器的不同而不同,比如在HTTP/1 .1下,IE7支持两个并发连接,IE8支持6个并发连接,Firefox3支持4个并发连接,我们使用htttp-watch这样的HTTP监视工具可以很清晰地看到这一点。所以,我们前面说到的服务器支持的最大并发数,具体到真实的用户,可能并不是一对一的关系,一个真实的用户可能会对服务器带 两个或更多的并发用户数的压力,一些高明的用户还可以通过一些方法修改浏览器的并发数限制。而我们在本书中为了简化模型,暂且认为每个用户的并发下载数均为1.

另一方面,从Web服务器的角度来看,实际并发用户数也可以理解为Web服务当前维护的代表不同用户的文件描述府总数,也就是并发连接数。当然,不是同时来了多少用户请求就建立多少连接,Web服务器一般会限制同时服务的最多用户数,比如Apache的Max-Clients参数,所以这个实际用户并发用户数,有时候大于服务器所维护的文件描述符总数,而多出的这些用户请求,则在服务器内核的数据接收缓冲区中等待处理,所以这些请求在用户看来处于阻塞状态。

但是,最大并发用户数和最大并发连接数的决定因素从本质上来说是不同的。举两个例子:

  • 当实际并发用户数稍稍大于服务器所能维护的文件描述符上限时,如果请求的性质决定了处理每个请求花费的时间非常少,比如请求1KB的静态网页,那么每个请求都可以快速被处理然后释放文件描述符,这样从用户的角度而言,等待时间几乎不会减少太多。所以在这处情况下,我们希望服务的最大并发用户数可以大于最大并发连接数。幸运的是,这种情况在我们后面介绍select模型在大并发下处理小文件请求时会有相应的测试。

  • 如果请求性质决定了处理每个请求要花费相当长的时间,比如下载10M文件或都请求动态内容,那么即使服务器可以支持较大的并发连接数,比如使用导步I/0理论上可能支持2万个并发连接,然而是否能够为这么多接入的用户提供快速响应的服务至关重要。对于下载10MB文件来说,可能由于带宽的瓜分而导致每个用户的下载速度缓慢,而对于请求动态内容,可以由于CPU的时间瓜分导致每个用户的等待时间过长。所以在这种情况下,我们希望服务的最大并发用户数小于理论上的最大并发连接数。

这样看来,从某种意义上可能说,Web服务器所做的工作本质就是,争取以最快的速度将内核缓冲区的用户请求数据一个不剩地都拿过来,然后尽大最大努力到则时快速处理完这些请求,并将响应数据放到内核维护的另一块用于发送数据的缓冲区中,接下来再尽快处理下拨请求,并尽量让用户请求在内核缓冲区中不要等太久。

通过Apache的mod_status,可以了解到它在此刻同时处理的请求数,也就是此刻的实际并发用户数,如下所示:

Server Version: Apache/2.4.23 (Win32) OpenSSL/1.0.2j mod_fcgid/2.3.9

Server MPM: WinNT

Server Built: Jul 1 2016 16:42:20

Distributed by: The Apache Haus

Compiled with: Visual Studio 2008


Current Time: Tuesday, 04-Sep-2018 16:10:23 Öйú±ê׼ʱ¼ä

Restart Time: Monday, 03-Sep-2018 09:44:27 Öйú±ê׼ʱ¼ä

Parent Server Config. Generation: 1

Parent Server MPM Generation: 1

Server uptime: 1 day 6 hours 25 minutes 55 seconds

Server load: -1.00 -1.00 -1.00

Total accesses: 91719 - Total Traffic: 2.2 GB

.837 requests/sec - 20.9 kB/second - 24.9 kB/request

6 requests currently being processed, 1914 idle workers

请求等待时间

在刚才讲到并发用户数的时候,我们简单地提到了用户等待时间,这也是压力测试结果中很重要的一个指标。总结一下,我们所关心的时间有以下两种:

  • 用户平均请求等待时间
  • 服务器平均请求处理时间

听起来好像比较绕口,但是我实在找不出更短并且可以完整表达其含义的词语。它们的本质区别是什么呢?其实在前面的商场柜台的故事已经有所涉及,我们这里再来举个例子,注意,我们暂时将数据在网络上的传输时间不计入内。

首先,假设并发用户数为1,也就是只有一个用户在向服务器源源不断地发送请求,那么每个请求的等待时间也就是它的处理时间,等于总时间除以总总请求数,这时用户平均请求等待时间和服务器平均请求处理时间是相同的,这很容易理解。

然后,假设并发用户数为·100,那么便会有100个用户同时向服务器发送请求,简单地说,这时Web服务器一般会采用多进程或多线程的并发模型,通过多个执行流来同时处理多个并发用户的请求,而多执行流体系的设计原则便是轮流交错使用CPU时间片,所以每个执行流花费的时间都被拉长。对每个用户而言,每个请求的平均等待时间必然增加;而对于服务器而言,如果并发策略得当,每个请求的平均处理时间可能减少。

所以,这两个时间的本质在于,用户平均请求等待时间主要用于衡量服务器在一定并发用户数的情况下,对于单个用户的服务质量;而服务器平均处理时间与前者相比,则用于衡量服务器的整体服务质量,它其实就是吞吐率的倒数。

硬件环境

到目前为止,我们似乎还是一直示提及服务器硬件配置、的确,试想一下,如果在商场柜台的故事中,换一个脑子反应快一点的营业员,并且增加柜台前的空间大小,也许可以让柜台支持20个并发数。在以下的压力测试中,我们普遍使用的Web服务器基本硬件配置如下:

Pentium(R) Dual-Core  CPU      E5300  @ 2.60GHz

内存:4G

硬盘:7200转。

除此之外,我们不对硬件配置做其他详细介绍,因为在本书中,我们探讨的大部分内容都不希望依赖于特定的硬盘配置,所以性能测试结果也不侧重于它的绝对数值意义,我们的目的是探讨如何测量性能及如何根据不同的场景及优化性能。

在后面的压力测试中,如无特殊说明,则均采用以上硬件配置,而对于单机硬件的垂直扩展带来的性能提升,本书不做重点讨论。

来一次压力测试,我们就可以用压力测试软件来计算吞吐率了,本书中大部分压力测试使用Apache附带的ab,毫不夸张地说,它非常容易使用,完全可以模拟以上各种前提条件。另外,ab可以以直接在WEB服务器本地发起测试请求,这至关重要,因为我们希望测试的是服务哭喊的处理时间,而不包括数据的网络传输时间以及PC本地的计算时间,而至于后面这些时间的花费,我们会在本书的特定章节单独介绍,比如在第2章中我们已经详细探讨了数据的网络传输本质,这样便有利于我们根据各部分的不同本质来分别分析策略。

需要清楚的是,ab进行一切测试的本质都是基于HTTP,所以可以说它是对于Web服务器软件的墨盒性能测试,它获得的一切数据和计算结果,都可以通过HTTP来解释。

另有一些压力测试软件,包括Load-Runner、Jmeter等,则是不同程度上包含了服务器处理之外的时间,比如LoadRunner运行在用户PC上,可以录制浏览器行为,这种测试的结果往往侧重于站点用户的角度,有另外一些层面的参考意义。

接下来,我们将使用ab来进行一次压力测试,在本书中我们使用Apache 2.4.6中附带的ab,其版本如下所示:

[root@localhost ~]# ab -V
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

好了,一切就绪,开始压力测试,请看下面的命令行信息:

[root@localhost ~]# ab -n 1000 -c 10 http://localhost/test.html
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Apache/2.4.6
Server Hostname:        localhost
Server Port:            80

Document Path:          /test.html
Document Length:        41484 bytes

Concurrency Level:      10
Time taken for tests:   0.229 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      41749000 bytes
HTML transferred:       41484000 bytes
Requests per second:    4360.32 [#/sec] (mean)
Time per request:       2.293 [ms] (mean)
Time per request:       0.229 [ms] (mean, across all concurrent requests)
Transfer rate:          177772.43 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       2
Processing:     1    2   0.4      2       4
Waiting:        0    0   0.2      0       2
Total:          1    2   0.5      2       5

Percentage of the requests served within a certain time (ms)
  50%      2
  66%      2
  75%      2
  80%      3
  90%      3
  95%      3
  98%      3
  99%      4
 100%      5 (longest request)

请注意我们在启动ab时,传入3个命令行参数,它们正是代表了前面提到的前提条件:

-n 1000 表示总请求数为1000

-c 10 表示并发用户数为10

http://localhost/test.html 表示这些请求的目标URL。

测试结果一目了然,我们看到吞吐率显示为4360.32reqs/s。同时,在测试结果中还有一些其他内容也值得我们关注,主要包括:

Server Software

表示被测试的Web服务器软件名称,这里是Apache/2.4.6,它来自于HTTP响应数据的头信息,所以如果是我们自己编写的Web服务器或者修改开源Web服务器软件的源代码,便可以随意改写这里的名称,就像我们曾经都喜欢用游戏修改器修改道具属性一样。

Server Hostname

表示请求的URL中的主机部分名称,它来自于HTTP请求数据头信息,这里我们请求的URL是http://localhost/test.html,所以主机名为localhost。说明我们的请求是从Web服务器端发起的。

Server Port

表示被测试的Web服务软件的监听端口,为了方便测试,我们后面会对多个不同的Web服务器软件使用不同的监听端口。

Document Path

表示请求的URL中的根绝对路径,它同样来自于HTTP请求数据的头信息,通过它的后缀名,我们一般可以了解该请求的类型。

Document Length

表示HTTP响应数据的正文长度。

Concurrency Level

表示并发用户数,这是我们设置的参数

Time taken for tests

表示所有这些请求被处理完成所花费的总时间。

Complete requests

表示总请求数,这是我们设置的相应参数。

Failed request

表示失败的请求数,这里的失败是批请求在连接服务器发送数据、接收数据等环节发生异常,以及无响应后超时的情况。对于超时时间的设置可以使用ab -t 参数。

而如果接收到的HTTP响应数据的头信息含有2xx以外的状态码,则会在测试结果显示另一个我为"Non-2xx responses"的统计项,用于统计这部分请求数,这些请求并不算是失败的请求。

Total transferred

表示所有请求的响应数据长度总和,包括每个HTTP响应数据的头信息和正文数据的长度。注意这里不包括HTTP请求数据的长度,所以Total transferred代表了从Web服务器流向PC的应用层数据总长度。通过使用ab的-v参数即可查看详细的HTTP头信息。

HTML transferred

表示所有请求的响应数据中正文数据的总和,也就是减去了Total transferred中HTTP响应数据中头信息的长度。

Requests per second 

这便是我们重点关注的吞吐率,它等于:

Complete request / Time tabek for tests

Time per request

这便是前面提到的用户平均请求等待时间,它等于 Time taken for tests/(Complete requests/Concurrency Level)

Time per request(accress all concurrent requests)

这便是前面提到的服务器平均请求处理时间,它等于:Time taken for tests / Complete request

这正是吞吐率的倒数。同时,它也等于:Time per request / Concurrency Level

Transfer rate\

表示这些请求在单位时间内从服务获取长度,它等于:Total transferred / Time taken for tests

这个统计项可以很好地说明服务器在处理能力达到极限时,其出口带宽的需求量。利用前面介绍的有关带宽知识,不难计算出结果。

Percentage o fthe requests served within a certain time(ms)

这部分数据用于描述每个请求处理时间的分布情况,比如在以上测试结果中,80%请求的处理时间不超过2ms,而99%的请求都不超过4ms。注意这里的处理时间,是批前面的Time per request,即对于单个用户面言,平均每个请求处理的时间。

首先看吞吐率真随并发用户数变化的曲线图,如图3-1所示。当并发用户超过100后,吞吐率开始走下坡路,并且在并发用户数超过150后,吞吐率直线下跌,惨不忍睹。

图3-1 吞吐率随并发用户数变化的曲线图

再看看服务器平均请求处理时间随着并发用户数变化的曲线图,如图3-2所示。它的实质便是吞吐率的倒数,所以他的曲线和前一张图刚好沿横坐标等比例对称,同是在图上也反映了当并发用户数超过100后,服务器处理一个请求的平均时间突然飙升。

 

图3-2 服务器平均请求处理时间随着并发用户数变化的曲线图

下面来看用户平均请求等待时间随并发用户民数变化的曲线图,如图3-3所示。当用户数超过300后,请求的平均等待时间大幅度增加.

到现在为止,我们已经对Web服务器的几个常见性能指标做了详细的介绍,并且熟悉了压力测试的方法和澧结果的含义。那么,影响这些指标的因素,除云服务器的硬件配置,那就是前面提到的并发策略了。简单地说,并发策略的设计就是在服务器同时处理较多请求的时候,如何合理协调并充分利用CPU计算和I/O操作,使其在较大并发用户数的情况下提供较高的吞吐率。

并不存在一个对的所有性质的请求都高效的并发策略,否则我们完全不需要介绍这些内容,任何架构师只要获得应用了最佳并发策略的web服务器软件便可以应付各种性质的并发用户请求,显然那是不存在的。那么,你了解你的站点正在处理哪些性质的请求吗?只有了解并根据这些性质选择最佳的并发策略,高性能web站点才会离你更近一步。

本章的其余部分将围绕并发策略,探讨一些你感兴趣的内容。

3.2 CPU并发计算

服务器之所以可以同时处理多个请求,在于操作系统通过多执行流体系设计使得多个任务可以轮流使用系统资源,这些资源包括CPU,内存以及I/O等。

进程

多执行流的一般实现便是进程。从本质上讲,多进程的好处并不仅仅在于CPU时间的轮流使用,还在于对CPU计算和I/O操作进行了很多的重叠利用,这里的I/O主要是指磁盘I/O和网络I/O,它们的速度和CPU相比,就像老牛漫步和超音速飞机一样相距甚远。事实上,大多数进程的时间都主要消耗在了I/O操作上,现代计算机的DMA技术可以让CPU不参与I/O操作的的全过程,比如进程通过系统调用,使得CPU向网卡或者磁盘等I/O设备发出指令,然后进程被挂起,释放出CPU资源,等待I/O设备完成工作后骑过中断来通知进程重新就绪。所以对于单任务而言,CPU大部分时间空闲,这时候多进程的作用便显得尤为重要。

进程的调试由内核来进行,从内核的观点看,进程的目的就是担当分配系统资源的实体。同时,进程也可以理解为记录程序实例当前运行到什么程度的一组数据,多个进程通过不同的进程描述符与这些数据进行关联。

每个进程都有自己独立的内存地址空间和生命周期。当子进程被父进程创建后,便将父进程地址空间的所有数据复制到自己的地址空间,完全继承父进程的所有上下文信息,它们之间可以通信,但是不互相依赖,也无权涉彼此的地址空间。

进程的创建使用 fork()系统调用,它的开销虽然不很昂贵,但是在繁忙的服务器上频繁地创建进程,其开销可能成为影响性能的主要因素。Linux 2.6对于fork()的实现进行了优化,减少了一些多余的内存复制,但在早期的版本或都其他平台中,fork()的开销随着当前进程量的递增而加大。

轻量级进程

由于进程之间相对独立,它们各自维护庞大的地址空间和上下文信息,无法很好地低成本共享数据,所以采用大量进程的Web服务器(比如Apache 的Perfork模型)在处理大量并发请求时,内存的大量消耗有时候会成为性能提升的制约因素。但是,进程的优越性有时也恰恰体现在其相互独立所带来的稳定性和键壮性方面。

为此,在Linux 2.0之后,提供了对轻量级进程的支持,它由一个新的系统调用clone()来创建,并由内核直接管理,像普通的进程一样独立存在,各自拥有进程描述符,但是这些进程已经允许共享一些资源,比如地址空间、打开的文件等。轻量级进程减少了内存的开销,并为多进程应用程序的数据共享提供了直接支持,但是其上下文切换的开销还是在所难免的。

线程

POSIX 1003.1c 为Linux定义了线程的接口"pthread",有很多种具体实现,有些不是由内核直接支持,在这种情况下,从内核角度来看,多线程只是一个普通的进程,这是由用户态通过一些库函数模拟实现的多执行流,所以多线程的管理完全在用户态完成,这种实现方式下线程切换的开销相比于进程和轻量级进程都要少些,但是它在多处理器的服务器(SMP)中表现较差,加为只有内核的进程调试器才有权利分配多个CPU的时间,这在随后的进程调试器中会有介绍。

POSIX线程的另一种实现是LinuxThreads,它可以谙是内核级线程(Kernel-Level Threads),因为它通过clone()来创建线程,也就是说,它的是说,它的实现原理是将线程和轻量级进程进行一对一关联,每个线程实际上就是一个轻量级进程,这样使得线程完全由内核的进程调度器来管理,所以它对于SMP的支持较好,但是线程切换的开销相比于用户态线程要多一些。

LinuxThreads已经加入了glibc和libc的目前版本,并且必须在Linux 2.0之后使用,加为它依赖的clone()系统调用和内核实时进程调试器出现在Linux 2.0之后。

在另一些操作系统(如Digital Unix、Solaris、 IRIX)中,内核线程被支持,线程管理由内核来进行,它们同样可以很好地支持多CPU。

进程调度器

在单CPU的机器上,虽然我们感觉到很多任务在同时运行,但是从微观意义上讲,任何时刻只有一个进程处于运行状态,项其他进程有的处于挂起状态并等待就绪,有的已经就绪并等待CPU时间片,还有的处于其他状态。

内核中的进程调度器(Scheduler)维护着各种状态的进程队列。在Linux中,进程调试器维护着一个包括所有可运行进程的队列,称为"运行队列"(Run Quere),以及一个包括所有休眠和僵尸进程的列表。

进程调度器的一项重要工作就是决定下一个运行的进程,如果运行队列中有不止一个进程,那就比较伤脑筋了,按照先来后到的顺序也不是那么合理,因为运行在系统中的进程有着不同的工作需要,比如有些进程需要处理紧急的事件,有些进程只是在后台发送不太紧急的邮件,所以每个进程需要告诉调度器各自的紧急程度,这就是进程优先级。

进程优先级除了可以由进程自己决定,进程调度吕在进程运行时也可以动态调整它们的优先级,比如对有些进程适当提高优先级,对有些进程则进行处罚,降低它们的优先级,这些行为的出发点都是为了让所有的进程更好地重叠利用系统资源。Linux对于进程的动态调整体现在进程的nice属性中,我们对lighttpd进行持续压力测试,使用100个并发用户来请求151B的静态文件,这时候通过top来观察lighttpd进程的优先级和动态调整,具体如下:

Tasks: 120 total,   4 running, 116 sleeping,   0 stopped,   0 zombie
%Cpu(s): 27.7 us, 53.3 sy,  0.0 ni,  0.8 id,  0.0 wa,  0.0 hi, 18.2 si,  0.0 st
KiB Mem :  3880640 total,  3475340 free,   145652 used,   259648 buff/cache
KiB Swap:  2592764 total,  2592764 free,        0 used.  3480188 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                      
 1501 root      20   0   90652  14872   2320 R  98.7  0.4   0:30.32 ab                                           
 1416 lighttpd  20   0   52152   5028   1704 R  96.3  0.1   0:29.86 lighttpd 

进程的优先级属性为Priority,在top结果中用PR表示, 而优先级动态调整值在top中用N1表示。

我们对Lighttpd的fastcgi进程进行压力测试,看看fastcgi进程的优先级,我们配置Lighttpd为打开4个fastcgi进程,请求资源为一个以CPU计算为主的PHP程序,结果如下所示:

top - 20:56:56 up 6 min,  2 users,  load average: 0.13, 0.11, 0.07
Tasks: 120 total,   2 running, 118 sleeping,   0 stopped,   0 zombie
%Cpu(s): 50.1 us,  0.2 sy,  0.0 ni, 49.8 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  3880640 total,  3537520 free,   130612 used,   212508 buff/cache
KiB Swap:  2592764 total,  2592764 free,        0 used.  3507232 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                      
 1427 lighttpd  20   0  455960   7264   1528 R 100.0  0.2   0:07.21 php-fpm 

 再看看Apache-perfork,同时是使用100个用户来请求151字节的静态文件,Apache子进程数上限设置为100,也许你的经验已经告诉你Apache处理这些请求时的CPU开销远远超过刚才Lighttpd,在随后我们将会专门来探讨这个问题。这时候的top结果如下所示:

Tasks: 211 total,  10 running, 200 sleeping,   0 stopped,   1 zombie
%Cpu(s): 28.5 us, 37.4 sy,  0.0 ni, 20.9 id,  0.0 wa,  0.0 hi, 13.3 si,  0.0 st
KiB Mem :  3880640 total,  3230720 free,   242416 used,   407504 buff/cache
KiB Swap:  2592764 total,  2592764 free,        0 used.  3356124 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                      
 1518 root      20   0   90652   7940   2320 R  60.9  0.2   0:09.06 ab                                           
 1519 apache    20   0  530004   9416   2128 R   1.3  0.2   0:00.53 httpd                                        
 1529 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.16 httpd                                        
 1536 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.11 httpd                                        
 1546 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.11 httpd                                        
 1549 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.11 httpd                                        
 1556 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.08 httpd                                        
 1557 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.08 httpd                                        
 1564 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.08 httpd                                        
 1566 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.08 httpd                                        
 1570 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.08 httpd                                        
 1573 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.08 httpd                                        
 1574 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.08 httpd                                        
 1577 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.08 httpd                                        
 1580 apache    20   0  530004   9416   2128 S   1.3  0.2   0:00.08 httpd                                        
 1480 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.66 httpd                                        
 1481 apache    20   0  530004   9400   2112 S   1.0  0.2   0:00.66 httpd                                        
 1483 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.69 httpd                                        
 1484 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.66 httpd                                        
 1485 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.66 httpd                                        
 1520 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.34 httpd                                        
 1521 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.35 httpd                                        
 1522 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.24 httpd                                        
 1524 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.24 httpd                                        
 1525 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.24 httpd                                        
 1527 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.16 httpd                                        
 1528 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.16 httpd                                        
 1530 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.16 httpd                                        
 1531 apache    20   0       0      0      0 Z   1.0  0.0   0:00.16 httpd                                        
 1532 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.16 httpd                                        
 1534 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.10 httpd                                        
 1535 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.11 httpd                                        
 1537 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.10 httpd                                        
 1539 apache    20   0  530004   9416   2128 S   1.0  0.2   0:00.10 httpd 

 PR所代表的值有什么含义呢?它其实就是进程高度器分配给进程的时间片长度,单位是时钟个数,那么一个时钟需要多长时间呢?这跟CPU的主频以及操作系统平台有关。比如Linux上一般为10ms, 那么RP值为15则表示这人进程的时间片为150ms。

在操作系统中,时间片的长度是权衡利弊的一个博弈结果。如果各进程的时间片太短,那么CPU浪费在进程切换上的时间比例就比较大,整体效率降低;而如果时间片太长,则多任务实时性以及交互性就无法保证。

另外,在传统的UNIX中,进程调度器为所有进程计算nice值的时候,需要锁住进程表,这时候对于使用了多处理器的服务器(SMP)来说非常糟糕,因为当一个CPU锁住进程表进行nice值计算时,其他CPU无法切换进程。这个问题在一些商业化操作系统以及Linux中得以解决,简单地说就是给每个处理器分分一个运行队列,互不影响,进程调度器负责将进程分配到适合的CPU。

系统负载

在进程调度器维护的运行队列中,任何时刻至少存在一个进程,那就是正在运行的进程。那就是正在运行的进程。而当运行队列中有不止一个进程的时候,就说明此时CPU比较抢手,其他进程还在等着,进程调度器应该尽快让正在运行的进程释放CPU。

通过在任何时刻查看/proc/loadavg,可以了解到运行队列的情况。

[root@localhost ~]# cat /proc/loadavg 
0.00 0.04 0.05 1/126 1385

注意 1/126这部分,其中的1代表此时运行队列中的进程个数,而126则代表此时的进程总数。最右边的1385代表到此时为止,最后创建的一个进程ID。接下来,请看左边的三个数值,分别是0.00‘0.04、0.05,它们就是我位常说的系统负载。我们都知道,系统负载越高时,代表CPU越繁忙,越无法很地满足所有进程的需要。但是,系统负载是如何计算而来的呢?根据定义,这是在单位时间内运行队列中就绪等待的进程数平平均值,所以当运行队列中就绪进程 不需要等待就可以马上获取CPU的时候,系统负载便非常低。当系统负载为0.00j时,说明任何进程只要就绪,就可以马上获得CPU,不需要等待,这时候系统响应速度最快。

那么,刚才提到的三个数值便是系统最近1分钟、5分钟和15分钟分别计算得出的系统负载。

我们还可以通过其他方法获得系统负载,比如top、w等工具。

[root@localhost ~]# top

top - 10:38:54 up  2:08,  2 users,  load average: 0.14, 0.05, 0.06

[root@localhost ~]# w
 10:39:13 up  2:09,  2 users,  load average: 0.11, 0.04, 0.05

了解这些内容后,要想提高服务器的系统负载,很简单,我们编写一个没有任何I/O操作并且长时间占用CPU时间的PHP脚本 ,比如一个循环累加器的实现代码如下:

<?php
$max = 100000000;
$sum = 0;
for ($i = 0 ; $i < $max; ++$i)
{
        $sum += $i;
}
echo $sum;
?>

然后用100个用户并发用户请求这个脚本,进行压力测试,这时候查看系统负载,结果如下:

[root@localhost ~]# top
top - 10:53:38 up  2:23,  2 users,  load average: 3.03, 2.39, 1.12

请注意,不要在对外提供服务的服务器上进行类似操作。

猜你喜欢

转载自blog.csdn.net/tjjingpan/article/details/82379176