分布式原则

 

分布式设计的5个原则

分层结构允许在不影响其他层的情况下修改某 一层的实现。同时,它也允许将来从物理上灵活地分隔各个层。但是,正如下面紧接着的部分中所述,不应该轻易决定在独立进程或机器上执行每一层。如果您决定 对某一层进行分布处理,那么必须对它进行特殊的分布设计。令人迷惑的是,某些设计策略实际上与传统的面向对象原则相矛盾。为弄清这些问题,这一部分阐述了 几项用于有效分布应用程序的原则以及使用这些原则的理由。

原则1:保守分布

对于分布式编程的书籍来说,这个原则看起来让人有些惊奇。然而,这项原则却是基于计算中一个简单且不可否认的事实:调用不同进程上对象的方法要比调用进程内对象的方法慢数百倍;将对象移动到网络中的另一台计算机上,这种方法调用又会慢数十倍

那什么时候才应当进行分布式处理呢?以前的观点是只有必须进行分布时才这样做。但是您可能想了解更多的细节,所以让我们从数据层开始考虑几个示例。通常,应用的数据库运行在独立的专用服务器上—— 换句话说,它相对于其他层是分布式的。这样做有几个很好的理由:

      数据库软件复杂而昂贵,而且通常需要高性能的硬件,所以分布数据库软件的多个副本将导致开销太大。

      数据库可包含和关联由许多应用程序共享的数据。但是,只有当每个应用程序正在访问单独的数据库服务器而不是自己的本地副本时,这种情况才可能发生

      数据库被设计为运行在独立的物理层上。它们提供最终的“chunky”接口:结构化查询语言(SQL)(请参考原则3以获得与chunky接口相关的细节。)

因此,当您决定使用数据库时,一般需要决定分布数据源逻辑。然而,决定分布表示逻辑会更复杂一些。首先,除非所有应用程序用户都使用公共的终端(例如ATM),否则表示层的某些部分就必须分布到每个用户机上。但问题是分布到什么程度。当然,近来的趋势是在服务器上执行大部分逻辑,而将简单的HTML发送到客户端Web浏览器。实际上,这正是遵守了保守分布的原则。然而,它也需要每个用户交互都遍历服务器,从而这些用户交互才能产生正确的响应。

Web迅速发展之前,普遍的情况是在每个客户机上执行整个表示逻辑(遵守原则2)。这样可与用户更快地交互,因为它最小化了服务器上的遍历,但它也需要更新用户接口并被部署到整个用户群。最后,选择使用哪一个客户机基本上与分布设计原则无关,但都与期望的用户经验和部署问题有关。

数 据逻辑几乎总是在一个独立的计算机上执行,表示层通常也是如此。现在只剩下业务逻辑,它是整个问题组中最复杂的部分。业务层有时被部署到每个用户,而其他 时候则被保存到服务器上。在许多情况下,业务层被分解成两个或更多个组件。与用户接口交互相关的组件被部署到客户机处,而与数据访问相关的组件则被保存到 服务器上。这就遵守了下一个原则,即相关内容本地化。

可以看到,您有许多分布选项。分布的时间、原因以及如何分布等受到很多因素的影响—— 其中的许多因素又互相竞争。下面几个原则会提供进一步的指导。

原则2:本地化相关内容

如果决定或被迫分布全部或部分业务逻辑层,那么应当保证经常交互的组件被放置在一起。换句话说,您应当本地化相关内容。例如,参考图1-1所 示的电子商务应用。这个应用程序将客户组件、商品组件和购物车组件分隔到指定的服务器上,这会在表面上允许并行执行。然而,当一件商品添加到购物车时,组 件之间就会进行多次交互。每一次交互都会带来跨网络方法调用的系统开销。因此,这种跨网络的行为会抵消掉并行处理带来的好处。再考虑到几千用户会同时使 用,可想其后果是破坏性的。如果还用前面提及的马和马车的类比,这种情况就等同于利用马的每条腿而不是整匹马。

