原作者姓名 Fang(fangguicheng@21cn.com)
正文
异步IO、APC、IO完成端口、线程池与高性能服务器之五 服务器的性能指标与实现高性能的途径
服务器的性能指标
作为一个网络服务器程序,性能永远是第一位的指标。性能可以这样定义:在给定的硬件条件和时间里,能够处理的任务量。能够最大限度地利用硬件性能的服务器设计才是良好的设计。
设计良好的服务器还应该考虑平均服务,对于每一个客户端,服务器应该给予每个客户端平均的服务,不能让某一个客户端长时间得不到服务而发生“饥饿”的状况。
可伸缩性,也就是说,随着硬件能力的提高,服务器的性能能够随之呈线性增长。
实现高性能的途径
一个实际的服务器的计算是很复杂的,往往是混合了IO计算和CPU计算。IO计算指计算任务中以IO为主的计算模型,比如文件服务器、邮件服务器等,混合了大量的网络IO和文件IO;CPU计算指计算任务中没有或很少有IO,比如加密/解密,编码/解码,数学计算等等。
在CPU计算中,单线程和多线程模型效果是相当的。《Win32多线程的性能》中说“在一个单处理器的计算机中,基于 CPU 的任务的并发执行速度不可能比串行执行速度快,但是我们可以看到,在 Windows NT 下线程创建和切换的额外开销非常小;对于非常短的计算,并发执行仅仅比串行执行慢 10%,而随着计算长度的增加,这两个时间就非常接近了。”
可见,对于纯粹的CPU计算来说,如果只有一个CPU,多线程模型是不合适的。考虑一个执行密集的CPU计算的服务,如果有几十个这样的线程并发执行,过于频繁地任务切换导致了不必要的性能损失。
在编程实现上,单线程模型计算模型对于服务器程序设计是很不方便的。因此,对于CPU计算采用线程池工作模型是比较恰当的。QueueUserWorkItem函数非常适合于将一个CPU计算放入线程池。线程池实现将会努力减少这种不必要的线程切换,而且控制并发线程的数目为CPU的数目。
我们真正需要关心的是IO计算,一般的网络服务器程序往往伴随着大量的IO计算。提高性能的途径在于要避免等待IO 的结束,造成CPU空闲,要尽量利用硬件能力,让一个或多个IO设备与CPU并发执行。前面介绍的异步IO,APC,IO完成端口都可以达到这个目的。
对于网络服务器来说,如果客户端并发请求数目比较少的话,用简单的多线程模型就可以应付了。如果一个线程因为等待IO操作完成而被挂起,操作系统将会调度另外一个就绪的线程投入运行,从而形成并发执行。经典的网络服务器逻辑大多采用多线程/多进程方式,在一个客户端发起到服务器的连接时,服务器将会创建一个线程,让这个新的线程来处理后续事务。这种以一个专门的线程/进程来代表一个客户端对象的编程方法非常直观,易于理解。
对于大型网络服务器程序来说,这种方式存在着局限性。首先,创建线程/进程和销毁线程/进程的代价非常高昂,尤其是在服务器采用TCP“短连接”方式或UDP方式通讯的情况下,例如,HTTP协议中,客户端发起一个连接后,发送一个请求,服务器回应了这个请求后,连接也就被关闭了。如果采用经典方式设计HTTP服务器,那么过于频繁地创建线程/销毁线程对性能造成的影响是很恶劣的。
其次,即使一个协议中采取TCP“长连接”,客户端连上服务器后就一直保持此连接,经典的设计方式也是有弊病的。如果客户端并发请求量很高,同一时刻有很多客户端等待服务器响应的情况下,将会有过多的线程并发执行,频繁的线程切换将用掉一部分计算能力。实际上,如果并发线程数目过多的话,往往会过早地耗尽物理内存,绝大部分时间耗费在线程切换上,因为线程切换的同时也将引起内存调页。最终导致服务器性能急剧下降,
对于一个需要应付同时有大量客户端并发请求的网络服务器来说,线程池是唯一的解决方案。线程池不光能够避免频繁地创建线程和销毁线程,而且能够用数目很少的线程就可以处理大量客户端并发请求。
值得注意的是,对于一个压力不大的网络服务器程序设计,我们并不推荐以上任何技巧。在简单的设计就能够完成任务的情况下,把事情弄得很复杂是很不明智,很愚蠢的行为。
正文完