12.请求处理模型优化
  公平对待等待中的连接,优化应用层队列
  在之前的性能计算中,我们提到增加应用层队列,以实现对进程数的控制,但实际上与我们之前的一个假设有冲突,将连接从队列中拿出来后放到另一个队列,这样这个连接并不能立即进行读数据的操作,我们可以优化一下,所有进入这个队列的连接都可以立即进行读数据的操作,这并不是平常我们所说的 “队列”了,是一种特殊的数据结构,我们往往通过系统调用select或epoll 来实现,所以拿到的连接都可以进行数据操作,这实现了一种事件驱动的模式,这种模式一定程序减少了应用层队列等待时间,它避免了前一个连接因传输慢而导致后一个连接不能传输数据的情况,当然因为我们的进程数因为是控制在一定数量的,仍然可能会有“队列”等待的情况,延迟值计算跟之前是一样的。
  这种“队列”,更像一种竖着的队列,进入队列中的连接都可以进行接下来的处理流程,如图6所示。


  
图6

  如果将所有的到来的请求都放入“队列”,这是一种事件驱动模式,如之前所述,这可能会导致所有请求延迟增加。
  缓冲各阶段吞吐量性能差,增加新的应用层队列
  一般来说网络数据读取的吞吐量是高于CPU处理的吞吐量,之前的模型只有前端有队列,整体吞吐量受限于小吞吐量,不过我们可以在网络数据读取与CPU处理间建立新的应用层队列,让网络数据处理以更高的吞吐量运行,如图7所示,当然积累的待处理请求不能超过新的队列长度。
  将应用队列放在前端的延迟值为:
  L*( X*S3/H3+S1/H1+S2/H2)
  增加新的队列后延迟值为:
  X*S3/H3+L’*(S1/H2+S2/H2)
  在队列长度相同的情况下,增加新的应用层队列后请求延迟值会降低,当然整体吞吐量不会改变。


  