1-1  一个不成功的分布式应用例子

在本地化相关内容的同时,如何利用分布式编程(也就是并行处理)的作用呢?再买一匹马?那就意味着复制整个应用程序并使它运行在另一台专门的服务器上。可以使用负载平衡方法将每个客户机请求路由到特殊的服务器,即如图1-2所示的这种结构。基于Web的应用程序经常通过在几个Web服务器上驻留相同的Web站点来使用这种模型,有时该设置被称为Web场。

1-2  一个成功的分布式应用示例

对应用程序服务器进行复制和均衡负载可以很好地提高应用程序的容量或不伸缩性。然而,您需要非常清楚如何管理状态。更详细的信息请参考原则4

原则3:使用Chunky接口,而不是chatty接口

面向对象编程的思想之一是创建具有许多简单方法的对象,每一个方法都专注于一个特殊的行为。考虑下面的Customer类。

 Class Customer

 {

         public string FirstName()

         { get; set;}

         public string LastName()

         { get; set;}

         public string Email()

         { get; set;}

         //etc. for Street, State, City, Zip, Phone ...

 

         public void Create();

         public void Save();

 }

这种实现会受到大多数面向对象专家的肯定。但是,我的第一反应是:相对于调用代码而言该对象在何处运行。如果直接在过程中访问Customer类,即使从大多数标准来看,这种设计也是非常正确的。但是,如果这个类被执行在其他过程或机器中的代码所调用,则无论现在或是将来,这种设计都是非常糟糕的。要了解具体原因,可考虑下面的代码,并且设想它正运行在纽约的客户机上,而Customer对象运行在伦敦的服务器上。

 Static void ClientCode()

 {

         Customer cust = new Customer();

         cust.Create();

         cust.FirstName = ″Nigel ″;

         cust.LastName = ″Tufnel ″;

         cust.Email = ″ntufnel@spinaltap.com ″;

         //etc. for Street, State, City, Zip, Phone...

 

         cust.Save();

 }

与前面相同的是,如果Customer对象位于客户机进程中,则这个示例不会产生任何问题。可是,设想一下每个属性和方法调用都要跨越大西洋进行遍历,这将会产生很严重的性能问题。

这个Customer类具有典型的chatty接口,或被更专业地称作细粒度接口。相反,哪怕是被进程外代码偶尔访问的对象也应当被设计成具有chunky接口,或者说是粗粒度接口。下面是具有chunky接口的Customer类。

 Class Customer

 {

         public void Create(string FirstName, string LastName, string Email,

                                                 //etc for Street, State, City, Zip, Phone ...

                                               );

         public void Save(string FirstName, string LastName, string Email,

                                         //etc for Street, State, City, Zip, Phone ...

                                        );

 }

可以看出,这段代码不如第一个Customer类那么清晰。但是相对于前者所具有的更多面向对象的特点而言,当Web站点的访问量突然变大,以至于需要扩充容量来满足新用户的访问时,后者却会提供更多的保护。

顺带提一下,可以简化具有chunky接口的Customer类。不需要将每一份客户数据作为独立的参数来传输,可以将客户数据封装到一个客户类中,而只需传输这个客户类。下面就是这种情况的示例。

 [Serializable] // <-- Explained in Chapter 2!

 class CustomerData

 {

         public string FirstName()

         { get; set;}

         public string LastName()

         { get; set;}

         public string Email()

         { get; set;}

         //etc for Street, State, City, Zip, Phone ...

 }

 

 class Customer

 {

         public void Create(CustomerData data);

         public void Save(CustomerData data);

 }

初看这段代码,它将chatty接口从Customer类移到了Customerdata类中。这样做有什么好处?关键之处是在CustomerData类定义前的Serializable特性。它告诉.NET运行库,只要对象越出进程边界就复制整个对象。因此,当客户机代码调用CustomerData类的属性时,实际上在访问一个本地对象。在第2章和第3章中会进一步讨论串行化和可串行化对象。

