Prashant Jain Douglas C. Schmidt
一系列迅速增长的通信服务正出现在Internet上。通信服务是为客户提供功能的服务器中的组件。在Internet可用的服务包括:WWW浏览和内容获取服务(例如,Alta Vista、Apache、Netscape的HTTP服务器)、软件发布服务(例如,Castinet)、电子邮件和网络新闻传输代理(例如,sendmail和nntpd)、远程文件访问(例如,ftpd)、远程终端访问(例如,rlogind和telnetd)、路由表管理(例如,gated和routed)、主机和用户活动报告(例如,fingerd和rwhod)、网络时间协议(例如,ntpd),以及请求代理服务(例如,orbixd和RPC portmapper),等等。
实现这些服务的常用方法是将每个服务开发成为单独的程序,随后编译、链接,并在单独的进程中执行每个程序。但是,这样的“静态”配置服务方法生成不灵活、常常也是低效的应用和软件体系结构。静态配置的主要问题是它相对于应用中的其他服务,将特定服务的实现和服务的配置紧耦合在一起。
本文描述服务配置器(Service Configurator)模式,它通过使服务的行为与服务实现被配置进应用的时间点去耦合来增强应用的灵活性(常常还有性能)。本文使用用C++编写的分布式时间服务作为例子来演示服务配置器模式。但是,服务配置器已经以许多方式被实现,范围从现代操作系统(像Solaris和Windows NT)中的设备驱动程序,到Internet超级服务器(像inetd和Windows NT服务控制管理器),以及Java applets。
使服务的行为与服务实现被配置进应用或系统的时间点去耦合。
超级服务器。
服务配置器模式使服务的实现与服务被配置进应用或系统的时间点去耦合。这样的去耦改善了服务的模块性,并允许服务独立于配置问题(比如两个服务是否必须驻留在一起,或是采用何种并发模型执行服务)而持续发展。
此外,服务配置器模式使它配置的服务的管理集中化。这便利了服务的自动初始化和终止,并且可以通过将常用的服务初始化和终止模式分解进高效的可复用组件而提高性能。
这一部分使用分布式时间服务作为例子来说明服务配置器模式的动机。
当服务需要进行动态发起、挂起、恢复和终止时,应该采用服务配置器模式。此外,如果服务配置决策必须被推迟至运行时,也应该采用服务配置器模式。