图7

  解绑请求与进程,减少所需进程数
  之前对需要的进程数计算是基于进程跟请求完全绑定的,这种情况进程在等待I/O时会进入睡眠状态,这其实是对进程资源的一种浪费,如果将请求与进程解绑,让一个进程在等待某种I/O请求时不是去睡眠,而是继续去干其它事,于是我们可以以更少的进程数完成我们的吞吐量及延迟要求。
  如果进程一直在“干活”而不会“睡觉”那应该需要多少进程数呢?进程一直干活需要使用CPU,所以这是跟CPU个数有关系的,在这种情况下需要的进程数是:
  Z*N1
  N1是CPU个数,Z是一个系数,一般应该在1到2之间。
  解绑请求与进程是比较复杂的,也根本没有一种方法拆分让进程完全不“睡觉”,例如有些数据读取到一个进程中后总不能让另一个进程去处理的,数据在不同进程间不能共享,这也是Z系数会大于1的原因。
  对一个请求拆分越细致对进程资源的浪费越小,但编程越复杂,也可能会导致增加CPU计算时间,这是需要衡量的问题,具体不再展开,具体编程是一个很复杂的问题。
  13.CPU密集型和I/O密集型对性能值的影响
  之前我们讨论的是三阶段情况下各性能值的计算,现实情况有些是CPU密集或I/O密集型的服务,即极端化下的两阶段,即只有网络请求和CPU计算或只能网络请求和I/O处理。简单讨论下这种两阶段服务计算性能时特别的地方。
  CPU密集型
  只有网络处理和CPU计算两个阶段。
  吞吐量及延迟的计算一样,只是少了一个阶段。
  因为只有CPU计算,不用对请求进行复杂的拆分,只需要启动少量进程,如跟CPU个数一样,即可以达到佳的吞吐量值。模型优化第三条可以不采用了。
  另外CPU密集型服务处理时间会相对较短,网络数据处理时间占总体时间会较大,需要使用模型优化第二条。
  也适宜事事件驱动的方式,并发度会很高,延迟不会增加特别厉害。
  I/O密集型
  只有网络处理和I/O读写两个阶段(CPU的时间小到忽略不计)
  同样的,吞吐量及延迟的计算一样,只是少了一个阶段。
  请求拆分以避免进程睡眼是较难的,这时启动的进程数应该跟如下公式相关
  min(H3/S3,H2/S2)* (X*S3/H3+S2/H2)
  这将远远多于CPU的个数。
  另外I/O密集型服务处理时间会相对较长,模型优化第二条也一般不采用,因为其带来的延迟减少占总延迟比是很小的,增加一个队列反而增加了编程复杂度,而且队列相关的CPU时间可能甚至要高于网络处理延迟减少的时间,导致得不偿失。
  不适宜事件驱动的方式,因为I/O处理耗时长,会让请求延时急剧上升。
  14.多系统性能值计算
  之前我们讨论的值计算都是基于单一系统,而现实中是一个请求可能要经过多个不同的系统,如一个http请求,先经过nginx,再经过php,后到mysql,如图8所示


 
 图8

  这种情况下性能值有什么不同呢?计算与单个系统各阶段计算类似
  吞吐量
  是各系统的小吞吐量值
  延迟
  各系统延迟值相加
  并发度
  吞吐量*总的延时
  事件驱动模式可以提高并发度,但一般我们只在第一个接入系统实现事件驱动模式可以提高整个系统的并发度,所以我们无须在所有后续都实现事件驱动,如php或mysql,在后续系统实现事件驱动的一个坏处是后面系统的到达处理极限不能很好的反应到前面的系统,导致并发度越来越大,而导致整个系统的延迟极高。
  php的进程模型里没有网络处理与CPU计算中间的应用层队列,当PHP模块吃紧时会很快反应到nginx模块。
  进程数
  各个系统的进程数可以依照之前公式算出来,我们关心的一个问题是,A系统调用B系统时,A系统应该启动多少进程。假设B系统的吞吐量为吞吐量B,每个请求的耗时为耗时B
  为了使B系统达到相应吞吐量,则A系统发起的请求吞吐量也必须达到相同值,所以调用方进程数在同步调用(进程可能投入睡眠状态)下计算如下
  A系统进程数 = A系统吞吐量*耗时A
  A系统吞吐量 = B系统吞吐量
  耗时A =耗时B
  可得:
  A系统进程数 = B系统吞吐量*耗时B
  15.如何进行性能测试
  现实世界比我们之前说的几个简单公式远远复杂多了,不可能有一个精确的公式可以描述出来,所以性能值我们也不能说填充几个参数可以像算数学题一样算出来,再加上我们面对的系统都是多个系统组合而成,直接算更不可能了,那些简单公式只是指导致我们进行测试。对于一个系统来说,真正的性能值需要通过工具测试出来,如用siege工具。
  用工具进行测试的思路
  吞吐量是一个比较固定的值,先观测它。根据之前的公式
  A系统进程数 = B系统吞吐量*耗时B
  得
  B系统吞吐量 = A系统进程数/耗时B
  可知为了观察到大的吞吐量应该不断提高工具的并发进程数,直到吞吐量不再上升。
  当吞吐量不再上升时说明已到了之前说所的H-S-F状态临界点,如果进一步提高客户端的并发,延时将会上升,这时我们可以得出延时的小值。
  客户端的并发度与待测系统的并发度不一定相同,但可以肯定的是他们是正相关关系。
  当延时处于小值,吞吐量处于大值时,这时得出的并发度是佳并发度。佳并发度不代表大并发度,大并发度对于事件驱动模型来说,甚至是无限的,只要不超过客户端超时时间,但过长的时间不是我们能接受的。首先我们定义一个可以接受的大延时,然我进一步提高客户端的并发度,直到延时达个这个值,这时得到的服务器的并发值才是有实际意义的大并发度值。
  性能测试工具siege
  Ben: $ siege -u shemp.whoohoo.com/Admin.jsp-d1 -r10 -c25
  ..Siege 2.65 2006/05/11 23:42:16
  ..Preparing 25 concurrent users for battle.
  The server is now under siege...done
  Transactions: 250 hits
  Elapsed time: 14.67 secs
  Data transferred: 448000 bytes
  Response time: 0.43 secs
  Transaction rate: 17.04 trans/sec
  Throughput: 30538.51 bytes/sec
  Concurrency: 7.38
  Status code 200: 250
  Successful transactions: 250
  Failed transactions: 0
  Transaction rate 指吞吐量
  Response time  指延时
  Concurrency 指并发度
  如何寻找瓶颈
  有时我们需要找到整个系统的瓶劲,以提高性能值。首先我们应该找到整个系统是哪个子系统的的处理出现了瓶劲,是nginx,php还是mysql。然后研究一个子系统,是网络读取,CPU还是磁盘I/O出现了瓶颈,或是进程数启得少了,或是我们要优化自己的程序。具体操作比较复杂,可另成文,不展开讨论。
  参考资料
  1. UNIX 网络编程
  2. High Performance MySQL