原则4:优先选用无状态对象,而不是有状态对象

如果上一个原则违背了面向对象拥护者的看法,那这个原则可能会激怒他们。与严格的面向对象定义相比,术语“无状态对象”就显得有些矛盾。然而,如果想利用在图1-2中显示的负载平衡体系结构,您就需要仔细管理分布式对象中的状态,或者干脆不使用状态。要记住,这条原则和原则3一样只能适用于分布在边界上并可能跨越进程边界的对象。而进程内的对象则可以自由地保存状态,而不会危及应用程序的可伸缩性。

无 状态对象这个术语看起来会在开发人员中引起混淆。下面尽可能简洁地定义它:无状态对象是能够在方法调用之间被安全创建和销毁的对象。这是个简单的定义,但 包含很多含义。首先,注意“能够”这个词。应用程序不需要在方法调用之后销毁无状态对象。但如果应用程序选择销毁它,这个动作也不会影响到其他用户。这个 特性不是轻易就能实现的,您必须对类进行特殊实现,这样类才不会依赖于公共方法调用之后实例字段是否继续存在。因为对实例字段没有依赖关系,所以无状态对 象倾向于使用chunky接口。

有 两个原因可说明有状态对象对于可伸缩性具有负面的影响。首先,有状态对象通常会在服务器上存在很长一段时间。在它的生存期中,它会聚集并使用服务器资源。 这样即使有状态对象不工作或正在等待其他用户的请求,它也会阻止其他对象使用这些资源。虽然一些人认为内存是资源竞争中的关键资源,但实际上这只是相对次 要的因素。如图1-3所示,有状态对象是耗费稀有资源(例如数据库连接)的真正罪魁祸首。

1-3  计算机资源的相对数量

有状态对象对可伸缩性具有负面影响的第二个原因是它们最小化了跨越多个服务器对应用程序进行复制和负载平衡的效率。考察图1-4的场景。

将图1-4看作是应用程序在某一时间点的快照。在该快照之前系统的负载很重。然而,在快照时间点许多用户已离开,只有3个客户机仍与系统进行连接。不过,应用程序使用了有状态对象,并且在系统重负载期间,所有的3个对象都被创建在服务器A上。现在,即使服务器B完全闲置,从客户机传输过来的请求也必须被发送到负载很重的服务器A上,因为在服务器A上保存着客户状态。如果这个应用程序使用了无状态对象,那么负载平衡器会直接将客户请求传递到负载最轻的服务器上,而不用考虑以前使用的是哪个服务器。

         1-4  有状态对象在需要负载平衡的环境下不能很好地工作

通过使用智能缓存、会话管理和负载均衡的方法,可以避免或者最小化使用有状态对象所带来的问题。这就是原则4中选用无状态对象的原因,但不是必须使用。这里需要再次说明,这个原则只能应用于为在其他进程或机器中执行的代码所提供的对象。

原则5:接口编程,而不是具体实现的编程

因为前两个原则直接与典型的面向对象经验相矛盾,看起来似乎面向对象编程对分布式编程用处不大。然而这根本不是所倡导的。所提倡的是某些面向对象原则(例如chatty接口和有状态对象)不应该用于应用程序中位于分布边界的对象。

其他的面向对象原则可被很好地移植到分布式环境中。特别是接口(而不是具体实现)编程在分布式编程领域得到了广泛的共识。它解决的问题与性能或可伸缩性无关。相反,接口提供了更简单的方式,从而可以减少频繁的且时常出问题的部署。

考虑到COM是完全基于接口的,向.NET平台的转变会使人们认为基于接口的编程不再受人欢迎。其实不然。虽然.NET允许直接的对象引用,但它也完全支持基于接口的编程。而且,如同在第5章将要学到,接口为将类型信息发布到客户端提供了一个简便的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值