图10-1 分布式时间服务
为说明该模式的动机,考虑图10-1中所示的分布式时间服务。该服务为在局域网和广域网中协作的计算机提供准确、容错的时钟同步。在要求多个主机维护精确的全局时间的分布式系统中,同步时间服务是很重要的。例如,大型分布式医学成像系统[1]需要全局同步的时钟,以确保病人的检查被精确地记录时间,并由放射科医生通过健康保健递送系统迅捷地加以分析。
- 时间服务器回答事务员作出的关于时间的查询。
- 事务员查询一或多个时间服务器,以确定正确的时间、使用若干分布式时间算法[2, 3]中的一种来计算近似正确的时间,并且更新它自己的本地系统时间。
- 客户使用事务员维护的全局时间信息来提供与其他主机上的客户所用的时间观念的一致性。
一种实现分布式时间服务的方法是将时间服务器、事务员和客户的逻辑功能静态地配置进分离的物理进程。在该方法中,可有一或多个主机运行时间服务器进程;后者处理来自事务员进程的时间更新请求。下面的C++代码段演示静态配置的时间服务器进程的结构:
// The Clerk_Handler processes time requests
// from Clerks.
class Clerk_Handler :
public Svc_Handler
{
public:
// This method is called by the Reactor
// when requests arrive from Clerks.
virtual int handle_input (void)
{
// Read request from Clerk and reply
// with the current time.
}
// ...
};
// The Clerk_Acceptor is a factory that
// accepts connections from Clerks and creates
// Clerk_Handlers.
typedef AcceptorClerk_Acceptor;
int main (int argc, char *argv[])
{
// Parse command-line arguments.
Options::instance ()->parse_args (argc, argv);
// Set up Acceptor to listen for Clerk connections.
Clerk_Acceptor acceptor(Options::instance ()->port ());
// Register with the Reactor Singleton.
Reactor::instance ()->register_handler(&acceptor, ACCEPT_MASK);
// Run the event loop waiting for Clerks to
// connect and perform time queries.
for (;;)
Reactor::instance ()->handle_events ();
/* NOTREACHED */
}
该程序使用反应堆(Reactor)模式[4]和接受器(Acceptor)模式[5]来实现静态配置的时间服务器进程。
每个需要全局时间同步的主机都可以运行事务员进程。事务员基于接收自一或多个时间服务器的值周期性地更新它们的本地系统时间。下面的C++代码段演示静态配置的事务员进程的结构:
// This class communicates with the Time_Server.
class Time_Server_Handler { /* ... */ };
// This class establishes connections with the
// Time Servers and periodically queries them for
// their latest time values.
class Clerk : public Svc_Handler
{
public:
// Initialize the Clerk.
Clerk (void)
{
Time_Server_Handler **handler = 0;
// Use the Iterator pattern and the
// Connector pattern to set up the
// connections to the Time Servers.
for (ITERATOR iterator (handler_set_);
iterator.next (handler) != 0;
iterator.advance ())
{
connector_.connect (*handler);
Time_Value timeout_interval (60);
// Register a timer that will expire
// every 60 seconds. This will trigger
// a call to the handle_timeout() method,
// which will query the Time Servers and
// retrieve the current time of day.
Reactor::instance ()->schedule_timer
(this, timeout_interval);
}
}
// This method implements the Clock Synchronization
// algorithm that computes local system time. It
// is called periodically by the Reactor’s timer
// mechanism.
int handle_timeout (void)
{
// Periodically query the servers by iterating
// over the handler set and obtaining time
// updates from each Time Server.
Time_Server_Handler **handler = 0;
// Use the Iterator pattern to query all
// the Time Servers
for (ITERATOR iterator (handler_set_);
iterator.next (handler) != 0;
iterator.advance ())
{
Time_Value server_time = (*handler)->get_server_time ();
// Compute the local system time and
// store this in shared memory that
// is accessible to the Client processes.
}
}
private:
typedef Unbounded_SetHANDLER_SET;
typedef Unbounded_Set_IteratorITERATOR;
// Set of Clerks and iterator over the set.
HANDLER_SET handler_set_;
// The connector_ is a factory that
// establishes connections with Time Servers
// and creates Time_Server_Handlers.
Connectorconnector_;
};
int main (int argc, char *argv[])
{
// Parse command-line arguments.
Options::instance ()->parse_args (argc, argv);
// Initialize the Clerk.
Clerk clerk;
// Run the event loop, periodically
// querying the Time Servers to determine
// the global time.
for (;;)
Reactor::instance ()->handle_events ();
/* NOTREACHED */
}
该程序使用反应堆(Reactor)模式[4]和连接器(Connector)模式[6]来实现静态配置的事务员进程。
客户进程可以使用它们的本地事务员报告的同步的时间。为最少化通信开销,当前时间可被存储在共享内存中,后者被映射到事务员和同一主机上所有客户的地址空间中。除了时间服务,这些主机提供的其他通信服务(比如文件传输、远程登录和HTTP服务器)也可以在分离的静态配置的进程中执行。
尽管像反应堆、接受器和连接器这样的模式的使用改善了上面所示的分布式时间服务器的模块性和可移植性,使用静态方法来配置通信服务有以下缺点:
- 在开发周期中必须过早地作出服务配置决策:这是不合需要的,因为开发者可能无法预先知道使服务组件驻留在一起或分布驻留的最佳方式。例如,在无线计算环境中、内存资源的匮乏可能会迫使客户和事务员被划分进运行在分离的主机上的两个独立进程。相反,在实时的航空控制环境中,可能必须使事务员和服务器驻留进一个进程,以降低通信响应延迟。迫使开发者过早地采用特定的服务配置会阻碍灵活性,并可能降低性能和功能。
- 修改某服务可能会对其他服务产生不利影响:每个服务组件的实现都与它的初始配置紧耦合在一起。这导致开发者难以在不影响其他服务的情况下修改某个服务。例如,在上面提到的实时航空控制环境中,可以静态地配置事务员和时间服务器、使它们在一个进程中执行,以降低响应延迟。但是如果事务员所实现的分布式时间算法被改变,现有的事务员代码就有可能也需要修改、重编译和静态重链接。而终止进程以改变事务员代码可能也会终止时间服务器。对于高度可用的系统(比如电信交换机或呼叫中心[7]),这样的服务中断可能是不能接受的。
- 系统性能不能高效地伸缩:将每个服务与一个进程关联在一起占死了OS资源(比如I/O描述符、虚拟内存和进程表槽口)。如果服务常常空闲,这样的设计可能是很浪费的。而且,对于许多短期生存的通信任务(比如向时间服务器请求当前时间,或在域名服务中解析主机地址请求)来说,进程常常是错误的抽象。在这些情况下,多线程主动对象(Active Object)(8)或单线程反应式[4]事件循环可能更为高效。
更为方便和灵活的实现分布式服务的方法常常是使用服务配置器模式。该模式使通信服务的行为与这些服务被配置进应用或系统的时间点去耦合。服务配置器模式消除了以下需求的压力:
- 延缓对服务的特定类型或特定实现的选择,直至设计周期中非常迟后的阶段:这允许开发者集中考虑服务的功能(例如,时间同步算法),而不用过早地卷入特定的服务配置。通过使功能与配置去耦合,服务配置器模式允许应用独立于系统所用的配置策略和机制而发展。
- 通过编写多个独立开发的、不需要全局知识的服务来构建完整的应用或系统:服务配置器模式要求所有服务都拥有用于配置和控制的统一接口。这使得服务可被当作积木、很容易地作为组件集成进更大的应用中。贯穿所有服务的统一接口使得它们在怎样配置上有着同样的“外观和感受”(“look and feel”)。继而,这样的统一性通过促进“最少惊讶法则”(“principle of least surprise”)而简化了应用开发。
- 在运行时优化、控制和重配置服务的行为:使服务的实现与它的配置去耦合使得调谐服务的特定实现或配置参数成为可能。例如,取决于硬件和操作系统上可用的并行性,在分离的线程或进程中运行多个服务可能更高效,也可能更低效。当有更多的信息可用于帮助优化服务时,服务配置器模式使应用能够在运行时选择和调整这些行为。此外,在分布式系统中增加新服务或更新服务常常可以无需停止现有服务就得以完成。
图10-2使用OMT表示法来演示根据服务配置器模式设计的分布式时间服务的结构。

