2 JAWS:高性能Web服务器构架

 

James C. Hu Douglas C. Schmidt

 

通信软件的开发者面临着许多挑战。通信软件包含固有的复杂性,比如错误检测和恢复;以及随机的复杂性,比如关键概念和组件的持续的重新发现和发明。应对这些挑战需要对面向对象应用构架和模式有全面的了解。本论文阐释我们是怎样将通信软件的构架和模式应用于开发称为JAWS的高性能Web服务器的。

JAWS是一种面向对象的构架,支持多种Web服务器策略的配置,比如使用异步I/OLRU缓存的线程池并发模型 vs. 使用同步I/OLFU缓存的Thread-per-Request并发模型。因为JAWS是一个构架,可以系统地对这些策略进行定制,独立地或协作地进行评估,以决定最佳的策略方案。使用这些方案,JAWS可以静态地和动态地改变自己的行为,以为给定的软件/硬件平台和工作负载采用最为有效的策略。JAWS的自适配软件特性使其成为用于构造高性能Web服务器的强大应用构架。

 

2.1 介绍

 

在过去的几年中,万维网(Web)上的通信流量发生了戏剧性的增长。流量的增长在很大程度上应归于廉价的和无处不在的Web浏览器(比如NCSA MosaicNetscape NavigatorInternet Explorer)的激增。同样地,Web协议和浏览器也正日益被应用于专门而昂贵的计算任务,比如西门子[1]和柯达[2]所用的图像处理服务器和像AltaVistaLexis-Nexis这样的数据库搜索引擎。

要跟上需求增长的步伐,必须开发高性能Web服务器。但是,在开发者配置和优化Web服务器时,他们面对的是一组异常丰富的设计策略。例如,开发者必须在广泛的并发模型(比如Thread-per-Request vs. 线程池)、分派模型(比如同步 vs. 异步分派)、文件缓存模型(比如LRU vs. LFU),以及协议处理模型(比如HTTP/1.0 vs. HTTP/1.1)中进行选择。没有哪种配置对于所有硬件/软件平台和工作负载来说都是最佳的[1, 3]

所有这些可选策略的存在保证了开发者可以定制Web服务器、以满足用户的需求。但是,在许多设计和优化策略间进行选择是麻烦而易错的。没有相应的指导,开发者将面临艰巨的任务:从头开始设计Web服务器来产生特定的解决方案。这样的系统常常难以维护、定制和调谐,因为许多设计工作都只是花在了使系统可运行上。

 

2.1.1 定义

 

我们将经常引用术语OO类库构架模式组件。这些术语所指的是用于构建可复用软件系统的工具。OO类库是一组软件对象实现,它们在用户调用对象方法时提供可复用功能。构架是一种可复用、“半完成”的应用,可被定制以产生自定义应用[4]。模式表示在特定的上下文中、软件开发问题的可反复使用的解决方案[5]。组件指的是一种“可具体化”的对象。OO类库和构架都是通过实例化和专门化而得以具体化的成组对象。模式组件则通过编码来具体化。

 

2.1.2 综述

 

本论文阐释怎样使用OO应用构架设计模式来产生灵活而高效的Web服务器。模式和构架可以协作应用,以改善Web服务器的效率和灵活性。模式以一种系统而易于理解的形式捕捉高性能和自适配Web服务器的抽象设计和软件体系结构。构架则使用特定的编程语言,比如C++Java,来捕捉Web服务器的具体设计、算法和实现。相反,OO类库提供构建应用所必需的原始材料,但没有对怎样将这些片段放在一起进行指导。

本论文聚焦于用于开发JAWS[1, 3]高性能Web服务器的模式和构架。JAWS既是一个Web服务器,又是一个构架,其他类型的服务器可通过它来进行构建。JAWS构架自身是使用ACE构架[6, 7]来开发的。ACE构架使通信软件领域中的一些关键模式[5]得以具体化。JAWSACE中的构架和模式是有代表性的解决方案,已被成功应用于许多通信系统,范围从电信系统管理[8]到企业医学成像[2]和实时航空控制系统[9]等。

本论文被组织如下:2.2给出对模式和构架的综述,并说明JAWS所提供的通信软件构架类型的动机;2.3阐释怎样应用模式和组件来开发高性能Web服务器;2.4比较JAWS与其他高性能Web服务器在高速ATM网络上的性能;2.5给出结束语。

 

2.2 将模式和构架应用于Web服务器

 

