原文地址:http://blog.csdn.net/leminsky/article/details/5074596
导言
我们都知道分布式计算的理论知识:通过在多个计算机上分布任务、而不是由一个中央计算机发起所有的进程,我们就能提高整体的吞吐量。问题是,在现实中真正实现这种设计是非常复杂的。
像EJB这样的技术应该能使其简单一些,但是它们已经被证明对设计和开发过程具有极度的侵入性。幸运的是,目前出现并融入主流的JVM级别集群技术,比如Terracotta,提供了一个可行的替代方法。
最近Shine Technologies发布了使用Terracotta的一个应用,显著提高了性能。过去,我们应用的性能往往受限于其运行的服务器。使用Terracotta的话,这似乎不再是问题。在我们的性能测试过程中,当一台服务器能力不足时,我们只要增加另一台服务器,应用的整体吞吐量就会显著增加——直到数据库成为主要限制因素。
一些背景
Shine Technologies是一家为许多澳大利亚能源产业公司服务的IT咨询和开发服务提供商。随着全面零售竞争(Full Retail Contestability,FRC)规定的实施,Shine已经为这些公司开发出许多产品。
简要地说,这些产品有利于促进批发商和零售商之间关于网络使用情况的财务交互。这些交互是大批量的,大型零售商不得不处理每月数百万笔的交易。因此,应用的伸缩能力变成了业务的关键——尤其是所有交易都在同一天涌向零售商的情况下。
这 一系列产品最近添加的部分已形成一个应用,电力零售商的服务提供商使用该应用。这个应用——也就是大家熟知的市场核对系统(Market Reconciliation System,MRS)——每周都接受大量的用电数据,还为零售商提供报告和核对机制,高亮显示他们期望的使用收费和实际收费之间的任何差异。
鉴于该产品的大数据量特性,伸缩性在应用架构中是一个很大的驱动因素。在进行了涉及各种分布式计算框架的概念验证之后,Terracotta被认为是最有可能满足MRS需求的框架。
Terracotta和主/从模式
Terracotta能把应用部署在多个JVM上,但仍然能彼此交互,就像它们运行在同一个JVM上。用户透明地指定JVM和Terracotta之间共享的对象,使它们都能获取这些对象。Terracotta为集群提供了预配置解决方案,特别是Java企业环境——例如Tomcat、JBoss或Spring。但是,在我们的情况中我们不使用容器,所以需要我们自己配置Terracotta。
幸好我们找到一种办法:主/从设计模式,它既符合我们的应用,也已被证明能很好地与Terracotta一起工作。Jonas Bonèr在他的精彩博文《如何使用Open Terracotta建立基于POJO的数据网格》中首次描述了主/从设计模式与Terracotta结合的用法。
对那些不熟悉主/从模式的人来说,“Master”负责确定需要完成工作的每一项,以及每一项在共享队列上的位置。接着它继续监控每一项的完成状态,当所有项都完成时也就完成了。“Workers”则从队列中逐一获取工作项并处理它们,当工作完成时设定项的完成状态。
在我们的实现中,Master和Workers分别运行在它们各自的JVM上。这些JVM分布在大量的机器上。任何共享对象(比如工作队列和工作项状态)都由Terracotta服务器管理(并有效地同步)。
有很多有用的方法来形象表示该架构。首先是一个纯物理图,显示机器、JVM、Master/Workers怎样在网络上彼此关联。
注意在一台机器上可能会有多个JVM。如果一个JVM堆已达到其允许最大值,但机器上还有更多物理内存、另一个JVM可以使用,你可以这样做。
那么,Terracotta在物理架构中放在哪里呢?它监视所有的JVM——那么,用第二种更合乎逻辑的可视化图也许能最好地阐明:
我们看到Terracotta装备了JVM,使这些JVM能越过物理网络透明地共享工作项队列。它甚至可以添加另外的Masters,使它们也共享该队列。
学习Terracotta:一个例子
虽 然Terracotta不要求你明确地开发分布式代码,但从长远来看,如果master和workers之间共享的数据能降至最少,Terracotta会有利于整体性能。workers越是能自主执行它们的操作,Terracotta服务器跨越多个JVM管理的东西就越少。
用这个去开发解决方案会让我们遇到一些麻烦和错误。为了有助于解释遇到的一些障碍,我们来举一个简化的例子。
我 们的应用处理多个文件,这些文件包含由逗号分隔值(comma-separated values,CSV)组成的记录。此外,每一条CSV记录包含一个叫日最大使用量(Maximum Daily Usage,MDU)的属性。为简单起见,我们只说我们的任务是报告每个文件的MDU最大值,当然还有其它东西(实际的处理远比这个要复杂)。
按照Jonas的博文,我们起初创建了实现Work接口的WorkUnit,Work接口由CommonJ WorkManager规范定义。WorkUnit负责处理指定的文件,找出MDU最大值。代码如下:
public class WorkUnit implements Work { private String filePath; private Reader reader; public WorkUnit(String aFilePath) { filePath = aFilePath; } public void run() { private String maxMDU = ""; setReader(new BufferedReader(new FileReader(filePath))); String record = reader.readLine(); while (record != null) { String[] fields = record.split(","); String mdu = fields[staging:4]; if (mdu.compareTo(maxMDU) > 0) { setMaxMDU(mdu); } record = reader.readLine(); } System.out.println("maximum MDU = " + maxMDU); doStuffWithReader(); doMoreStuffWithReader(); doEvenMoreStuffWithReader(); } private void setReader(Reader reader) { this.reader = reader; } ... }
对 于指定目录下的每个文件,Master都创建一个WorkUnit,向它提供要处理的文件的位置。接着WorkUnit封装在WorkItem里 面——WorkItem包含一个状态标志——并将其放在共享队列中被调度。然后我们的Worker实现获取WorkItem、得到WorkUnit,并调 用它的run()方法,run()方法反过来处理文件并报告MDU最大值。接着它会继续处理文件的其它内容。
需要注意的关键一点是,我们选择把Reader做为一个实例变量来存储——它实际上被很多方法用到,使用局部变量并到处传递也是不切实际的。
为了在workers之间共享队列,我们像下面所示的那样来配置Terracotta的tc-config.xml文件:
<application> <dso> <roots> <root> <field-name> com.shinetech.mrs.batch.core.queue.SingleWorkQueue.m_workQueue </field-name> </root> </roots> <locks> <autolock> <method-expression>* *..*.*(..)</method-expression> <lock-level>write</lock-level> </autolock> </locks> <instrumented-classes> <include> <class-expression> com.shinetech.mrs.batch.core.queue..* </class-expression></include> <include> <class-expression> com.shinetech.mrs.batch.input.workunit..* </class-expression> </include> </instrumented-classes> </dso> </application>
你不必明白这个文件的太多细节,除了知道它指定了共享的属性——这里就是我们的队列——并指定了应该装配哪个类来安全地共享它。
我 们第一次使用WorkUnit运行时失败了——当WorkUnit去给reader变量赋值时,Terracotta抛出了UnlockedSharedObjectException异常,异常信息是“Attempt to access a shared object outside the scope of a shared lock”。实质上Terracotta告诉我们,我们在更新由Master和Worker共享的对象的一个属性,但是我们并没告诉Terracotta这个属性需要加锁(或同步)。
关键问题是,尽管在运行worker之前我们都不实例化该实例变量,但是Terracotta认为它是由master和worker共享的,因为这 个变量属于共享队列上的一个对象。(顺便说一下,Terracotta的异常处理非常棒;如果你试图去做你不能做的事情,Terracotta异常就会告 诉你你做错了什么、以及做些什么去修复它)。
在这个阶段我们能采用几个不同的方法。其中一个方法是,在WorkUnit中添加一个同步的setReader()方法。Terracotta会根 据tc-config.xml中的autolock片段锁定对reader的访问。不改变源代码的选择是,我们可以在tc-config.xml文件的locks片段中添加一个named-lock,它反过来告诉Terracotta有效地跨越JVM集群对reader的访问进行同步。
<named-lock> <lock-name>WorkUnitSetterLock</lock-name> <method-expression> * com.shinetech.mrs.batch.dataholder..*.set*(..) </method-expression> <lock-level>write</lock-level> </named-lock>
然 而,这两个办法最终都不是我们所需要的。举一个更为现实和复杂的例子,WorkUnit可能有很多实例变量,它们实际上只在由Worker运行的 时候才是相关的。Master不需要了解它们的任何东西,它们实际上也只在run()方法运行期间存在。从性能角度来看,如果Master永不访问WorkUnit的属性,那么我们是不希望Terracotta对这些属性的访问进行同步的。
我们真正想要的是WorkUnit不由master实例化的能力,而是在Worker从队列中获取WorkItem的时候实例化WorkUnit。要做到这一点,我们引入了WorkUnitGenerator:
在这个阶段我们能采用几个不同的方法。其中一个方法是,在WorkUnit中添加一个同步的setReader()方法。Terracotta会根 据tc-config.xml中的autolock片段锁定对reader的访问。不改变源代码的选择是,我们可以在tc-config.xml文件的locks片段中添加一个named-lock,它反过来告诉Terracotta有效地跨越JVM集群对reader的访问进行同步。
<named-lock> <lock-name>WorkUnitSetterLock</lock-name> <method-expression> * com.shinetech.mrs.batch.dataholder..*.set*(..) </method-expression> <lock-level>write</lock-level> </named-lock>
然 而,这两个办法最终都不是我们所需要的。举一个更为现实和复杂的例子,WorkUnit可能有很多实例变量,它们实际上只在由Worker运行的 时候才是相关的。Master不需要了解它们的任何东西,它们实际上也只在run()方法运行期间存在。从性能角度来看,如果Master永不访问WorkUnit的属性,那么我们是不希望Terracotta对这些属性的访问进行同步的。
我们真正想要的是WorkUnit不由master实例化的能力,而是在Worker从队列中获取WorkItem的时候实例化WorkUnit。要做到这一点,我们引入了WorkUnitGenerator:
现 在,Master创建了WorkUnitGenerator,提供给它要处理文件的位置。WorkUnitGenerator封装在WorkItem里面,并被调度。Worker实现获取WorkItem、得到WorkUnitGenerator,并调用它的run()方法。run ()方法实例化一个新的WorkUnit对象,并委托WorkUnit的run()方法完成文件处理。所以,在我们的情况中WorkUnit应该独立于Master,现在的情形是WorkUnit实际上就是独立的,Terracotta也不需要跨越JVM做任何不必要的同步。
上 面概述的代码例子仅仅是实现主/从模式的一个方法。可能还有完成它的其它方法,随着积累更多的Terracotta经验,我们也可以找到更好的实 现。主要的一点是,尽管Terracotta和主/从模式有跨多台机器分布工作的能力,但是你必须注意你想共享的和你不想共享的东西。
性能结果
Terracotta的概念验证显示了很多希望,但它真的能交付吗?为此设计一个情景来测试应用的扩展性。89个数据文件被加载,总共包含872,998条记录。这些都是从一个真实的生产系统里面获得的,因此使我们有信心建立一个真实的数据集。
Terracotta服务器和单个主进程运行在一台机器上。不同数目的分布式worker机器用来处理数据,每台机器上运行4个worker。结果如下:
Worker Machines | Workers | Time (seconds) |
1 | 4 | 416 |
2 | 8 | 261 |
3 | 12 | 214 |
4 | 16 | 194 |
5 | 20 | 193 |
这些数据可图形展示,如下:
只在一台机器上运行4个worker,加载89个文件花费的总时间是416秒。在我们的分布式计算系统中只增加了一台worker机器,加载89个文件的时间就几乎减半。每增加一台新的worker机器,都会获得进一步的性能提升。
正如从上图中看到的一样,扩展性开始趋于稳定。随着添加更多的worker,数据库服务器受到了增加负荷的影响,并在一些点上最终可能成为瓶颈。
结论
通过扩展Joseph Boner对Terracotta和主/从模式的工作,我们能在我们的应用中建立一个分布式计算组件。
在这一阶段,我们的应用运行在一个客户的生产环境里面,客户的数据处理需求只需要使用单机。但是,使用从另一个生产环境获得的更大的数据集跨多台机器进行性能测试后,我们仍然确信在时机成熟的时候Terracotta能够满足应用的规模扩大的要求。
最终证明数据库是瓶颈,但鉴于我们得到的性能,还是可以忍受的。如果你运行的进程是只受到CPU能力限制的,那么伸缩性上的提高就不可估量了。
目前在我们的架构中,所有Masters(即创建被执行任务的进程)都与Terracotta一起运行在同一台机器上的,现在对我们来说,万一当前单个Master机器超载了,添加额外的Master机器来扩展Master负载还是一个繁琐的工作。
Terracotta已经在背后帮我们的应用做了许多脏活累活。我们正期待着在接下来的几个月里,看着它的性能随真实数据集的增加而增加,并看到我们其它的应用怎样从我们的经验中受益。
Terracotta已经在背后帮我们的应用做了许多脏活累活。我们正期待着在接下来的几个月里,看着它的性能随真实数据集的增加而增加,并看到我们其它的应用怎样从我们的经验中受益。