James C. Hu Douglas C. Schmidt
通信软件的开发者面临着许多挑战。通信软件包含固有的复杂性,比如错误检测和恢复;以及随机的复杂性,比如关键概念和组件的持续的重新发现和发明。应对这些挑战需要对面向对象应用构架和模式有全面的了解。本论文阐释我们是怎样将通信软件的构架和模式应用于开发称为JAWS的高性能Web服务器的。
JAWS是一种面向对象的构架,支持多种Web服务器策略的配置,比如使用异步I/O和LRU缓存的线程池并发模型 vs. 使用同步I/O和LFU缓存的Thread-per-Request并发模型。因为JAWS是一个构架,可以系统地对这些策略进行定制,独立地或协作地进行评估,以决定最佳的策略方案。使用这些方案,JAWS可以静态地和动态地改变自己的行为,以为给定的软件/硬件平台和工作负载采用最为有效的策略。JAWS的自适配软件特性使其成为用于构造高性能Web服务器的强大应用构架。
在过去的几年中,万维网(Web)上的通信流量发生了戏剧性的增长。流量的增长在很大程度上应归于廉价的和无处不在的Web浏览器(比如NCSA Mosaic、Netscape Navigator和Internet Explorer)的激增。同样地,Web协议和浏览器也正日益被应用于专门而昂贵的计算任务,比如西门子[1]和柯达[2]所用的图像处理服务器和像AltaVista和Lexis-Nexis这样的数据库搜索引擎。
要跟上需求增长的步伐,必须开发高性能Web服务器。但是,在开发者配置和优化Web服务器时,他们面对的是一组异常丰富的设计策略。例如,开发者必须在广泛的并发模型(比如Thread-per-Request vs. 线程池)、分派模型(比如同步 vs. 异步分派)、文件缓存模型(比如LRU vs. LFU),以及协议处理模型(比如HTTP/1.0 vs. HTTP/1.1)中进行选择。没有哪种配置对于所有硬件/软件平台和工作负载来说都是最佳的[1, 3]。
所有这些可选策略的存在保证了开发者可以定制Web服务器、以满足用户的需求。但是,在许多设计和优化策略间进行选择是麻烦而易错的。没有相应的指导,开发者将面临艰巨的任务:从头开始设计Web服务器来产生特定的解决方案。这样的系统常常难以维护、定制和调谐,因为许多设计工作都只是花在了使系统可运行上。
我们将经常引用术语OO类库、构架、模式和组件。这些术语所指的是用于构建可复用软件系统的工具。OO类库是一组软件对象实现,它们在用户调用对象方法时提供可复用功能。构架是一种可复用、“半完成”的应用,可被定制以产生自定义应用[4]。模式表示在特定的上下文中、软件开发问题的可反复使用的解决方案[5]。组件指的是一种“可具体化”的对象。OO类库和构架都是通过实例化和专门化而得以具体化的成组对象。模式组件则通过编码来具体化。
本论文阐释怎样使用OO应用构架和设计模式来产生灵活而高效的Web服务器。模式和构架可以协作应用,以改善Web服务器的效率和灵活性。模式以一种系统而易于理解的形式捕捉高性能和自适配Web服务器的抽象设计和软件体系结构。构架则使用特定的编程语言,比如C++或Java,来捕捉Web服务器的具体设计、算法和实现。相反,OO类库提供构建应用所必需的原始材料,但没有对怎样将这些片段放在一起进行指导。
本论文聚焦于用于开发JAWS[1, 3]高性能Web服务器的模式和构架。JAWS既是一个Web服务器,又是一个构架,其他类型的服务器可通过它来进行构建。JAWS构架自身是使用ACE构架[6, 7]来开发的。ACE构架使通信软件领域中的一些关键模式[5]得以具体化。JAWS和ACE中的构架和模式是有代表性的解决方案,已被成功应用于许多通信系统,范围从电信系统管理[8]到企业医学成像[2]和实时航空控制系统[9]等。
本论文被组织如下:2.2给出对模式和构架的综述,并说明JAWS所提供的通信软件构架类型的动机;2.3阐释怎样应用模式和组件来开发高性能Web服务器;2.4比较JAWS与其他高性能Web服务器在高速ATM网络上的性能;2.5给出结束语。
为了给数目正在增长的Internet和Intranet用户提供服务和内容,对于高性能Web服务器的需求正在日益增长。Web服务器的开发者正在努力构建快速、可伸缩和可配置的系统。但是,如果不注意避开一些常见的陷阱和缺陷,其中包括麻烦而易错的低级编程细节、缺乏可移植性,以及广泛的设计选择,这样的任务可能是十分困难的。这一部分给出了这些危险的路标。随后我们描述开发者怎样通过有效利用设计和代码复用,将模式和构架应用于避免这些危险。
Web服务器开发者面临着一些反复发生的挑战,这些挑战在很大程度上独立于特定的应用需求。例如,像其他通信软件一样,Web服务器必须执行多种任务:连接建立、事件处理器分派、进程间通信、内存管理和文件缓存、静态和动态的组件配置、并发、同步,以及持续性。在大多数Web服务器中,这些任务是以特定的方式、使用低级的本地OS应用编程接口(API)(比如用C编写的Win32或POSIX)来实现的。
遗憾的是,本地OS API并不是开发Web服务器或其他类型的通信中间件和应用[10]的有效途径。下面是与本地OS API的使用相关联的常见缺陷:
过多的低级细节:通过本地OS API来构建Web服务器要求开发者熟悉低级的OS细节。开发者必须仔细地追踪每个系统调用返回的错误代码,并在他们的服务器中处理这些特定于OS的问题。这样的细节使得开发者的注意力从更广阔的、更为战略性的问题(比如语义和程序结构)上转移开来。例如,使用wait系统调用的UNIX开发者必须在下面两种错误之间进行区分:由于没有子进程存在而返回的错误和来自信号中断的错误。在后一种情况下,必须重新发出wait调用。
持续地重新发现和发明不兼容的更高级编程抽象:常用的对过多的OS API细节的补救方法是定义更高级的编程抽象。例如,许多Web服务器都创建文件缓存,以避免每次客户请求都要访问文件系统。但是,这些类型的抽象常常被各个开发者或项目独立地重新发现和发明。这样的特定处理妨碍了生产效率,并创建出不兼容的组件,无法迅速地在大型软件组织的项目内和项目间复用。
高错误可能性:由于低级OS API缺乏类型安全性,对它们进行编程是麻烦而易错的。例如,大多数Web服务器都使用Socket API[11]来编写。但是,Socket API中的通信端点被表示为无类型的句柄。这增加了发生微妙的编程错误和运行时错误的可能性。
缺乏可移植性:低级OS API出了名地不可移植,即使是在同一OS的不同版本间也是如此。例如,Win32平台上的Socket API实现(WinSock)与UNIX 平台上的实现有着微妙的不同。而且,即使是Windows
NT的不同版本上的WinSock实现也具有不兼容的、与时俱变的错误:在执行非阻塞连接时会导致偶发的失败。
陡峭的学习曲线:由于有过多的细节,掌握OS级API所需的努力可能是很高的。例如,学习怎样正确地使用POSIX异步I/O[12]来编程十分困难。学习怎样使用异步I/O机制来编写可移植的应用甚至会更困难,因为它们在各OS平台间有着极大的不同。
不能处理更高的复杂性:OS API为一些机制定义了基本接口,像进程和线程管理、进程间通信、文件系统,以及内存管理。但是,当应用的大小和复杂性增长时,这些基本接口无法适当地升级。例如,典型的UNIX进程只允许缓冲大约7个待处理连接[13]。对于被大量访问的、必须处理成百并发客户的Web服务器来说,这个数目是不够的。
软件复用是被广泛称许的减少开发工作量的方法。复用有效利用了有经验的开发者的领域知识和以前的成果。在有效地应用时,复用可以避免重新创建和认证常用的、针对重复发生的应用需求和软件设计挑战的解决方案。
Java的java.lang.net和RougeWave Net.h++是两个常见的将可复用OO类库应用于通信软件的例子。尽管类库有效地支持小规模的组件复用,它们的范围是严重受限的。特别地,类库不会对相关软件组件族之间的规范控制流和协作进行捕捉。因而,应用基于类库的复用的开发者常常要为每个新应用重新发明和实现整个的软件体系结构。
更为强大的克服上面描述的缺陷的途径是对在成功的Web服务器之下的模式进行标识,并在面向对象应用构架中使这些模式具体化。通过捕捉常见软件开发问题的解决方案,模式和构架有助于减少对关键的Web服务器概念和组件的重新发现和发明[5]。
将模式应用于Web服务器的好处:模式提供了常见的Web服务器微体系中的结构和参与者的文档。例如,反应堆(Reactor)[14]和主动对象(Active Object)[15]模式分别被广泛用作Web服务器的分派和并发策略。这些模式是已被证明有益于构建灵活而高效的Web服务器的对象结构的一般化。
传统上,这些模式类型或者被锁在老练的开发者的头脑里,或者被深埋在源码中。但是,让这样有价值的信息只是放在这些地方是危险而昂贵的。例如,如果不编写文档,有经验的Web服务器设计者的洞见可能会随时间而消逝。同样地,可能需要相当的努力才能从现有源码中反向地设计出模式来。因此,为了给负责增强和维护现有软件的开发者保留设计信息,明确地捕捉Web服务器模式并编写文档是必要的。而且,特定领域的知识还有助于指导在其他领域中构建新服务器的开发者的设计决策。
将构架应用于Web服务器的好处:模式知识有助于减少开发工作和维护代价。但是,只是复用模式并不足以创建灵活而高效的Web服务器软件。在模式使抽象设计和体系结构知识复用成为可能的同时,被编写为模式的抽象并不会直接产生可复用的代码[16]。因此,有必要增加对模式的研究,考查它们与构架的创建和使用的关系。通过实现常用设计模式、并分解出常见实现角色,构架可帮助开发者避免对标准的Web服务器组件进行昂贵的重新发明。
通过集成成组的抽象类,并定义这些类的实例进行协作的标准方式,构架为应用提供了可复用的软件组件[4]。一般而言,组件并不是自包含的,因为它们常常依赖于构架中其他组件所提供的功能。但是,这些组件聚合在一起构成了特定的实现,也就是,应用骨架。可以通过继承和实例化构架中的可复用组件来对骨架进行定制。
Web服务器中复用的范围可以显著地大于使用传统的函数库或组件的OO类库。特别地,2.3中描述的JAWS构架特别为广泛的Web服务器任务作了裁剪。这些任务包括服务初始化、错误处理、流控制、事件处理、文件缓存、并发控制和原型流水线操作。重要的是要记住这些任务对于其他许多类型的通信软件来说也是可复用的。
总而言之,构架和组件以下面几种方式增强了基于组件类库的复用技术:
构架定义“半完成”应用,其中包含了特定领域的对象结构和功能:类库提供了一种粒度相对较小的复用。例如,图2-1中的类和字符串、复数、数据及位组一样,是典型的低级、相对独立和通用的组件。
图2-1 类库组件体系结构
相反,构架中的组件相互协作,来为相关应用族提供可定制的体系结构骨架。完整的应用可以通过从构架组件继承、以及/或者实例化构架组件来合成。如图2-2所示,构架减少了应用特有代码的数量,因为特定领域的许多处理被分解进通用的构架组件中。
图2-2 应用构架组件体系结构
构架是主动的,并在运行时显示出“控制的反转”:类库组件通常被动地工作。特别地,类库组件常常从“自指引”(self-directed)的应用对象那里借用线程控制来完成它们的处理。因为应用对象是自指引的,在很大程度上应用开发者要负责决定怎样组合组件和类,以形成完整的系统。例如,通常要为每个新应用重写管理事件循环、并在可复用和应用特有组件间确定控制流的代码。
通过类库和组件构建的应用的典型结构和动力特性在图2-1中演示。该图还演示了设计模式怎样帮助指导类库组件的设计、实现和复用。注意,在提供工具解决特定任务(例如建立网络连接)的同时,类库的存在并没有提供对系统设计的明确指导。特别地,软件开发者要独自负责在他们的应用设计中确定并应用模式。
相对于类库,构架中的组件更为主动。特别地,它们通过像反应堆(Reactor)[14]和观察者(Observer)[5]这样的事件分派分派模式来管理应用中的规范控制流。构架的回调驱动的运行时体系结构如图2-2所示。
图2-2演示了构架的一种关键特性:它在运行时的“控制的反转”。这种设计使得规范的应用处理步骤可由通过构架的反应式分派机制[14]调用的事件处理器对象来定制。在事件发生时,构架的分派器通过调用预登记处理器对象的挂钩方法来进行反应,由该方法完成事件的应用特有的处理。
控制的反转允许构架,而不是每个应用,确定调用哪一组应用特有方法来响应外部事件(比如HTTP连接和数据到达Socket)。作为结果,构架使一组集成的模式具体化、并预先应用进协作的组件中。这样的设计减轻了软件开发者的负担。
在实践中,构架、类库和组件是互相补充的技术。构架常常在内部利用类库和组件来简化构架的开发。例如,JAWS的一些部分使用由C++标准模板库(STL)[17]提供的字符串和向量容器来管理连接映射和其他查找结构。此外,由构架事件处理器调用的应用特有的回调常常使用类库组件来完成基本的任务,比如字符串处理、文件管理和数字分析。
为演示怎样成功地应用OO模式和构架来开发灵活而高效的通信软件,本论文的余下部分检查JAWS构架的结构、使用和性能。
将构架和模式应用于通信软件的好处最好通过例子来演示。这一部分描述JAWS的结构和功能。JAWS是一种高性能和自适配的、实现了HTTP协议的Web服务器。它还是一个平台无关的应用构架,其他类型的通信服务器可以通过它来构建。
图2-3 JAWS构架的体系概览
图2-3演示组成JAWS自适配Web服务器构架的主要结构组件和设计模式。JAWS的设计允许定制多种Web服务器策略,以响应环境因素。这些因素包括静态因素,比如对OS中的内核级线程及/或异步I/O的支持、可用CPU的数目;以及动态因素,比如Web流量模式和工作负载特性。
JAWS被构造为构架的构架(framework
of frameworks)。整个JAWS构架含有以下组件和构架:事件分派器(Event Dispatcher)、并发策略(Concurrency Strategy)、I/O策略(I/O Strategy)、协议流水线(Protocol
Pipeline)、协议处理器(Protocol Handler),以及缓存式虚拟文件系统(Cached Virtual Filesystem)。各个构架都被构造为一组使用ACE[18]中的组件实现的协作对象。JAWS组件和构架之间的协作由一个模式族进行指导,该模式族在图2-3中沿着图的边缘列出。对JAWS中的关键构架、组件和模式的概述在下面给出。更为详细的对这些模式怎样应用于JAWS的设计的描述将在2.3.2中给出。
事件分派器(Event
Dispatcher):该组件负责协调JAWS的并发策略和它的I/O策略。Web客户的被动连接建立遵循接受器(Acceptor)模式 [19]。新到来的请求由一种并发策略服务。在事件被处理时,它们被分派到协议处理器(Protocol Handler),后者由一种I/O策略参数化。从一系列可选方案中选择,以动态绑定到特定并发策略和I/O策略的机制遵循策略(Strategy)模式 [5]。
并发策略(Concurrency
Strategy):该构架实现的并发机制(比如单线程、Thread-per-Request,或线程池)可被适配性地选择:在运行时使用状态(State)模式,或在初始化时预先确定。服务配置器(Service Configurator)模式[20]用于在运行时将特定的并发策略配置进Web服务器。当并发涉及多线程时,策略会创建遵循主动对象(Active Object)模式[15]的协议处理器。
I/O策略(I/O Strategy):该构架实现多种I/O机制,比如异步、同步和反应式I/O。多种I/O机制可以同时使用。异步I/O通过前摄器(Proactor)[21]和异步完成令牌(Asynchronous
Completion Token)[22]模式来实现。反应式I/O通过反应堆(Reactor)模式[14]来完成。反 应式I/O利用Memento模式[5]来捕捉请求状态,并使其外在化,以在后面将其恢复。
协议处理器(Protocol
Handler):该构架允许系统开发者将JAWS构架应用于Web系统应用的变种。协议处理器由并发策略和I/O策略来参数化。这些策略对协议处理器来说是不透明的(通过使用适配器(Adapter)[5]模式)。在JAWS中,该组件实现了HTTP/1.0请求方法的解析与处理。该抽象使得其他协议(比如HTTP/1.1和DICOM)能够很容易地结合进JAWS。要增加新协议,开发者只需简单地编写新的协议处理器实现,随后将其配置进JAWS构架中。
协议流水线(Protocol
Pipeline):该构架使过滤器操作能够很容易地与正在被协议处理器处理的数据进行合成。这种集成是通过采用适配器模式来完成的。流水线遵循用于输入处理的管道和过滤器(Pipes and Filters)模式[23]。使用服务配置器模式,可在运行时动态链接流水线组件。
缓存式虚拟文件系统(Cached
Virtual Filesystem):该组件通过减少文件系统访问开销来改善Web服务器性能。可以遵循策略模式[5]来选择多种缓存策略,比如LRU、LFU、提示式策略和结构化策略。这使得开发者可以根据有效性来对不同的缓存策略进行裁剪,并静态或动态地配置最佳策略。各个Web服务器的缓存通过使用单体(Singleton)模式[5]来实例化。
Tilde Expander:该组件是另一种缓存组件,它使用理想哈希表[24]来将简写的用户登录名(例如,~schmidt)映射到用户主目录(例如,/home/cs/faculty/schmidt)。当个人Web页面存储在用户主目录中、而用户目录又没有驻留在共同的根上时,该组件能够充分地减少访问系统用户信息文件(比如/etc/passwd)所需的磁盘I/O开销。通过服务配置器模式的效力,可以动态地解除Tilde Expander的链接,并将其重新链接进服务器(例如,在新用户加入系统时)。
图2-3中的JAWS体系结构图演示了JAWS是怎样构造的,但并没有说明它为什么以这种特定的方式构造。要理解JAWS为什么包含有像并发策略、I/O策略、协议处理器和事件分派器这样的构架和组件,需要对在通信软件领域(一般而言)和Web服务器(特定的)之下的设计模式有更深入的了解。图2-4 演示与JAWS有关的战略和战术模式。这些模式在下面进行总结。
图2-4 JAWS构架中使用的设计模式
下面的模式对于Web服务器的整个软件体系结构来说是战略性的。它们的使用广泛地影响了系统中大量组件的交互水平。这些模式还被广泛用于指导许多其他类型的通信软件的体系结构。
接受器模式(Acceptor
Pattern):该模式使被动的连接建立与连接一旦建立后所执行的服务去耦合[19]。JAWS使用接受器模式来独立于它的连接管理策略适配性地改变它的并发和I/O策略。图2-5演示在JAWS的上下文中的接受器模式的结构。接受器是一种工厂[5],无论何时事件分派器通知它有连接已从客户到达,它都会创建、接受并启用一个新的协议处理器。
图2-5 JAWS中接受器模式的结构
反应堆模式(Reactor
Pattern):该模式使服务器应用的同步事件多路分离及事件处理器通知分派逻辑与为响应事件而执行的服务去耦合[14]。JAWS使用反应堆模式来处理来自多个事件源的多个同步事件,而又无需轮询所有事件源,或是无限期地阻塞在任何事件源上。图2-6演示在JAWS上下文中的反应堆模式的结构。
图2-6 JAWS中反应堆模式的结构
JAWS Reactive IO Handler(反应式I/O处理器)对象将自身登记到Initiation
Dispatcher(发起分派器),以与一些事件(也就是,在由HTTP请求建立的连接上的输入和输出)相关联。当与这些Reactive IO Handler对象相关联的事件发生时,Initiation Dispatcher调用它们的handle_input通知挂钩方法。2.3.3.2介绍的单线程Web服务器并发模型使用了反应堆模式。
前摄器模式(Proactor
Pattern):该模式使服务器应用的异步事件多路分离及事件处理器完成分派逻辑与为响应事件而执行的服务去耦合[21]。JAWS使用前摄器模式来在异步地处理其他I/O事件的同时执行服务器特有的处理,比如解析请求头。图2-7演示在JAWS上下文中的前摄器模式的结构。JAWS Proactive IO Handler(前摄式I/O处理器)对象将自身登记到Completion Dispatcher(完成分派器),以与一些事件(也就是,在由HTTP请求建立的连接上的文件接收和递送)相关联。
图2-7 JAWS中前摄器模式的结构
反应堆和前摄器模式之间的主要区别是Proactive IO Handler定义完成挂钩,而Reactive IO Handler处理器定义发起挂钩。因此,当像recv_file或send_file这样的异步调用的操作完成时,Completion
Dispatcher会调用这些Proactive IO Handler对象的适当的完成挂钩方法。2.3.3.2中的线程池的异步变种使用了前摄器模式。
主动对象模式(Active
Object Pattern):该模式使方法调用与方法执行去耦合,允许方法并发地运行[15]。JAWS使用主动对象模式来在分离的线程控制中并发地执行客户请求。图2-8演示在JAWS上下文中的主动对象模式的结构。
图2-8 JAWS中主动对象模式的结构
Protocol Handler(协议处理器)发出请求给Scheduler(调度器),后者将请求方法(比如HTTP请求)转换为存储在Activation
Queue(启用队列)中的Method Object(方法对象)。运行在与客户分离的线程中的Scheduler使这些Method Object出队,并将它们转换回方法调用,以执行指定的协议。在2.3.3.2描述的Thread-per-Request、线程池,以及Thread-per-Session并发模型中使用了主动对象模式。
服务配置器模式(Service
Configurator Pattern):该模式使系统中个体组件的实现与它们被配置进系统的时间去耦合。JAWS使用服务配置器模式来在安装时或运行时动态地优化、控制及重配置Web服务器策略的行为[25]。图2-9演示在协议流水线过滤器(Protocol Pipeline Filter)和缓存策略(Caching Strategy)的上下文中的服务配置器模式的结构。
该图描述服务配置器模式怎样动态地管理动态链接库(DLL)。这使得构架能够在运行时动态地配置服务器策略的不同实现。Filter Repository(过滤器仓库)和Cache Strategy Repository(缓存策略仓库)从Service Repository(服务仓库)继承功能。同样地,策略实现(比如Parse Request(解析请求)和LRU Strategy(LRU策略))从模式的Service(服务)组件那里借用接口,以使仓库能动态地对它们进行管理。
图2-9 JAWS中服务配置器模式的结构
Web服务器还利用了许多战术模式,比起上面描述的战略模式,它们要更为普遍和与领域无关。下列战术模式被用于JAWS中:
策略模式(Strategy
Pattern):该模式定义一个算法族,对其中的每个算法进行封装,并使它们成为可互换的[5]。JAWS大量地使用此模式来有选择地配置不同的缓存替换策略,而又不影响Web服务器的核心软件体系结构。
适配器模式(Adapter
Pattern):该模式将不兼容的接口转换为可由客户使用的接口[5]。JAWS在它的I/O策略构架中使用此模式,以统一封装同步、异步和反应式I/O操作。
状态模式(State
Pattern):该模式定义一种合成对象,其行为取决于其状态[5]。JAWS中的Event Dispatcher使用状态模式来无缝地支持不同的并发策略,以及同步和异步I/O。
单体模式(Singleton
Pattern):该模式确保一个类只有一个实例,并提供一个对它进行访问的全局访问点[5]。JAWS使用单体来确保它的缓存式虚拟文件系统只有一份拷贝存在于Web服务器进程中。
相对于早先描述的战略模式,战术模式对软件设计有着相对局部的影响。例如,单体是一种战术模式,常常用于统一Web服务器中特定的可全局访问的资源。尽管此模式是领域无关的、因而也是可广泛应用的,它所解决的问题并不像战略模式(比如主动对象和反应堆)那样普遍而深入地影响Web服务器软件体系结构。但是,要实现高度灵活、能良好地响应应用需求和平台特性的变化的软件,必须全面理解战术模式。
这一部分的余下部分讨论JAWS的用于并发、I/O、协议流水线处理和文件缓存的构架的结构。对于每一个构架,我们描述关键的设计挑战、并概述可选方案策略的范围。随后,我们解释各个JAWS构架是怎样被结构、以支持可选策略方案的配置的。
并发策略会显著地影响Web系统的设计和性能。对现有Web服务器(包括Roxen、Apache、PHTTPD、Zeus、Netscape和Java Web服务器)的实验研究[3]表明大部分与I/O无关的Web服务器开销来自Web服务器的并发策略。关键的开销包括同步、线程/进程创建,以及上下文切换。因此,选择高效的并发策略对于获取高性能来说是至关紧要的。
选择正确的并发策略并非无关紧要的事情。影响决策的有动态和静态两种因素。静态因素可被预先确定。这些因素包括硬件配置(例如,处理器数目、内存数量,以及网络连接速度)、OS平台(例如,线程和异步I/O的可用性),以及Web服务器使用情况(例如,数据库连接、图像服务器,或是HTML服务器)。动态因素是那些在系统执行过程中发生的可检测和可度量的情况。这些因素包括机器负载、并发请求数、动态内存的使用,以及服务器工作负载。
现有的Web服务器使用了广泛的并发策略来回应有关的众多因素。这些策略包括单线程并发(例如,Roxen)、基于进程的并发(例如,Apache和Zeus),以及多线程并发(例如,Apache和JAWS)。每种策略都会产生正面和负面的效果,必须在静态和动态因素的上下文中才能加以分析和评估。这些权衡在下面总结。
图2-10 JAWS中的Thread-per-Request策略
Thread-per-Request:该模式在单独的线程控制中处理每个来自客户的请求。因而,当每个请求到达时,就会创建一个新线程来处理该请求。这种设计允许每个线程使用同步I/O机制来读写所请求的文件。图2-10在JAWS构架的上下文中演示此模式。在这里,接受器反复地等待连接,创建协议处理器,并派生新线程,以使处理器能够继续处理连接。
Thread-per-Request的优点是它的简单性和它利用多处理器平台上的并行性的能力。它的主要缺点是缺乏可伸缩性、也就是,正在运行的线程的数目有可能无节制地增长,耗尽可用内存和CPU资源。因此,Thread-per-Request对于轻负载、低延迟的服务器来说是足够的。但是,它可能不适用于那些被频繁访问、执行费时任务的服务器。
Thread-per-Session:会话(Session)是客户向服务器做出的一系列请求。在Thread-per-Session中,所有这些请求都通过在每个客户与Web服务器进程中的单独线程之间的一个连接来提交。因此,该模型在多次请求间分摊了线程创建和连接建立开销。
Thread-per-Session的资源耗费比Thread-Per-Request要少,因为它并不为每个请求派生一个单独的线程。但是,在客户的数量增长时,它还是易于无节制地消耗资源。还有,Thread-per-Session的使用要求客户和服务器都支持在多个请求间复用已建立连接的概念。例如,如果Web客户和Web服务器都遵循HTTP/1.1,就可以在它们之间使用Thread-per-Session。但是,如果客户或服务器只支持HTTP/1.0,Thread-per-Session就会退化为Thread-per-Request[26, 27]。
线程池(Thread
Pool):在此模型中,在Web服务器初始化过程中会预先派生一组线程。每个线程从作业队列中获取一项任务。在线程处理作业的同时,它从线程池中被移除。一旦任务完成,线程就返回池中。如图2-11所示,正被获取的作业是接受器的完成。当它完成时,线程创建协议处理器,并出借它的线程控制,以使处理器能够处理连接。
线程池比Thread-per-Request的开销要少,因为线程创建的代价通过预先派生而被分摊掉了。而且,线程池所能消耗的资源的数量是有限的,因为池的大小是固定的。但是,如果池太小,它可能会被耗尽。这将导致新到来的请求被丢弃或无限期地等待。更进一步,如果池太大,资源耗费可能并不比使用Thread-per-Request更好。
图2-11 JAWS中的线程池策略
单线程(Single-Threaded):在此模型中,所有连接和请求都由同一线程控制来处理。单线程服务器的简单实现依次对请求进行处理。通常这对于高流量的产品服务器来说是不够的,因为后续请求会被阻塞、直到轮到它们进行处理,从而产生不可接受的延迟。
更为成熟的单线程实现使用异步或反应式I/O(在2.3.4描述)来并发地处理多个请求。在支持异步I/O的单处理器机器上,单线程并发策略可以比多线程方案执行得更好[1]。因为JAWS的I/O构架与它的并发构架是不相关的,我们认为单线程并发策略是线程池的池大小为1时的一种特例。
[1]和[3]中的实验演示了并发和时间分派策略的选择对负载条件遇到变化的Web服务器性能的影响。特别地,没有哪种服务器策略能够为所有情况都提供最佳性能。因而,服务器构架至少应该提供两种程度的自由:
1.
静态适配性:构架应该允许Web服务器开发者选择能最好地满足系统的静态需求的并发策略。例如,多处理器机器可能比单处理器机器更适合多线程并发。
2.
动态适配性:构架应该允许它的并发策略动态地适配当前的服务器环境,以在服务器负载发生动态变化的情况下取得最佳性能。例如,为了应付意外的负载使用,有可能必须增加线程池中可用线程的数目。
如上面所讨论的,没有哪种并发策略在所有情况下都能最佳地执行。但是,也不是所有平台都能够有效地使用所有可用的并发策略。为解决这些问题,JAWS并发策略构架同时支持相关于它的并发和事件分派策略的静态和动态的适配性。
图2-12演示JAWS的并发策略构架的OO设计。Event Dispatcher(事件分派器)和Concurrency(并发)对象依据State(状态)模式来交互。如图中所演示的,server可以在对server->dispatch()的连续调用间改变为使用Thread-per-Connection或线程池,从而使不同的并发策略产生效果。Thread-per-Connection策略是对上面讨论的Thread-per-Request和Thread-per-Session策略的抽象。每种并发机制都使用了Task(任务)。取决于并发的选择,任务可以表示单个主动对象,或是一组主动对象。并发对象的行为遵循接受器(Acceptor)模式。这样的体系结构使得服务器开发者能够集成各种可选的并发策略。通过策略配置文件的帮助,服务器可以在运行时动态地选择不同策略,以获得最佳的性能。
图2-12 并发策略构架的结构
对Web服务器开发者的另一项关键挑战是设计高效的数据获取和递送策略,合起来称为I/O。围绕高效I/O的问题可以是极具挑战性的。系统开发者常常必须安排多个I/O操作、以利用硬件/软件平台上可用的并发性。例如,高性能Web服务器在并发地解析新获取的来自其他客户的请求时,应该能同时在网络上传输多个文件。
特定类型的I/O操作有着与其他类型的I/O操作不同的需求。例如,涉及货币基金转账的Web事务可能需要同步地运行,也就是,用户在事务结束后才能继续其他操作。相反,访问静态信息的Web,比如基于CGI的搜索引擎查询,可以异步地运行,因为它们可以在任何时候被取消。这些不同的需求把我们引向了不同的执行I/O的策略。
如上面所指出的,有多种因素影响对I/O策略的选择。Web服务器的设计可使用若干不同的I/O策略,比如同步、反应式和异步的I/O。使用这些策略的相关好处在下面讨论。
同步I/O策略:同步I/O描述在Web服务器进程和内核之间的I/O交互的模型。在此模型中,内核不到所请求的I/O操作完成、部分完成或失败,就不会将线程控制返回给服务器。[1]显示在高速ATM网络上的Windows NT中,用于小文件传输的同步I/O通常执行良好。
同步I/O广为UNIX服务器程序员所知,并且最容易使用(有争论的)。但是,该模型也有一些缺点。首先,它与单线程并发策略结合在一起,不可能同时执行多个同步I/O操作。其次,当使用多个线程(或进程)时,I/O请求还是有可能无限期地阻塞。因而,有限的资源(比如Socket句柄或文件描述符)可能会耗尽,使得服务器不再有响应。
反应式I/O策略:早期版本的UNIX只提供同步I/O。系统V UNIX引入了非阻塞式I/O,以避免阻塞问题。但是,非阻塞式I/O要求Web服务器轮询内核、以发现是否有任何输入可用[11]。反应式I/O减轻了同步I/O的阻塞问题,而又不诉诸轮询方法。在此模型中,Web服务器使用OS事件多路分离系统调用(例如,UNIX中的select,或Win32中的WaitForMultipleObjects)来确定哪一个Socket可以执行I/O。当调用返回时,服务器可在返回的句柄上执行I/O,也就是,服务器对发生在分开的句柄上的多个事件进行反应。
反应式I/O被事件驱动应用(比如X windows)广泛使用,并已被编写为反应堆(Reactor)设计模式[14]。但是除非小心地封装反应式I/O,由于管理多个I/O句柄的复杂性,这种技术很容易出错。而且,反应式I/O可能无法有效地利用多CPU。
异步I/O策略:异步I/O简化了一或多个线程控制中多个事件的多路分离,而又不会阻塞Web服务器。当Web服务器发起I/O操作时,内核在服务器处理其他请求的同时、异步地执行操作直到完成。例如,Windows NT中的TransmitFile操作可以异步地将整个文件从服务器传输到客户去。
异步I/O的优点是Web服务器不需要在I/O请求上阻塞,因为它们是异步完成的。这使得服务器能够高效地为高I/O延迟的操作(比如大文件传输)进行伸缩。异步I/O的缺点是它在许多OS平台(特别是UNIX)上不可用。此外,编写异步程序比编写同步程序可能要更为复杂[21, 22, 28]。
[1]中的实验研究将不同的服务器策略系统地归属到多种负载条件。结果揭示出各种I/O策略在不同的负载条件下的行为也不同。而且,没有哪种I/O策略能够在所有负载条件下最优地执行。通过使I/O策略动态地适应运行时服务器环境,JAWS I/O策略构架解决了这一问题。而且,如果新的OS提供了一种定制的I/O机制(比如,异步分散/集中式I/O),有可能提供更好的性能,可以很容易地改编JAWS I/O策略构架来使用它。
图2-13 I/O策略构架的结构
图2-13演示JAWS所提供的I/O策略构架的结构。Perform Request(执行请求)是一种Filter(过滤器),派生自在2.3.5中阐释的Protocol Pipeline(协议流水线)。在此例中,Perform Request发出I/O请求给它的InputOutput Handler(输入输出处理器)。InputOutput Handler将对它发出的I/O请求委托给InputOutput对象。
JAWS构架提供派生自InputOutput的Synchronous、Asynchronous和Reactive IO组件。各种I/O策略使用适当的机制来发出请求。例如,Synchronous
IO组件使用传统的阻塞式read和write系统调用;Asynchronous IO依据前摄器模式[21]来执行请求;而Reactive IO则使用反应堆模式[14]。
InputOutput组件由接受器通过相关联的流、从2.3.3描述的Task组件创建。文件操作通过2.3.6描述的Filecache Handle(文件缓存句柄)组件来执行。例如,send_file操作将由Filecache Handle表示的文件发送给由接受器返回的流。
早期的Web服务器,像NCSA最初的httpd,执行的文件处理非常少。它们只是简单地取得所请求的文件,将其内容传输给请求者。但是,现代的Web服务器除了执行文件获取,还进行数据处理。例如,HTTP/1.0协议可用于确定各种文件属性,比如文件类型(例如,文本、图像、音频或视频)、文件编码和压缩类型、文件大小,以及它的最后修改日期。这些信息通过HTTP头返回给请求者。
通过引入CGI,Web服务器甚至已经能够执行更为广泛的任务,包括搜索引擎、地图生成、数据库系统连接,以及安全的商业和金融交易,等等。但是,CGI的限制是服务器必须派生新进程来扩展服务器功能。典型地,每个请求要求CGI派生它自己的进程来处理它,致使服务器成了Process-per-Request服务器,一种性能“抑制剂”[3]。高性能Web服务器构架的挑战是:允许开发者扩展服务器功能,而又不诉诸CGI进程。
概念上,大多数Web服务器都在若干阶段中进行数据流处理或变换。例如,处理HTTP/1.0请求的诸阶段可被组织为一系列任务。这些任务涉及(1)读入请求,(2)解析请求,(3)解析请求头信息,(4)执行请求,以及(5)生成请求日志。如图2-14所示,这一系列任务构成了处理到来的请求的任务流水线(pipeline)。
图2-14 为HTTP请求而执行的任务流水线
处理HTTP/1.0请求所执行的任务有着固定的结构。因而,图2-14演示了一种静态流水线配置。对于被请求执行有限数目的处理操作的服务器扩展来说,静态配置是有用的。如果这些操作相对较少,并且已被预先了解的话,它们可以预先制作,并在服务器执行过程中直接使用。静态处理的例子包括:数据整编和数据去整编、通过自定义Web协议层进行的数据多路分离,以及编译applet代码。将静态流水线结合进服务器中,使得开发者能够扩展服务器功能,而又不求助于派生外部进程。Apache Web服务器通过使用模块(module)提供了这种类型的可扩展性;模块封装静态处理,并且动态地链接到Web服务器。
但是,有些情况可能需要动态配置操作流水线。它们发生在这样的时候:Web服务器扩展涉及任意的处理流水线配置,所以数目在本质上是没有限制的。如果有大量中间的流水线组件,可以通过任意的次序排列,就有可能发生上述情况。如果对数据的操作只有在程序执行的过程中才能知道,通过组件动态地构造流水线就提供了一种经济的解决方案。有许多例子可说明动态流水线何时有用,包括:
高级搜索引擎:根据所提供的查询字符串,搜索引擎可以动态地构造数据过滤器。例如,像“(performance AND (NOT symphonic))”这样的查询查找含有单词“performance”、但没有单词“symphonic”的Web页面;它可被实现为由一个肯定的匹配组件和一个否定的匹配组件耦合而成的流水线。
图像服务器:图像服务器可以根据用户所请求的操作来动态地构造过滤器。例如,用户可以请求剪切、缩放、旋转和抖动图像。
自适配Web内容:Web内容可以根据最终用户的特性而动态地递送。例如,个人数字助理(PDA)接收的应该是Web内容综述和较小的图像,而工作站可以接收全部的富含多媒体的页面。同样地,家用电脑用户可以选择屏蔽某些类型的内容。
现有的允许开发者动态增强Web服务器功能的解决方案不是太针对特定应用,就是太过一般化。例如,Netscape的NetDynamics整个地聚焦于Web服务器与现有数据库应用的集成。相反,基于Java的Web服务器(比如Sun的Java Server和W3C的Jigsaw)允许进行任意的服务器扩展,因为Java应用有能力动态执行任意的Java代码。但是,怎样提供可动态配置的操作流水线的问题仍然要由服务器开发者来解决。因此,开发者需要在不利用基于设计模式的应用构架所提供的好处的情况下,自己定制解决方案的设计。
构造服务器来处理逻辑阶段的数据被称为管道和过滤器(Pipes and Filters)模式[23]。在该模式帮助开发者考虑怎样组织处理流水线的组件的同时,它并没有提供完成这些工作的构架。没有这样一个构架,改编自定义服务器、以高效地采用新协议特性的任务可能会太过昂贵、困难,或产生维护开销很高的代码。
前面的讨论激发了开发者对扩展服务器功能、而又不诉诸外部进程的需要。我们还描述了怎样将管道和过滤器模式应用于创建静态和动态的信息处理流水线。JAWS的协议流水线构架被设计用于简化编写这些流水线所需的工作。这是通过提供流水线组件的任务骨架(task skeleton)来完成的。当开发者完成流水线组件逻辑时,组件可随即与其他组件组合,以创建一条流水线。已完成组件被存储进仓库中,以使它们可在服务器运行时被访问。这使得服务器构架可以在Web服务器执行时按照需要动态地创建流水线。
图2-15提供了对构架结构的演示。该图描述协议处理器(Protocol Handler),它利用协议流水线(Protocol Pipeline)来处理到来的请求。过滤器(Filter)组件派生自协议流水线,并被用作流水线组件的任务骨架。
图2-15 协议流水线构架的结构
流水线的实现者从过滤器派生来创建流水线组件。每个流水线组件的svc方法首先调用父svc,由其获取要处理的数据,并随即针对数据执行它的组件特有的逻辑。父svc调用前面组件的svc方法。因而,组件的组合产生了一条调用链,拖着数据通过流水线。这条调用链在负责获取原始输入的组件那里结束,该组件直接派生自协议流水线抽象。
[3]中的结果显示访问文件系统是Web服务器的开销的重要来源。大多数分布式应用都可以从缓存中获益,Web服务器也不例外。因此,不奇怪,对Web服务器性能的研究会聚焦于怎样通过文件缓存来获得更好的性能[29, 30]。
缓存是一种存储媒介,它提供比所需信息通常所在的媒介更为高效的检索。在Web服务器的情况下,缓存驻留在服务器的主内存中。因为内存是有限的资源,文件只是暂时地驻留在缓存中。因而,围绕最佳缓存性能的问题是由数量(有多少信息应被存储在缓存中)和持续时间(信息应在缓存中停留多长时间)来决定的。
分配给缓存的内存大小会极大地影响数量和持续时间。如果内存不足,缓存少量大文件可能是不合需要的,因为缓存许多较少的文件能给出更好的平均性能。如果有更多的内存可用,缓存较大的文件也可能是可行的。
最近最少使用(LRU)缓存:这种缓存替换策略假定大多数对已缓存文件的请求都有着时间局部性(temporal locality),也就是,一个被请求的文件将很快被再度请求。因而,当把新文件插入缓存、要求移去另一个文件时,最近最少使用的文件将会被移除。该策略与提供具有临时特性的内容(比如每日新闻报告和股票报价)的Web系统有关。
最不经常使用(LFU)缓存:这种缓存替换策略假定已被频繁请求的文件最有可能被再度请求,这是另一种形式的时间局部性。因而,最不经常使用的缓存文件将是缓存中第一个被替换的文件。该策略与提供相对静态的内容的Web系统(比如Lexis-Nexis和其他历史事实数据库)有关。
提示式缓存:这种形式的缓存是在[29]中提议的。该策略源于对Web页面获取模式的分析,它们似乎表明Web页面有着空间局部性(spatial
locality)。就是说,浏览一个Web页面的用户也很有可能浏览该页面中的链接。提示式缓存与“预取”(pre-fetching)有关,虽然[29]建议修改HTTP协议,以使连接的统计信息(或提示)能够被发回给请求者。此修改允许客户决定预取哪些页面。同样的统计信息还可用于使服务器确定预先缓存哪些页面。
结构化缓存:这种缓存了解正在缓存的数据。对于HTML页面,结构化缓存指的是存储缓存文件、以支持单个Web页面的层次浏览。因而,缓存利用Web页面中出现的结构来确定将要传输给客户的最为相关的部分(例如,页面的顶级视图)。对于带宽和主内存有限的客户,比如PDA,这有可能加快Web访问。结构化缓存与数据库中B树的使用有关,B树能够使获取查询数据所需的磁盘访问的次数最小化。
图2-16 缓存虚拟文件系统的结构
上面描述的解决方案给出了实现文件缓存的若干策略。但是采用固定的缓存策略并不总能提供最佳的性能[31]。JAWS缓存式虚拟文件系统构架以两种方法来解决此问题。其一是使开发者很容易地将缓存替换算法和缓存策略集成进构架中。而且,通过利用策略Profile,可以动态地选择这些算法和策略,以在变化的服务器负载条件下优化性能。
图2-16演示JAWS中的一些组件,它们互相协作、以构造缓存式虚拟文件系统构架。InputOutput对象对Filecache Handle进行实例化,文件交互(比如读和写)通过该句柄来进行管理。Filecache Handle引用Filecache Object,后者是由Filecache组件来管理的。Filecache Object维护在所有引用它的句柄间共享的信息,比如内存映射文件的基地址。Filecache组件是缓存虚拟文件系统构架的心脏。它通过哈希方法来管理Filecache Object,并且在选择Cache Strategy来处理文件请求时遵循状态模式。Cache Strategy组件利用策略模式来使不同的缓存算法(比如LRU和LFU)能够互相替换。
2.2激发了对通过构架和模式来构建高性能Web服务器的需要。我们使用JAWS作为例子来演示构架和模式是怎样使程序员避开开发Web服务器软件的常见陷阱的。模式和构架共同支持集成化组件和设计抽象的复用 [4]。
2.3描述怎样建构JAWS构架,以及它所提供的策略。为了清晰地说明JAWS的设计组织,我们概述了对JAWS构架有最大影响的战略和战术设计模式。最为重要的战略模式有接受器、反应堆、前摄器、主动对象和服务配置器模式,等等。策略、适配器、状态和单体模式是JAWS中所用的有影响的战术模式。
这些模式被用来提供JAWS体系结构的“脚手架”。它们描述了必需的、形式为较小的组件的协作实体。从这里开始,构架的主要组件(也就是,并发策略、I/O策略、协议流水线和缓存式虚拟文件系统构架)被构建和集成进在一个骨架应用中。通过定制构架的特定子组件(例如,过滤器和缓存策略),开发者可以使用该骨架来构造专门的Web服务器软件系统。
图2-17显示JAWS构架的所有模式和组件是怎样集成的。JAWS构架以下面的方式来推动高性能Web服务器的构建。首先,它提供若干预配置的并发和I/O分派模型、一个提供标准的缓存替换策略的文件缓存,以及一个用于实现协议流水线的构架。其次,JAWS中所用的模式有助于使Web服务器策略与(1)实现细节和(2)配置时间去耦合。这样的去耦合使得新策略可以很容易地集成进JAWS。最后,JAWS的设计允许服务器静态和动态地改变它的行为。例如,通过适当的策略Profile,JAWS可以适应在Web服务器执行期间所发生的不同情况。通过进行动态适配来应用这些可扩展的策略和组件,JAWS简化了高性能Web服务器的开发和配置。
2.3描述的模式和构架组件使得JAWS能够静态和动态地适应它的环境。尽管这样的灵活性是有益的,Web服务器的成功最终还是要依赖于它怎样满足Internet以及企业Intranet的性能需求。要满足这些需求,需要对影响Web服务器性能的关键因素有全面的了解。
这一部分描述在系统地将不同负载条件应用于JAWS的不同配置时,我们所获得的性能结果。这些结果说明了为什么没有哪种配置能够在所有负载条件下最优地执行。这也论证了对像JAWS这样的灵活的构架的需要;这样的构架可在变化的负载条件下重配置、以获取最优性能。
图2-17 JAWS Web服务器构架
我们的硬件测试床如图2-18所示。测试床由两台Micron Millennia PRO2 plus工作站组成。每台PRO2有128 MB RAM,并配有2个Pentium Pro处理器。客户机的时钟速度为200 MHZ,而服务器则为180 MHZ。此外,每台PRO2还有一块由Efficient Networks, Inc制造的ENI-155P-MF-S ATM卡,并由Orca 3.01驱动软件来驱动。两台工作站经由ATM网络相连,网络通过FORE Systems ASX-200BX运行,最大带宽为622 Mbps。但是,由于LAN竞争模式的限制,我们的测试床的最高带宽大约为120 Mbps。
图2-18 基准测试床概览
2.4.2
软件请求生成器(Software Request Generator)
我们使用WebSTONE[32] v2.0基准测试软件来收集客户和服务器端的测试数据。这些数据包括服务器平均吞吐量和客户平均响应时间。WebSTONE是一个标准的基准测试工具,可以生成负载请求、模拟典型的Web服务器文件访问模式。我们的实验使用WebSTONE来为特定大小的文件生成负载及收集统计数据,以确定不同的并发和事件分派策略的影响。
在此测试中所用的文件访问模式如表2-1所示。该表显示在流行的服务器上的实际负载条件,其数据来源于SPEC所指导的一项对文件访问模式的研究[33]。
文档大小 |
频度 |
500字节 5K字节 50K字节 5M字节 |
35% 50% 14% 1% |
表2-1 文件访问模式
下面给出的结果比较了JAWS Web服务器的若干不同的适配版本的性能。我们将讨论不同的事件分派和I/O模型对于吞吐量和响应时间的影响。为这次实验,使用了JAWS的三种适配版本。
1.
同步Thread-per-Request:在此适配版本中,JAWS被配置成派生一个新线程来处理每个到来的请求,并且使用同步I/O。
2.
同步线程池:JAWS被配置成在使用同步I/O的同时、预派生一个线程池来处理到来的请求。
3.
异步线程池:对于这种配置,JAWS被配置成在为异步I/O使用TransmitFile的同时、预先派生一个线程池来处理到来的请求。TransmitFile是Win32函数,它在网络连接上同步或异步地发送文件。
吞吐量被定义为每秒钟客户接收到的平均bit数。在客户基准测试软件发送HTTP请求之前,启动了一个高精度的用于吞吐量测量的定时器。只要连接在客户端一被关闭,定时器就会停止。接收到的bit数包括服务器发送的HTML头。
响应时间被定义为从客户发送请求开始,到完整地接受完文件为止、客户所看到的以毫秒为单位的延迟量。它测量在发送HTTP GET请求给Web服务器之后、并且在内容开始到达客户之前,最终用户必须等待多长时间。客户基准测试软件一发送HTTP请求,用于响应时间测量的定时器就会启动;客户一完成对所请求的来自服务器的文件的接收,定时器就会停止。
下面的五幅图分别是实验中所用的不同大小的文件(从500字节到5M字节,因子为10)的测试结果。这些文件大小代表在我们的实验中所测量的文件大小的“谱段”,用以揭示文件大小对性能都有什么影响。
图2-19到图2-23演示当所请求文件的大小和服务器点击率系统地增加时,吞吐量的变化情况。如我们所预期的,在每秒的连接数增长时,每个连接的吞吐量通常会退化。这源于正在被维护的同时连接数的增长,导致了每个连接吞吐量的下降。
如图2-21所示,当连接负载增长时,对于较小文件,Thread-per-Request的吞吐量有可能迅速退化。相反,同步线程池实现的吞吐量退化要更为温和。这种差异的原因是Thread-per-Request导致更高的线程创建开销,因为要为每个GET请求派生一个新线程。相反,线程池策略中的线程创建开销通过在服务器开始执行时预先派生线程而分摊了。
图2-19到2-23中的结果说明对于小文件(也就是,< 50K字节),TransmitFile执行得异常糟糕。我们的实验表明TransmitFile的性能直接依赖于同时请求数。我们相信在繁重的服务器负载情况下(也就是,高点击率),TransmitFile被迫在内核处理到来的请求时进行等待。这创建了数目很大的同时连接,从而降低了服务器性能。
但是在文件的的大小增长时,TransmitFile很快就胜过了同步分派模型。例如,在5M字节文件(在图2-23中显示)的繁重负载下,它超出下一种最为接近的模型大约40%。TransmitFile被优化以利用Windows NT内核特性,从而减少了数据拷贝和上下切换的次数。
图2-19到2-23演示当所请求文件的大小和服务器点击率增加时,响应性能的变化情况。如我们所预期的,在每秒的连接数增长时,每个连接的响应时间通常也会变长。这反映出加在服务器上的额外负载,降低了它服务新客户请求的能力。
和前面一样,对于小文件,TransmitFile执行得异常糟糕。但是,在文件大小增长时,相对于轻负载情况下的同步分派,它的响应时间得到了迅速的改善。
如这一部分中的结果所示,取决于并发和事件分派机制,吞吐量和响应时间有着显著的变化。对于小文件,同步线程池策略提供了更好的整体性能。在中等负载下,同步事件分派模型提供的响应时间要略微优于异步模型。但是,在传输大文件的繁重负载情况下,使用TransmistFile的异步模型提供了更好的服务质量。因而,在Windows NT下,取决于服务器的工作负载和文件请求的分布,理想的Web服务器应该将自身适配到合适的事件分派和文件I/O模型。
图2-19 500字节文件的实验结果
图2-20 5K文件的实验结果
图2-21 50K文件的实验结果
图2-22 500K文件的实验结果
图2-23 5M文件的实验结果
在我们的研究中,我们已经发现有可能通过更好的服务器设计来改善服务器性能(在[34]中给出了类似的观察报告)。因而,在“硬编码”服务器(也就是,使用固定并发、I/O和缓存策略的服务器)能够不可否认地提供极好的性能的同时,灵活的服务器构架,比如JAWS,并不必然与糟糕的性能相联系。
这一部分总结决定Web服务器性能的最为重要的因素。这些观察报告基于我们对现有服务器设计和实现策略、以及我们调谐JAWS的经验的研究[1, 3]。这些研究揭示了开发高性能Web服务器的主要优化目标。
轻量级并发:如在[3]中所看到的,基于进程的并发机制可能会产生糟糕的性能。在多处理器系统中,基于进程的并发机制可以良好地执行,特别是在进程数目与处理器数目相等时。在这种情况下,每个处理器可以运行一个Web服务器进程,从而使上下文切换开销得以最小化。
通常,进程应被预先生成,以避免动态进程创建的开销。但是,更可取的方法是使用轻量级并发机制(例如,使用POSIX线程)来使上下文切换开销最小化。和进程一样,动态线程创建开销也可以通过在服务启动时将线程预先派生进线程池中来加以避免。
专用OS特性:OS供应商常常会提供能给出更好性能的专用编程接口。例如,Windows NT 4.0提供TransmitFile函数,使用Windows NT虚拟内存缓存管理器来获取文件数据。TransmitFile允许在文件数据之前和之后分别增加数据。这特别适用于Web服务器,因为它们通常与所请求文件一起发送HTTP头数据。因此,所有给客户的数据可以在单个的系统调用中发送,从而使模式切换开销得以最小化。
通常,这些接口必须相对于标准API仔细进行基准测试,以了解特定接口给出更好性能的条件。如2.4所示,在使用TransmitFile的情况下,我们的实验数据表明,在Windows NT上通过Socket传输大文件,异步形式的TransmitFile是最为高效的机制。
请求生存期系统调用开销:在Web服务器中,请求生存期被定义为服务器在它从客户接收HTTP请求之后、以及在它发送出所请求文件之前,所必须执行的一系列指令。执行请求生存期所用的时间直接影响客户观察到的响应时间。因此,在此路径上使系统调用开销和其他处理最小化是很重要的。下面描述在Web服务器中可以减少这样的开销的各种地方。
在某些情况下,获取和释放锁还可能导致占先(preemption)。因而,如果线程读进HTTP请求,随后试图获取一个锁,它就有可能会被占先,并可能在它被再次分派之前等待相对较长的时间。这会增加Web客户所经受的响应延迟。
日志开销:大多数Web服务器允许管理员对他们所服务的不同页面的点击数进行日志记录。日志常常用于评估服务器在一天的不同时间里的负载情况。它还常常因为商业原因而使用,例如,网站可能基于页面的点击率来确定广告的费率。但是,因为以下原因,对HTTP请求进行日志记录会产生显著的开销:
传输层优化:应该配置下面的传输层选项,以提高Web服务器在高速网络上的性能:
在计算能力和网络带宽在过去十年间戏剧性增长的同时,开发通信软件所涉及的复杂性同样也发生了戏剧性增长。特别地,高性能Web服务器软件的设计和实现是昂贵而易错的。许多代价和工作都来自于对基本的设计模式和构架组件持续的重新发现和发明。而且,越来越多的硬件体系结构异种性和OS及网络平台间的差异使得从头开始构建正确、可移植和高效的Web服务器变得十分困难。
构建高性能Web服务器需要了解服务器中的每个子系统(例如,并发和事件分派、服务器请求处理、文件系统访问和数据传输)对于性能的影响。高效地实现和集成这些子系统要求开发者迎接各种设计挑战,并在可选的解决方案中进行选择。领会在这些可选方案之间进行的权衡对于提供最优性能来说是必需的。但是,在需求(不可避免地)变化时,开发特定设计所需的工作常常并非是经济的。例如,性能可能被证明为不足、可能需要另外的功能,或是软件需要被移植到不同的体系结构。
通过有效利用已被证明的设计模式和实现、以产生可被定制来满足新应用需求的可复用组件,面向对象应用构架和设计模式有助于减少开销、并改善软件的性能。在本论文中描述的JAWS构架演示了怎样简化和统一高性能Web服务器的开发。JAWS成功的关键是它能够捕捉常见的通信软件设计模式,并将这些模式统一进灵活的构架组件中;这些组件高效地封装并增强了低级的OS机制:并发、事件多路分离、动态配置、文件缓存,等等。这里给出的基准测试结果用于演示使用构架来开发高性能应用的有效性。它照亮了这一事实:可适应不同情况的软件与性能并非是对立的。
JAWS的源码和文档可在http://www.cs.wustl.edu/~schmidt/ACE.html找到。
[1] J. Hu,
I. Pyarali, and D. C. Schmidt, “Measuring the Impact of Event Dispatching and
Concurrency Models on Web Server Performance Over High-speed Networks,” in Proceedings
of the 2nd Global Internet Conference, IEEE, November 1997.
[2] I.
Pyarali, T. H. Harrison, and D. C. Schmidt, “Design and Performance of an
Object-Oriented Framework for High-Performance Electronic Medical Imaging,” USENIX
Computing Systems, vol. 9, November/December 1996.
[3] J. Hu,
S. Mungee, and D. C. Schmidt, “Principles for Developing and Measuring
High-performance Web Servers over ATM,” in Proceeedings of INFOCOM ’98,
March/April 1998.
[4] R.
Johnson, “Frameworks = Patterns + Components,” Communications of the ACM,
vol. 40, Oct. 1997.
[5] E.
Gamma, R. Helm, R. Johnson, and J. Vlissides, Design Patterns: Elements of
Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.
[6] D. C.
Schmidt and T. Suda, “An Object-Oriented Framework for Dynamically Configuring
Extensible Distributed Communication Systems,” IEE/BCS Distributed Systems
Engineering Journal (Special Issue on Configurable Distributed Systems),
vol. 2, pp. 280–293, December 1994.
[7] D. C.
Schmidt, “Applying Design Patterns and Frameworks to Develop Object-Oriented
Communication Software,” in Handbook of Programming Languages (P. Salus,
ed.), MacMillan Computer Publishing, 1997.
[8] D. C.
Schmidt, “A Family of Design Patterns for Application-level Gateways,” The
Theory and Practice of Object Systems (Special Issue on Patterns and Pattern
Languages), vol. 2, no. 1, 1996.
[9] T. H.
Harrison, D. L. Levine, and D. C. Schmidt, “The Design and Performance of a
Real-time CORBA Event Service,” in Proceedings of OOPSLA ’97, (Atlanta,
GA), ACM, October 1997.
[10] D. C.
Schmidt and C. Cleeland, “Applying Patterns to Develop Extensible and
Maintainable ORB Middleware,” Communications of the ACM, to appear,
1998.
[11] M. K.
McKusick, K. Bostic, M. J. Karels, and J. S. Quarterman, The Design and
Implementation of the 4.4BSD Operating System. Addison Wesley, 1996.
[12]
“Information Technology – Portable Operating System Interface (POSIX) – Part 1:
System Application: Program Interface (API) [C Language],” 1995.
[13] W. R.
Stevens, UNIX Network Programming, Second Edition. Englewood Cliffs, NJ:
Prentice Hall, 1997.
[14] D. C.
Schmidt, “Reactor: An Object Behavioral Pattern for Concurrent Event
Demultiplexing and Event Handler Dispatching,” in Pattern Languages of
Program Design (J.O.Coplien and D. C. Schmidt, eds.), pp. 529–545, Reading,
MA: Addison-Wesley, 1995.
[15] R. G.
Lavender and D. C. Schmidt, “Active Object: an Object Behavioral Pattern for
Concurrent Programming,” in Pattern Languages of Program Design (J. O.
Coplien, J. Vlissides, and N. Kerth, eds.), Reading, MA: Addison-Wesley, 1996.
[16] D. C.
Schmidt, “Experience Using Design Patterns to Develop Reuseable Object-Oriented
Communication Software,” Communications of the ACM (Special Issue on
Object-Oriented Experiences), vol. 38, October 1995.
[17] A. Stepanov
and M. Lee, “The Standard Template Library,” Tech. Rep. HPL-94-34,
Hewlett-Packard Laboratories, April 1994.
[18] D. C.
Schmidt, “ACE: an Object-Oriented Framework for Developing Distributed
Applications,” in Proceedings of the 6th USENIX C++ Technical
Conference, (Cambridge, Massachusetts), USENIX Association, April 1994.
[19] D. C.
Schmidt, “Acceptor and Connector: Design Patterns for Initializing
Communication Services,” in Pattern Languages of Program Design (R.
Martin, F. Buschmann, and D. Riehle, eds.), Reading, MA: Addison-Wesley, 1997.
[20] P.
Jain and D. C. Schmidt, “Service Configurator: A Pattern for Dynamic
Configuration of Services,” in Proceedings of the 3rd Conference
on Object-Oriented Technologies and Systems, USENIX, June 1997.
[21] T.
Harrison, I. Pyarali, D. C. Schmidt, and T. Jordan, “Proactor – An Object
Behavioral Pattern for Dispatching Asynchronous Event Handlers,” in The 4th
Pattern Languages of Programming Conference (Washington University technical
report #WUCS-97-34), September 1997.
[22] I.
Pyarali, T. H. Harrison, and D. C. Schmidt, “Asynchronous Completion Token: an
Object Behavioral Pattern for Efficient Asynchronous Event Handling,” in Pattern
Languages of Program Design (R. Martin, F. Buschmann, and D. Riehle, eds.),
Reading, MA: Addison-Wesley, 1997.
[23] F.
Buschmann, R. Meunier, H. Rohnert, P. Sommerlad, and M. Stal, Pattern-Oriented
Software Architecture - A System of Patterns. Wiley and Sons, 1996.
[24] D. C.
Schmidt, “GPERF: A Perfect Hash Function Generator,” in Proceedings of the 2nd
C++ Conference, (San Francisco, California), pp. 87–102, USENIX, April
1990.
[25] P.
Jain and D. C. Schmidt, “Service Configurator: A Pattern for Dynamic
Configuration and Reconfiguration of Communication Services,” in The 3rd
Pattern Languages of Programming Conference (Washington University technical
report #WUCS-97-07), February 1997.
[26] T.
Berners-Lee, R. T. Fielding, and H. Frystyk, “Hypertext Transfer Protocol –
HTTP/1.0,” Informational RFC
1945,
Network Working Group, May 1996. Available from http://www.w3.org/.
[27] R.
Fielding, J. Gettys, J. Mogul, H. Frystyk, and T. Berners-Lee, “Hypertext
Transfer Protocol – HTTP/1.1,” Standards Track RFC 2068, Network Working Group,
January 1997. Available from http://www.w3.org/.
[28] D. C.
Schmidt and C. D. Cranor, “Half-Sync/Half-Async: an Architectural Pattern for
Efficient and Well-structured Concurrent I/O,” in Pattern Languages of
Program Design (J. O. Coplien, J. Vlissides, and N. Kerth, eds.), Reading,
MA: Addison-Wesley, 1996.
[29] J. C.
Mogul, “Hinted caching in the Web,” in Proceedings of the Seventh SIGOPS
European Workshop: Systems Support for Worldwide Applications, 1996.
[30] S.
Williams, M. Abrams, C. R. Standridge, G. Abdulla, and E. A. Fox, “Removal
Policies in Network Caches for World Wide Web Documents,” in Proceedings of
SIGCOMM ’96, (Stanford, CA), pp. 293–305, ACM, August 1996.
[31] E. P.
Markatos, “Main memory caching of web documents,” in Proceedings of the
Fifth International World Wide Web Conference, May 1996.
[32] Gene
Trent and Mark Sake, “WebSTONE: The First Generation in HTTP Server
Benchmarking.” Silicon Graphics, Inc. whitepaper, February 1995. Available from
http://www.sgi.com/.
[33] A.
Carlton, “An Explanation of the SPECweb96 Benchmark.” Standard Performance
Evaluation Corporation whitepaper, 1996. Available from
http://www.specbench.org/.
[34] H. F.
Nielsen, J. Gettys, A. Baird-Smith, E. Prud’hommeaux, H. W. Lie, and C. Lilley,
“Network Performance Effects of HTTP/1.1, CSS1, and PNG,” in To appear in Proceedings
of ACM SIGCOMM ’97, 1997.
[35] M. S.
Johns, “Identification Protocol,” Network Information Center RFC 1413,
Feb. 1993.