为了给数目正在增长的InternetIntranet用户提供服务和内容,对于高性能Web服务器的需求正在日益增长。Web服务器的开发者正在努力构建快速、可伸缩和可配置的系统。但是,如果不注意避开一些常见的陷阱和缺陷,其中包括麻烦而易错的低级编程细节、缺乏可移植性,以及广泛的设计选择,这样的任务可能是十分困难的。这一部分给出了这些危险的路标。随后我们描述开发者怎样通过有效利用设计和代码复用,将模式和构架应用于避免这些危险。

 

2.2.1 Web服务器软件的常见缺陷

 

Web服务器开发者面临着一些反复发生的挑战,这些挑战在很大程度上独立于特定的应用需求。例如,像其他通信软件一样,Web服务器必须执行多种任务:连接建立、事件处理器分派、进程间通信、内存管理和文件缓存、静态和动态的组件配置、并发、同步,以及持续性。在大多数Web服务器中,这些任务是以特定的方式、使用低级的本地OS应用编程接口(API)(比如用C编写的Win32POSIX)来实现的。

遗憾的是,本地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实现也具有不兼容的、与时俱变的错误:在执行非阻塞连接时会导致偶发的失败。

 

陡峭的学习曲线:由于有过多的细节,掌握OSAPI所需的努力可能是很高的。例如,学习怎样正确地使用POSIX异步I/O[12]来编程十分困难。学习怎样使用异步I/O机制来编写可移植的应用甚至会更困难,因为它们在各OS平台间有着极大的不同。

 

不能处理更高的复杂性:OS API为一些机制定义了基本接口,像进程和线程管理、进程间通信、文件系统,以及内存管理。但是,当应用的大小和复杂性增长时,这些基本接口无法适当地升级。例如,典型的UNIX进程只允许缓冲大约7个待处理连接[13]。对于被大量访问的、必须处理成百并发客户的Web服务器来说,这个数目是不够的。

 

2.2.2 通过模式和构架克服Web服务器的缺陷

 

软件复用是被广泛称许的减少开发工作量的方法。复用有效利用了有经验的开发者的领域知识和以前的成果。在有效地应用时,复用可以避免重新创建和认证常用的、针对重复发生的应用需求和软件设计挑战的解决方案。

Javajava.lang.netRougeWave Net.h++是两个常见的将可复用OO类库应用于通信软件的例子。尽管类库有效地支持小规模的组件复用,它们的范围是严重受限的。特别地,类库不会对相关软件组件族之间的规范控制流和协作进行捕捉。因而,应用基于类库的复用的开发者常常要为每个新应用重新发明和实现整个的软件体系结构。

更为强大的克服上面描述的缺陷的途径是对在成功的Web服务器之下的模式进行标识,并在面向对象应用构架中使这些模式具体化。通过捕捉常见软件开发问题的解决方案,模式和构架有助于减少对关键的Web服务器概念和组件的重新发现和发明[5]

 

将模式应用于Web服务器的好处:模式提供了常见的Web服务器微体系中的结构和参与者的文档。例如,反应堆(Reactor[14]和主动对象(Active Object[15]模式分别被广泛用作Web服务器的分派和并发策略。这些模式是已被证明有益于构建灵活而高效的Web服务器的对象结构的一般化。

传统上,这些模式类型或者被锁在老练的开发者的头脑里,或者被深埋在源码中。但是,让这样有价值的信息只是放在这些地方是危险而昂贵的。例如,如果不编写文档,有经验的Web服务器设计者的洞见可能会随时间而消逝。同样地,可能需要相当的努力才能从现有源码中反向地设计出模式来。因此,为了给负责增强和维护现有软件的开发者保留设计信息,明确地捕捉Web服务器模式并编写文档是必要的。而且,特定领域的知识还有助于指导在其他领域中构建新服务器的开发者的设计决策。

 

将构架应用于Web服务器的好处:模式知识有助于减少开发工作和维护代价。但是,只是复用模式并不足以创建灵活而高效的Web服务器软件。在模式使抽象设计和体系结构知识复用成为可能的同时,被编写为模式的抽象并不会直接产生可复用的代码[16]。因此,有必要增加对模式的研究,考查它们与构架的创建和使用的关系。通过实现常用设计模式、并分解出常见实现角色,构架可帮助开发者避免对标准的Web服务器组件进行昂贵的重新发明。

 

2.2.3 构架、模式,以及其他复用技术之间的关系

 

通过集成成组的抽象类,并定义这些类的实例进行协作的标准方式,构架为应用提供了可复用的软件组件[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构架的结构、使用和性能。

 

2.3 JAWS自适配Web服务器

 

将构架和模式应用于通信软件的好处最好通过例子来演示。这一部分描述JAWS的结构和功能。JAWS是一种高性能和自适配的、实现了HTTP协议的Web服务器。它还是一个平台无关的应用构架,其他类型的通信服务器可以通过它来构建。

 

2.3.1 JAWS构架综述

 

 

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.1DICOM)能够很容易地结合进JAWS。要增加新协议,开发者只需简单地编写新的协议处理器实现,随后将其配置进JAWS构架中。

 

协议流水线(Protocol Pipeline):该构架使过滤器操作能够很容易地与正在被协议处理器处理的数据进行合成。这种集成是通过采用适配器模式来完成的。流水线遵循用于输入处理的管道和过滤器Pipes and Filters)模式[23]。使用服务配置器模式,可在运行时动态链接流水线组件。

 

缓存式虚拟文件系统(Cached Virtual Filesystem):该组件通过减少文件系统访问开销来改善Web服务器性能。可以遵循策略模式[5]来选择多种缓存策略,比如LRULFU、提示式策略和结构化策略。这使得开发者可以根据有效性来对不同的缓存策略进行裁剪,并静态或动态地配置最佳策略。各个Web服务器的缓存通过使用单体Singleton)模式[5]来实例化。

 

