ACE应用-第2章 JAWS:高性能Web服务器构架
发布: 2008-6-13 13:46 | 作者: Douglas C. Schmidt | 来源: 转载 | 查看: 276次
并发策略会显著地影响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表示的文件发送给由接受器返回的流。
