服务器性能和可伸缩性杀手
George V. Reilly
微软公司
2月22, 1999
目录
序言
应用程序服务器
IIS的灵活性和性能
扼杀服务器性能的十条戒律
结论
--------------------------------------------------------------------------------
序言
现在,服务器性能问题是许多书写桌面应用程序的人所要面对的问题。组件对象模型(Component Object Model,COM)和Component ware的成功产生了一个意想不到的结果,这就是如果使用像ASP(IIS的一个扩展)这样的应用程序服务器,就不用编写主机代码了,其实以前的主机代码都不是在真正的服务器环境下编写的。桌面环境和服务器环境之间有许多重要的不同,这些不同会在性能上产生不可预测的影响。
桌面应用程序服务器
影响桌面应用程序性能的因素是众所周知的。长指令路径意味着更慢的代码,这是性能方面的一个主要缺陷。使用大量资源会使应用程序变得更加臃肿,这样系统中的其他应用程序可用的资源就会更少。减慢启动时间会激怒用户。太多的运行设置会使机器的页错误率增高,使它们变慢而且反映迟钝。服务器应用程序也常受到这些因素影响,另外还有一些其他因素介绍如下:
通常,服务器应用程序同时处理的客户没有几百也有几十。对桌面应用程序来说,如果能在1/10秒内对用户做出反应就算是很快的了。假设一个操作需要整整100ms的话,那么这个应用程序在一秒中只能进行10个操作。大多数服务器应用程序需要比每秒钟十次请求大得多的通量。高延迟时间网络(延迟时间=消息的传输时间)加长了反应时间,这就需要服务器的反应更快以满足要求。
服务器应用程序经常处理大量的数据设置。效率低下的,尤其是那些浪费运行时间的方法,是不能用于处理上百万条数据的。
服务器机器比桌面机器更强大。服务器机器有更多的内存,更大的磁盘,更快的CPUs,并且通常有多个处理器。但是这些仍然不够。桌面机器处理的是零星的突发性业务,大部分时间是空闲的,而服务器的负载是连续不断的。服务器机器很昂贵,必须运行得很好才行。
服务器应用程序需要具有以月计算的正常运行时间。过了一段时间后,服务器的性能必须不会由于资源泄露或 cruft(一种需要周期性清除的数据结构和统计结果)的积聚而降低。
大多数服务器应用程序都需要采用多线程结构。考虑一个一次只处理一个请求。而将大部分时间都化在I/O上的单线程服务器,这样的性能是很难让人接受的。线程池可以利用其他空闲的处理器时钟周期同时处理几个请求。为了充分利用多处理器系统,服务器应用程序必须是多线程的。不幸的是,多线程应用程序很难编写,很难调试,而且很难运行得好,尤其是在多处理器系统中。但是一旦正确地得到它,其性能会远远超过同样的单线程应用程序,从这一点来说,使用多线程应用程序还是值得的。
单线程应用程序相对简单,很容易理解:程序中某一时刻只有一个事件发生。在多
线程应用程序中,并发行为导致复杂的相互作用,其影响很难预测。另外,这些相
互作用,不管是否是灾难性的,都很难再生。桌面应用程序很少有多于一个线程
的,即使有,这些线程也只是用于分立的后台业务,例如打印。
IIS的灵活性和性能
Internet Information Server(IIS)是一个应用程序服务器。在很多方面,它像是一个虚拟操作系统,因为有许多ASP和ISAPI应用程序在处理间隔中运行。
IIS使用一个I/O线程池来处理所有到来的请求。对静态文件(.htm,.jpg等文件)的请求会马上得到满足,而对动态内容的请求被分派到适当的ISAPI扩展动态连接库。ASP扩展利用一个工人线程池运行ASP页。因为ASP是基于COM的,所以所有组件都是在我们的处理过程中执行的。这是一个好坏掺半的事情。它对开发者来说是好极了,因为它允许组件的简单重用,使ASP非常灵活,因此使ASP和IIS非常成功。但是,这个灵活性导致了性能问题。因为许多组件是为桌面系统编写的,并且许多专门为ASP创建的组件是由那些不是十分会写高性能服务器组件的人编写的。
对ISAPI扩展和过滤器也是一样。不同组件之间及同一组件的不同实例中都存在着严重的相互影响。
下面的所有说明都适用于IIS,其中的大多数也适用于其他服务器应用程序。
扼杀服务器性能的10条戒律
下面的每一条戒律都将有效地影响代码的性能和可伸缩性。换句话说,尽可能不要照着戒律去做!下面,我将解释如何破坏他们以便提高性能和可伸缩性。
应该分配和释放多个对象
你应该尽量避免过量分配内存,因为内存分配可能是代价高昂的。释放内存块可能更昂贵,因为大多数分配算符总是企图连接临近的已释放的内存块成为更大的块。直到Windows NT? 4.0 service pack 4.0,在多线程处理中,系统堆通常都运行得很糟。堆被一个全局锁保护,并且在多处理器系统上是不可扩展的。
不应该考虑使用处理器高速缓存
大多数人都知道由虚拟内存子系统导致的hard 页错误代价很高,最好避免。但是许多人认为其他内存访问方法没有什么区别。自从80486以后,这一观点就不对了。现代的CPUs比RAM要快得多,RAM至少需要两级内存缓存 ,高速L1 缓存能保存8KB数据和8KB指令,而较慢的L2 缓存能保存几百KB的数据和代码,这些数据和代码混合在一起。L1 缓存中内存区域的一个引用需要一个时钟周期,L2 缓存的引用需要4到7个时钟周期,而主内存的引用需要许多个处理器时钟周期。后一数字不久将会超过100个时钟周期。在许多方面,缓存像一个小型的,高速的,虚拟内存系统。
至于和缓存有关的基本内存单元不是字节而是缓存列。Pentium 缓存列有32个字节宽。Alpha 缓存列有64个字节宽。这意味着在L1 缓存中只有512个slot给代码和数据。如果多个数据一起使用(时间位置)而并不存储在一起(空间位置),性能会很差。数组的空间位置很好,而相互连接的列表和其他基于指针的数据结构的位置往往很差。
把数据打包到同一个缓存列中通常会有利于提高性能,但是它也会破坏多处理器系统的性能。内存子系统很难协调处理器间的缓存。如果一个被所有处理器使用的只读数据,和一个由一个处理器使用并频繁更新的数据共享一个缓存 列,那么缓存将会花费很长时间更新这个缓存列的拷贝。这个Ping-Pong高速游戏通常被称为"缓存 sloshing"。如果只读数据在一个不同的缓存 列中,就可以避免sloshing。
对代码进行空间优化比进行速度优化效率更高。代码越少,代码所占的页也越少,这样需要的运行设置和产生的页错误也会更少,同时占据的缓存 列也会更少。然而,某些核心函数应该进行速度优化。可以利用profiler去识别这些函数。
决不要缓存频繁使用的数据。
软件缓存可以被各种应用程序使用。当一个计算代价很高时,你会保存结果的一个拷贝。这是一个典型的时空折中方法:牺牲一些存储空间以节省时间。如果做得好,这种方法可能非常有效。
你必须正确地进行缓存。如果缓存了错误数据,就会浪费存储空间。如果缓存得太多,其他操作可以使用的内存将会很少。如果缓存得太少,效率又会很低,因为你必须重新计算被缓存 遗漏的数据。如果将时间敏感数据缓存得时间过长,这些数据将会过时。一般,服务器更关心的是速度而不是空间,所以他们要比桌面系统进行更多的缓存。一定要定期去除不用的缓存,否则将会有运行设置问题。
应该创建多个线程,越多越好。
调整服务器中起作用的线程数目是很重要的。如果线程是I/O-bound的,将会花费很多时间用来等待I/O的完成-一个被阻塞的线程就是一个不做任何有用工作的线程。加入额外的线程可以增加通量,但是加入过多的线程将会降低服务器的性能,因为上下文交换将会成为一个重大的overhead。上下文交换速度应该低的原因有三个:上下文交换是单纯的overhead,对应用程序的工作没有任何益处;上下文交换用尽了宝贵的时钟周期;最糟的是,上下文交换将处理器的缓存填满了没用的数据,替换这些数据是代价高昂的。
有很多事情是依靠你的线程化结构的。每个客户端一个线程是绝对不合适的。因为对于大量用户端,它的扩展性不好。上下文交换变得难以忍受,Windows NT用尽了资源。线程池模型会工作得更好,在这种方法中一个工人线程池将处理一条请求列,因为Windows 2000提供了相应的APIs,如QueueUserWorkItem。
应该对数据结构使用全局锁
使数据线程安全的最简单方法是把它套上一把大锁。为简单起见,所有的东西都用同一把锁。这种方法会有一个问题:序列化。为了得到锁,每一个要处理数据的线程都必须排