Tilde Expander该组件是另一种缓存组件,它使用理想哈希表[24]来将简写的用户登录名(例如,~schmidt)映射到用户主目录(例如,/home/cs/faculty/schmidt)。当个人Web页面存储在用户主目录中、而用户目录又没有驻留在共同的根上时,该组件能够充分地减少访问系统用户信息文件(比如/etc/passwd)所需的磁盘I/O开销。通过服务配置器模式的效力,可以动态地解除Tilde Expander的链接,并将其重新链接进服务器(例如,在新用户加入系统时)。

 

2.3.2 JAWS中的设计模式综述

 

2-3中的JAWS体系结构图演示了JAWS是怎样构造的,但并没有说明它为什么以这种特定的方式构造。要理解JAWS为什么包含有像并发策略I/O策略协议处理器事件分派器这样的构架和组件,需要对在通信软件领域(一般而言)和Web服务器(特定的)之下的设计模式有更深入的了解。图2-4 演示与JAWS有关的战略战术模式。这些模式在下面进行总结。

 

 

2-4 JAWS构架中使用的设计模式

 

2.3.2.1 战略模式

 

下面的模式对于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_filesend_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 StrategyLRU策略))从模式的Service(服务)组件那里借用接口,以使仓库能动态地对它们进行管理。

 

 

 

 

2-9 JAWS中服务配置器模式的结构

 

2.3.2.2 战术模式

 

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构架是怎样被结构、以支持可选策略方案的配置的。

 

2.3.3 并发策略

 

2.3.3.1 设计挑战

 

并发策略会显著地影响Web系统的设计和性能。对现有Web服务器(包括RoxenApachePHTTPDZeusNetscapeJava Web服务器)的实验研究[3]表明大部分与I/O无关的Web服务器开销来自Web服务器的并发策略。关键的开销包括同步、线程/进程创建,以及上下文切换。因此,选择高效的并发策略对于获取高性能来说是至关紧要的。

 

2.3.3.2 可选策略方案

 

选择正确的并发策略并非无关紧要的事情。影响决策的有动态和静态两种因素。静态因素可被预先确定。这些因素包括硬件配置(例如,处理器数目、内存数量,以及网络连接速度)、OS平台(例如,线程和异步I/O的可用性),以及Web服务器使用情况(例如,数据库连接、图像服务器,或是HTML服务器)。动态因素是那些在系统执行过程中发生的可检测和可度量的情况。这些因素包括机器负载、并发请求数、动态内存的使用,以及服务器工作负载。

现有的Web服务器使用了广泛的并发策略来回应有关的众多因素。这些策略包括单线程并发(例如,Roxen)、基于进程的并发(例如,ApacheZeus),以及多线程并发(例如,ApacheJAWS)。每种策略都会产生正面和负面的效果,必须在静态和动态因素的上下文中才能加以分析和评估。这些权衡在下面总结。

 

 

2-10 JAWS中的Thread-per-Request策略

 

Thread-per-Request该模式在单独的线程控制中处理每个来自客户的请求。因而,当每个请求到达时,就会创建一个新线程来处理该请求。这种设计允许每个线程使用同步I/O机制来读写所请求的文件。图2-10JAWS构架的上下文中演示此模式。在这里,接受器反复地等待连接,创建协议处理器,并派生新线程,以使处理器能够继续处理连接。

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.0Thread-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]。因为JAWSI/O构架与它的并发构架是不相关的,我们认为单线程并发策略是线程池的池大小为1时的一种特例。

 