图10-2 分布式时间服务的结构
Service基类为配置和控制服务(比如时间服务或事务员)提供标准接口。基于服务配置器的应用使用该接口来发起、挂起、恢复和终止服务,以及获取关于服务的运行时信息(比如它的IP地址和端口号)。服务自身驻留在Service Repository(服务仓库)中,可由基于服务配置器的应用来在Service Repository中增加或移除。
Service基类的两个子类出现在分布式时间服务中:Time Server和Clerk。每个子类表示一个在分布式时间服务中有着特定功能的具体Service。Time Server服务负责接收和处理来自Clerk的时间更新请求。Clerk服务是连接器(Connector)[6]工厂,负责(1)为每个服务器创建新连接,(2)动态分配新的处理器,以向相连的服务器发送时间更新请求,(3)通过处理器接收来自所有服务器的回复,以及(4)随后更新本地系统时间。
通过管理时间服务中的服务组件的配置,服务配置器模式使分布式时间服务变得更为灵活,并从而使它与实现问题分离开来。此外,服务配置器提供了构架来将其他通信服务的配置和管理合并在一个管理单元中。
当有以下情况时使用服务配置器模式:
- 服务必须被动态地发起、挂起、恢复及终止;以及
- 服务的实现可能会改变,但是它相对于有关服务的配置保持不变;并且/或者一组驻留在一起的服务可能会改变,但是它们的实现保持不变;或是
- 通过编写多个独立开发和可动态配置的服务,应用或系统可以被简化;或是
- 通过使用单一管理单元进行配置,多个服务的管理可以被简化或优化。
当有以下情况时不要使用服务配置器模式:
- 由于安全限制,动态(重)配置不合需要(在这种情况下,可能必须使用静态配置);或是
- 服务的初始化或终止太过复杂,或与上下文的耦合过于紧密,以致于不能以统一的方式来完成;或是
- 因为从不变动,服务不能从动态配置中受益;或是
- 对于动态(重)配置使用的OS和语言机制所带来的额外的间接层次,苛刻的性能需求要求使其最小化。
在图10-3中使用OMT表示法演示了服务配置器模式的结构:

图10-3 服务配置器模式的结构
服务配置器模式中的关键参与者包括:
- 服务(Service):规定含有挂钩方法[9](比如初始化和终止)的接口,由基于服务配置器的应用用于动态配置Service。
- 具体服务(Clerk和Time Server):实现服务的挂钩方法及其他的服务特有的功能(比如事件处理和与客户的通信)。
- 服务仓库(Service Repository):维护基于服务配置器的应用所提供的所有服务的仓库。这使得管理实体可以对被配置的服务的行为进行集中管理和控制。
