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构架的结构、使用和性能。