[1][3]中的实验演示了并发和时间分派策略的选择对负载条件遇到变化的Web服务器性能的影响。特别地,没有哪种服务器策略能够为所有情况都提供最佳性能。因而,服务器构架至少应该提供两种程度的自由:

 

1.    静态适配性:构架应该允许Web服务器开发者选择能最好地满足系统的静态需求的并发策略。例如,多处理器机器可能比单处理器机器更适合多线程并发。

2.    动态适配性:构架应该允许它的并发策略动态地适配当前的服务器环境,以在服务器负载发生动态变化的情况下取得最佳性能。例如,为了应付意外的负载使用,有可能必须增加线程池中可用线程的数目。

 

2.3.3.3 JAWS并发策略构架

 

如上面所讨论的,没有哪种并发策略在所有情况下都能最佳地执行。但是,也不是所有平台都能够有效地使用所有可用的并发策略。为解决这些问题,JAWS并发策略构架同时支持相关于它的并发和事件分派策略的静态和动态的适配性。

2-12演示JAWS的并发策略构架的OO设计。Event Dispatcher(事件分派器)和Concurrency(并发)对象依据State(状态)模式来交互。如图中所演示的,server可以在对server->dispatch()的连续调用间改变为使用Thread-per-Connection或线程池,从而使不同的并发策略产生效果。Thread-per-Connection策略是对上面讨论的Thread-per-RequestThread-per-Session策略的抽象。每种并发机制都使用了Task(任务)。取决于并发的选择,任务可以表示单个主动对象,或是一组主动对象并发对象的行为遵循接受器Acceptor)模式。这样的体系结构使得服务器开发者能够集成各种可选的并发策略。通过策略配置文件的帮助,服务器可以在运行时动态地选择不同策略,以获得最佳的性能。

 

 

2-12 并发策略构架的结构

 

2.3.4 I/O策略

 

2.3.4.1 设计挑战

 

Web服务器开发者的另一项关键挑战是设计高效的数据获取和递送策略,合起来称为I/O。围绕高效I/O的问题可以是极具挑战性的。系统开发者常常必须安排多个I/O操作、以利用硬件/软件平台上可用的并发性。例如,高性能Web服务器在并发地解析新获取的来自其他客户的请求时,应该能同时在网络上传输多个文件。

特定类型的I/O操作有着与其他类型的I/O操作不同的需求。例如,涉及货币基金转账的Web事务可能需要同步地运行,也就是,用户在事务结束后才能继续其他操作。相反,访问静态信息的Web,比如基于CGI的搜索引擎查询,可以异步地运行,因为它们可以在任何时候被取消。这些不同的需求把我们引向了不同的执行I/O的策略。

 

2.3.4.2 可选策略方案

 

如上面所指出的,有多种因素影响对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]

 

2.3.4.3 JAWS I/O策略构架

 

[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构架提供派生自InputOutputSynchronousAsynchronousReactive IO组件。各种I/O策略使用适当的机制来发出请求。例如,Synchronous IO组件使用传统的阻塞式readwrite系统调用;Asynchronous IO依据前摄器模式[21]来执行请求;而Reactive IO则使用反应堆模式[14]

InputOutput组件由接受器通过相关联的流、从2.3.3描述的Task组件创建。文件操作通过2.3.6描述的Filecache Handle(文件缓存句柄)组件来执行。例如,send_file操作将由Filecache Handle表示的文件发送给由接受器返回的流。

 

2.3.5 协议流水线策略

 

2.3.5.1 设计挑战

 

早期的Web服务器,像NCSA最初的httpd,执行的文件处理非常少。它们只是简单地取得所请求的文件,将其内容传输给请求者。但是,现代的Web服务器除了执行文件获取,还进行数据处理。例如,HTTP/1.0协议可用于确定各种文件属性,比如文件类型(例如,文本、图像、音频或视频)、文件编码和压缩类型、文件大小,以及它的最后修改日期。这些信息通过HTTP头返回给请求者。

通过引入CGIWeb服务器甚至已经能够执行更为广泛的任务,包括搜索引擎、地图生成、数据库系统连接,以及安全的商业和金融交易,等等。但是,CGI的限制是服务器必须派生新进程来扩展服务器功能。典型地,每个请求要求CGI派生它自己的进程来处理它,致使服务器成了Process-per-Request服务器,一种性能“抑制剂”[3]。高性能Web服务器构架的挑战是:允许开发者扩展服务器功能,而又不诉诸CGI进程。

 

2.3.5.2 可选策略方案

 

概念上,大多数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服务器功能的解决方案不是太针对特定应用,就是太过一般化。例如,NetscapeNetDynamics整个地聚焦于Web服务器与现有数据库应用的集成。相反,基于JavaWeb服务器(比如SunJava ServerW3CJigsaw)允许进行任意的服务器扩展,因为Java应用有能力动态执行任意的Java代码。但是,怎样提供可动态配置的操作流水线的问题仍然要由服务器开发者来解决。因此,开发者需要在不利用基于设计模式的应用构架所提供的好处的情况下,自己定制解决方案的设计。

构造服务器来处理逻辑阶段的数据被称为管道和过滤器Pipes and Filters)模式[23]。在该模式帮助开发者考虑怎样组织处理流水线的组件的同时,它并没有提供完成这些工作的构架。没有这样一个构架,改编自定义服务器、以高效地采用新协议特性的任务可能会太过昂贵、困难,或产生维护开销很高的代码。

 

