K
kidneyball
Unregistered / Unconfirmed
GUEST, unregistred user!
分布策略
--------------------------------------------------------------------------------
当对象被创建不久,有时甚至是从对象被创建开始,人们就倾向于用分布式的策略来分开它们。但是很多人意识不到,对象的分布(实际上,任何其他东西的分布处理都一样)存在着很多缺陷 —— 特别是在开发商们那些令人舒畅的推销小册子的感染下,这种情况很容易发生。这一章将讲述这些教训 —— 我曾经亲眼看到我的客户用很大的代价来换取这些教训。
分布式对象的吸引之处
让我们看看这样一个故事:这是一个例行的设计总结会议(译注:作者是一个软件架构顾问),这种会议每年都要有两到三次。会上,一个参与新的OO系统开发的系统架构师很自豪地向人们展示一个新的分布式系统。让我们假定,这是一个涉及到customers(顾客),orders(订单),products(产品),和deliveries(货品分发)的系统。他向我展示了一个类似图1的设计。设计方案把customers, orders, products, 和deliveries都作为分离的remote object(远端对象)。每一部分都是一个单独的部件(component),能放在单独的processing node(处理节点)上运行。
(缺图)
图1:一个通过在不同节点运行不同组件实现分布式应用系统的例子
于是我发问:“你为什么要这样做?”
“当然是为了执行效率了!”架构师回答说,并用有点古怪的眼神看着我,“对于每一个部件,我们都可以把它放在一个独立的机器上运行。如果某个部件需要处理的东西太多,我们可以加入更多的机器来处理那个部件 —— 这样我们就有能力来调节整个应用系统的平衡。”说到这里,他的眼神仿佛在说,“你究竟明不明白什么是真正的分布式对象呀?”
这个时候,我面临着一个有趣的两难局面:我究竟应该直接告诉他,他的设计令我感到非常的厌恶,然后任由他把我赶出门外;还是应该尝试慢慢地让他明白过来。当然,第二种做法对我比较有利,但是却需要费上很多的时间。因为我的客户们通常对自己建立的架构非常满意,要他们放弃这样一个美梦是很困难的。
好,假定到现在为止你还没有把我拒之门外的话,我想你会感到好奇:为什么这个分布架构令人不快?毕竟许多工具提供商会告诉你分布式对象的好处就在于你可以把一系列对象根据你的意愿分配到进程节点(processing nodes)中去,而他们强大的中间件则为你提供透明的操作。这种透明操作允许在同一个进程或不同进程——甚至是不同机器——上的对象互相调用,而不用理会要调用的对象是否在同一进程或是同一机器中。
这种透明度是很有价值的,但是,尽管在分布式对象中有很多东西可以设计成对开发人员透明,“性能”通常并不属于其中之一。虽然我们的原型架构师是为了提高性能而使用分布式 – 而实际上他的设计方案往往要么会削弱性能,要么使得系统难于建立和配置,或者两者皆有。
远端接口和本地接口
基于类模型来进行分布效率不高的原因来自于一个基本事实:在进程内部进行过程调用是非常非常快的;在两个不同进程之间进行过程调用则会以几个数量级的幅度减慢;而把这些进程放到不同的机器上的话,则又会减慢一到两个数量级 —— 这取决于你的网络的拓扑结构。
因此,你需要为将会被远程调用的对象设计一种特别的接口,这种接口有别于那种用来在同一进程中被本地调用的接口。
一个本地接口最好被设计成“精细接口”(fine-grained interface)。这样,如果我有一个“地址类”,那么一个好的接口应该有读取城市,读取省份,设置城市,设置省份等等的独立方法。一个“精细接口”的好处在于它遵循了常规的OO原则:大量的短小的方法,可以通过多种方式组合和重载,便于将来的扩展。
然而,当你设计远程接口时,“精细接口”并不能很好地工作。在调用对象方法变得非常慢的时候,你会希望在一次调用中取得(或设置)城市、省份、和邮政编码,而不是三次。这样就产生了“粗糙接口”(coarse-grained interface),它是为了使调用次数最小化而设计出来的,而不是为了良好的弹性和扩展性等其他东西。在“粗糙接口”中,你会看到类似“取地址信息”或“设置地址信息”这样的接口。这个“粗糙接口”在编程角度来看显得非常笨拙,但因为性能的原因,你必须这样做。
当然,中间件提供商告诉你的是:使用他们的中间件进行远程或本地调用并不会有额外的开支。如果这是一个本地调用,那么它会以本地调用的速度完成。如果这是一个远程调用,它就会以较慢的速度完成。这样,当你需要进行远程调用时,你仅仅需要为这个远程调用付出一定代价。在一定程度上,这种说法是正确的。但并不能回避任何会被远程调用的对象应该有一个“粗糙接口”而任何被本地调用的对象应该有一个“精细接口”。当两个对象进行通信得时候,你必须选择使用哪种接口。如果这个对象有可能被放到另外得进程中去,那么你就必须使用“粗糙接口”并承担这种难于编程的模型所带来的代价。很明显,仅仅在你的确需要的时候承担代价才是有意义的 —— 所以你需要把进程间的协作最小化。
因此,你并不能仅仅把一组你基于单一进程设计出来的类通过CORBA或其他类似的东西直接变为分布式模型。分布式设计并不仅仅是这样做。如果你基于类来设计你的分布式策略 —— 你的系统会做大量的远程调用,并因此需要笨拙的“粗糙接口”。最后,即使你在每一个远程对象上都使用了“粗糙接口”,你的系统中的远程调用仍然会太多,也因此变得难以修改。
所以我们得出Fowler分布式对象设计第一定律:不要做分布式对象!
如果这样,你怎样才能有效地利用多个处理器呢?在多数情况下,你可以使用聚类(clustering)。把所有类放到单一的进程中然后在不同的节点中运行这个进程的多个复制。这样一来,每个进程都使用本地调用来完成工作,从而可以更快地完成。你还能在进程中的所有对象上应用“精细接口”,从而获得一个简单的编程模型所带来的良好的可维护性。
(缺图)
图2:把同一应用系统的多个复制放在不同节点上的clustering
你必须使用分布式的地方
这样,你会希望把你进行分布式的范围最小化,而尽可能通过聚类来使用你的处理器节点。问题是这种方法存在着一定的限制。有些地方你必须分离开你的进程。如果你足够敏感,你就应该象困兽那样拼命战斗,但你只能尽可能地消灭它们,而永远也不可能把它们杀光。
一个明显的分离是在商业软件的传统客户端与服务器之间。用户们桌面上的PC是共享数据的不同节点。因为它们是不同的机器,所以你需要能互相通信的分离的进程。客户端/服务器分离是一个典型的进程间(inter-process)分离
第二个分离通常发生在基于服务器的应用软件(应用程序服务器)与数据库之间。当然你不一定要这样做。你可以在数据库本身的进程中加入你的应用代码——例如使用储存过程之类的方法。但这种做法常常不那么实用,这样的话你就必须把进程分离开来。它们可以在同一台机器上运行,但一旦你把进程分开,你就立即要付出使用远程调用的代价。幸亏SQL本来就被设计为一个远程调用接口,因此你通常可以做适当安排来使代价最小化。
另一个进程分离可能是在web系统中,在web服务器与应用程序服务器之间。所有你要做的所有事情都是相同的,那么在单一进程中运行web服务器和应用程序服务器是最好不过的。但是,事情并不总是相同的。(译注:All things being equal it's best to run the web and appilcation server in a single process. But all things are not always equal. 我的理解是对于某个业务逻辑,如果你要为客户端使用不同的页面外观,那么就应该把web server与application server分开)
你可能遇到因为提供商的差异而必须分离的情况。如果你使用一个软件包,它通常需要在它自己的进程中运行。这样,你又需要使用分布式了,但至少一个设计良好的软件包会提供便于分布的“粗糙接口”
最后,也许会有一些实际情况令你不得不拆开你的应用程序服务器软件。你应该尽力去避免它,但事实上的确会出现这种情况。(这句理解得不好,原文是:You should sell any grand-parent you can get your hands on to avoid this, but casesdo
come up.) 如果这样,你只好捂着鼻子,狠心把你的软件分成多个远程的,使用“粗糙接口”的组件。
总的原则是——用Colleen Roe的令人难忘的话来说——对使用分布式对象应该表现得十分吝啬(译注:惜墨如金?)。Sell your favorite grandma first if you possibly can (译注:不知在说什么 )
处理分布边界(Distribution Boundary)
当你设计你的系统时,你需要尽可能限制你的分布边界。而当它们出现,你就需要把它们放在心上。每一个远程调用都象马车一样在计算机环境中行进。而系统中的每一个地方都要作出相应改变来最小化这些远程调用,这基本上就是使用这些马车的价钱了。
但是,你仍然可以在单一的进程中使用“精细接口”对象。关键在于在内部使用“精细接口”而把“粗糙接口”对象放在分布边界上,它们的作用是为“精细接口”提供远程接口。这些“粗糙接口”其实并不做任何实际的事情,它们只是作为这些“精细接口”的一个facade(译注:参考设计模式 facade),一个专为分布而存在的facade,用专门术语来说叫Remote Facade
使用Remote Facade有助于把“粗糙接口”引入的困难最小化。用这种方法,只有真正需要远程服务的对象才需要“粗糙接口”方法,而且对开发人员来说,显然他们需要接受这个代价。透明化有它的优点,但你并不会希望一个潜在的远程调用对你透明。(译注:透明指对开发人员隐藏这个远程调用)
通过把“粗糙接口”保持在纯粹的facade层面,你允许人们在清楚自己的程序在单一进程里运行时,可以使用“精细接口”对象。这使得整个分布式规则更加清晰。
与Remote Facade手牵手出现的是Data Transfer Object。你不仅仅需要“粗糙接口”方法,你还需要传递“粗糙接口”对象。例如当你需要一个地址信息时,你要把这个信息传递到另一个节点。通常你不能把业务对象本身传递过去,因为这个业务对象已经被嵌入到一个使用“精细接口”的本地对象引用网络中了。(译注:意思是这个业务对象中包含了对其他对象的引用,仅把这个对象传递过去是没有意义的)所以你只好把所有客户端需要的数据绑定到一个专作传递用的特定对象中,因此它被称为Data Transfer Object。(许多在enterprise Java社区的人使用术语value object,但这造成了与Value Object的另一个意义的命名冲突)Data Transfer Object同时出现在连接的两端,因此有一点非常重要:它不能包括任何不通过连接共享的东西。(译注:也就是只能包括通过连接共享的东西)也就是说,一个Data Transfer Object通常只包括对其它Data Transfer Object的引用,或者一些基础的对象——例如字符串。
另一条通往分布式之路是使用一个在不同进程间迁移对象的中间人(broker)。这个方法的要点是使用一个Lazy Load方案——通常这是用来对数据库实现“惰性读入”的,但这里你把这个概念用于在连接中迁移对象。这个方法的难点在于你要保证不会因此弄出一大堆远程调用来。我从未亲眼见过任何人尝试在应用系统中使用这一方法,但一些O-R Mapping工具(例如TOPLink)提供了相关的工具,而且我曾听过关于这个方法的一些不错的报告。
--------------------------------------------------------------------------------
当对象被创建不久,有时甚至是从对象被创建开始,人们就倾向于用分布式的策略来分开它们。但是很多人意识不到,对象的分布(实际上,任何其他东西的分布处理都一样)存在着很多缺陷 —— 特别是在开发商们那些令人舒畅的推销小册子的感染下,这种情况很容易发生。这一章将讲述这些教训 —— 我曾经亲眼看到我的客户用很大的代价来换取这些教训。
分布式对象的吸引之处
让我们看看这样一个故事:这是一个例行的设计总结会议(译注:作者是一个软件架构顾问),这种会议每年都要有两到三次。会上,一个参与新的OO系统开发的系统架构师很自豪地向人们展示一个新的分布式系统。让我们假定,这是一个涉及到customers(顾客),orders(订单),products(产品),和deliveries(货品分发)的系统。他向我展示了一个类似图1的设计。设计方案把customers, orders, products, 和deliveries都作为分离的remote object(远端对象)。每一部分都是一个单独的部件(component),能放在单独的processing node(处理节点)上运行。
(缺图)
图1:一个通过在不同节点运行不同组件实现分布式应用系统的例子
于是我发问:“你为什么要这样做?”
“当然是为了执行效率了!”架构师回答说,并用有点古怪的眼神看着我,“对于每一个部件,我们都可以把它放在一个独立的机器上运行。如果某个部件需要处理的东西太多,我们可以加入更多的机器来处理那个部件 —— 这样我们就有能力来调节整个应用系统的平衡。”说到这里,他的眼神仿佛在说,“你究竟明不明白什么是真正的分布式对象呀?”
这个时候,我面临着一个有趣的两难局面:我究竟应该直接告诉他,他的设计令我感到非常的厌恶,然后任由他把我赶出门外;还是应该尝试慢慢地让他明白过来。当然,第二种做法对我比较有利,但是却需要费上很多的时间。因为我的客户们通常对自己建立的架构非常满意,要他们放弃这样一个美梦是很困难的。
好,假定到现在为止你还没有把我拒之门外的话,我想你会感到好奇:为什么这个分布架构令人不快?毕竟许多工具提供商会告诉你分布式对象的好处就在于你可以把一系列对象根据你的意愿分配到进程节点(processing nodes)中去,而他们强大的中间件则为你提供透明的操作。这种透明操作允许在同一个进程或不同进程——甚至是不同机器——上的对象互相调用,而不用理会要调用的对象是否在同一进程或是同一机器中。
这种透明度是很有价值的,但是,尽管在分布式对象中有很多东西可以设计成对开发人员透明,“性能”通常并不属于其中之一。虽然我们的原型架构师是为了提高性能而使用分布式 – 而实际上他的设计方案往往要么会削弱性能,要么使得系统难于建立和配置,或者两者皆有。
远端接口和本地接口
基于类模型来进行分布效率不高的原因来自于一个基本事实:在进程内部进行过程调用是非常非常快的;在两个不同进程之间进行过程调用则会以几个数量级的幅度减慢;而把这些进程放到不同的机器上的话,则又会减慢一到两个数量级 —— 这取决于你的网络的拓扑结构。
因此,你需要为将会被远程调用的对象设计一种特别的接口,这种接口有别于那种用来在同一进程中被本地调用的接口。
一个本地接口最好被设计成“精细接口”(fine-grained interface)。这样,如果我有一个“地址类”,那么一个好的接口应该有读取城市,读取省份,设置城市,设置省份等等的独立方法。一个“精细接口”的好处在于它遵循了常规的OO原则:大量的短小的方法,可以通过多种方式组合和重载,便于将来的扩展。
然而,当你设计远程接口时,“精细接口”并不能很好地工作。在调用对象方法变得非常慢的时候,你会希望在一次调用中取得(或设置)城市、省份、和邮政编码,而不是三次。这样就产生了“粗糙接口”(coarse-grained interface),它是为了使调用次数最小化而设计出来的,而不是为了良好的弹性和扩展性等其他东西。在“粗糙接口”中,你会看到类似“取地址信息”或“设置地址信息”这样的接口。这个“粗糙接口”在编程角度来看显得非常笨拙,但因为性能的原因,你必须这样做。
当然,中间件提供商告诉你的是:使用他们的中间件进行远程或本地调用并不会有额外的开支。如果这是一个本地调用,那么它会以本地调用的速度完成。如果这是一个远程调用,它就会以较慢的速度完成。这样,当你需要进行远程调用时,你仅仅需要为这个远程调用付出一定代价。在一定程度上,这种说法是正确的。但并不能回避任何会被远程调用的对象应该有一个“粗糙接口”而任何被本地调用的对象应该有一个“精细接口”。当两个对象进行通信得时候,你必须选择使用哪种接口。如果这个对象有可能被放到另外得进程中去,那么你就必须使用“粗糙接口”并承担这种难于编程的模型所带来的代价。很明显,仅仅在你的确需要的时候承担代价才是有意义的 —— 所以你需要把进程间的协作最小化。
因此,你并不能仅仅把一组你基于单一进程设计出来的类通过CORBA或其他类似的东西直接变为分布式模型。分布式设计并不仅仅是这样做。如果你基于类来设计你的分布式策略 —— 你的系统会做大量的远程调用,并因此需要笨拙的“粗糙接口”。最后,即使你在每一个远程对象上都使用了“粗糙接口”,你的系统中的远程调用仍然会太多,也因此变得难以修改。
所以我们得出Fowler分布式对象设计第一定律:不要做分布式对象!
如果这样,你怎样才能有效地利用多个处理器呢?在多数情况下,你可以使用聚类(clustering)。把所有类放到单一的进程中然后在不同的节点中运行这个进程的多个复制。这样一来,每个进程都使用本地调用来完成工作,从而可以更快地完成。你还能在进程中的所有对象上应用“精细接口”,从而获得一个简单的编程模型所带来的良好的可维护性。
(缺图)
图2:把同一应用系统的多个复制放在不同节点上的clustering
你必须使用分布式的地方
这样,你会希望把你进行分布式的范围最小化,而尽可能通过聚类来使用你的处理器节点。问题是这种方法存在着一定的限制。有些地方你必须分离开你的进程。如果你足够敏感,你就应该象困兽那样拼命战斗,但你只能尽可能地消灭它们,而永远也不可能把它们杀光。
一个明显的分离是在商业软件的传统客户端与服务器之间。用户们桌面上的PC是共享数据的不同节点。因为它们是不同的机器,所以你需要能互相通信的分离的进程。客户端/服务器分离是一个典型的进程间(inter-process)分离
第二个分离通常发生在基于服务器的应用软件(应用程序服务器)与数据库之间。当然你不一定要这样做。你可以在数据库本身的进程中加入你的应用代码——例如使用储存过程之类的方法。但这种做法常常不那么实用,这样的话你就必须把进程分离开来。它们可以在同一台机器上运行,但一旦你把进程分开,你就立即要付出使用远程调用的代价。幸亏SQL本来就被设计为一个远程调用接口,因此你通常可以做适当安排来使代价最小化。
另一个进程分离可能是在web系统中,在web服务器与应用程序服务器之间。所有你要做的所有事情都是相同的,那么在单一进程中运行web服务器和应用程序服务器是最好不过的。但是,事情并不总是相同的。(译注:All things being equal it's best to run the web and appilcation server in a single process. But all things are not always equal. 我的理解是对于某个业务逻辑,如果你要为客户端使用不同的页面外观,那么就应该把web server与application server分开)
你可能遇到因为提供商的差异而必须分离的情况。如果你使用一个软件包,它通常需要在它自己的进程中运行。这样,你又需要使用分布式了,但至少一个设计良好的软件包会提供便于分布的“粗糙接口”
最后,也许会有一些实际情况令你不得不拆开你的应用程序服务器软件。你应该尽力去避免它,但事实上的确会出现这种情况。(这句理解得不好,原文是:You should sell any grand-parent you can get your hands on to avoid this, but casesdo
come up.) 如果这样,你只好捂着鼻子,狠心把你的软件分成多个远程的,使用“粗糙接口”的组件。
总的原则是——用Colleen Roe的令人难忘的话来说——对使用分布式对象应该表现得十分吝啬(译注:惜墨如金?)。Sell your favorite grandma first if you possibly can (译注:不知在说什么 )
处理分布边界(Distribution Boundary)
当你设计你的系统时,你需要尽可能限制你的分布边界。而当它们出现,你就需要把它们放在心上。每一个远程调用都象马车一样在计算机环境中行进。而系统中的每一个地方都要作出相应改变来最小化这些远程调用,这基本上就是使用这些马车的价钱了。
但是,你仍然可以在单一的进程中使用“精细接口”对象。关键在于在内部使用“精细接口”而把“粗糙接口”对象放在分布边界上,它们的作用是为“精细接口”提供远程接口。这些“粗糙接口”其实并不做任何实际的事情,它们只是作为这些“精细接口”的一个facade(译注:参考设计模式 facade),一个专为分布而存在的facade,用专门术语来说叫Remote Facade
使用Remote Facade有助于把“粗糙接口”引入的困难最小化。用这种方法,只有真正需要远程服务的对象才需要“粗糙接口”方法,而且对开发人员来说,显然他们需要接受这个代价。透明化有它的优点,但你并不会希望一个潜在的远程调用对你透明。(译注:透明指对开发人员隐藏这个远程调用)
通过把“粗糙接口”保持在纯粹的facade层面,你允许人们在清楚自己的程序在单一进程里运行时,可以使用“精细接口”对象。这使得整个分布式规则更加清晰。
与Remote Facade手牵手出现的是Data Transfer Object。你不仅仅需要“粗糙接口”方法,你还需要传递“粗糙接口”对象。例如当你需要一个地址信息时,你要把这个信息传递到另一个节点。通常你不能把业务对象本身传递过去,因为这个业务对象已经被嵌入到一个使用“精细接口”的本地对象引用网络中了。(译注:意思是这个业务对象中包含了对其他对象的引用,仅把这个对象传递过去是没有意义的)所以你只好把所有客户端需要的数据绑定到一个专作传递用的特定对象中,因此它被称为Data Transfer Object。(许多在enterprise Java社区的人使用术语value object,但这造成了与Value Object的另一个意义的命名冲突)Data Transfer Object同时出现在连接的两端,因此有一点非常重要:它不能包括任何不通过连接共享的东西。(译注:也就是只能包括通过连接共享的东西)也就是说,一个Data Transfer Object通常只包括对其它Data Transfer Object的引用,或者一些基础的对象——例如字符串。
另一条通往分布式之路是使用一个在不同进程间迁移对象的中间人(broker)。这个方法的要点是使用一个Lazy Load方案——通常这是用来对数据库实现“惰性读入”的,但这里你把这个概念用于在连接中迁移对象。这个方法的难点在于你要保证不会因此弄出一大堆远程调用来。我从未亲眼见过任何人尝试在应用系统中使用这一方法,但一些O-R Mapping工具(例如TOPLink)提供了相关的工具,而且我曾听过关于这个方法的一些不错的报告。