2.3.5.3 JAWS协议流水线策略构架

 

前面的讨论激发了开发者对扩展服务器功能、而又不诉诸外部进程的需要。我们还描述了怎样将管道和过滤器模式应用于创建静态和动态的信息处理流水线。JAWS协议流水线构架被设计用于简化编写这些流水线所需的工作。这是通过提供流水线组件的任务骨架task skeleton)来完成的。当开发者完成流水线组件逻辑时,组件可随即与其他组件组合,以创建一条流水线。已完成组件被存储进仓库中,以使它们可在服务器运行时被访问。这使得服务器构架可以在Web服务器执行时按照需要动态地创建流水线。

2-15提供了对构架结构的演示。该图描述协议处理器Protocol Handler),它利用协议流水线Protocol Pipeline)来处理到来的请求。过滤器Filter)组件派生自协议流水线,并被用作流水线组件的任务骨架。

 

 

2-15 协议流水线构架的结构

 

流水线的实现者从过滤器派生来创建流水线组件。每个流水线组件的svc方法首先调用父svc,由其获取要处理的数据,并随即针对数据执行它的组件特有的逻辑。父svc调用前面组件的svc方法。因而,组件的组合产生了一条调用链,拖着数据通过流水线。这条调用链在负责获取原始输入的组件那里结束,该组件直接派生自协议流水线抽象。

 

2.3.6 文件缓存策略

2.3.6.1 设计挑战

 

[3]中的结果显示访问文件系统是Web服务器的开销的重要来源。大多数分布式应用都可以从缓存中获益,Web服务器也不例外。因此,不奇怪,对Web服务器性能的研究会聚焦于怎样通过文件缓存来获得更好的性能[29, 30]

缓存是一种存储媒介,它提供比所需信息通常所在的媒介更为高效的检索。在Web服务器的情况下,缓存驻留在服务器的主内存中。因为内存是有限的资源,文件只是暂时地驻留在缓存中。因而,围绕最佳缓存性能的问题是由数量(有多少信息应被存储在缓存中)和持续时间(信息应在缓存中停留多长时间)来决定的。

 

2.3.6.2 可选策略方案

 

分配给缓存的内存大小会极大地影响数量和持续时间。如果内存不足,缓存少量大文件可能是不合需要的,因为缓存许多较少的文件能给出更好的平均性能。如果有更多的内存可用,缓存较大的文件也可能是可行的。

 

最近最少使用(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 缓存虚拟文件系统的结构

 

2.3.6.3 JAWS缓存式虚拟文件系统构架

 

上面描述的解决方案给出了实现文件缓存的若干策略。但是采用固定的缓存策略并不总能提供最佳的性能[31]JAWS缓存式虚拟文件系统构架以两种方法来解决此问题。其一是使开发者很容易地将缓存替换算法和缓存策略集成进构架中。而且,通过利用策略Profile,可以动态地选择这些算法和策略,以在变化的服务器负载条件下优化性能。

2-16演示JAWS中的一些组件,它们互相协作、以构造缓存式虚拟文件系统构架。InputOutput对象对Filecache Handle进行实例化,文件交互(比如读和写)通过该句柄来进行管理。Filecache Handle引用Filecache Object,后者是由Filecache组件来管理的。Filecache Object维护在所有引用它的句柄间共享的信息,比如内存映射文件的基地址。Filecache组件是缓存虚拟文件系统构架的心脏。它通过哈希方法来管理Filecache Object,并且在选择Cache Strategy来处理文件请求时遵循状态模式。Cache Strategy组件利用策略模式来使不同的缓存算法(比如LRULFU)能够互相替换。

 

2.3.7 JAWS