在J2EE中使用 Work Manager 规范执行并行任务

作者:Dmitri Maximovich 时间:2005-11-23 19:53 出处:bea

 

到目前为止,人们还没有发现一种途径可以方便地在 J2EE 应用程序中启动执行并行任务。设想您的应用程序需要处理多个客户端请求(不管是在 servlet 中还是在 session bean 中),而如果这些请求以并行的方式执行,其效率可能更高。作为一个简单例子,我们假设这样一种情况,即客户请求可以通过从多个 URL、web服务或 SQL 查询语句获得数据而得到处理,并且执行顺序不重要。这些用例非常需要以并行的方式执行;它们花费绝大部分时间来等待响应,因此它们占用的处理能力并不多。除此之外,因为有很多已经实现了,因此强健的并发设计是可扩展软件的重要组成部分。

  本文研究的是在 J2EE 中使用 Work Manager 规范来执行并行任务。我们创建了一个可实行的例子,来说明如何使用 Work Manager 规范在 servlet 容器中实现任务的并行执行。

J2EE 中的并发

  为了在J2EE 容器中实现并行任务的执行,我们有哪些选择?奇怪的是,方法并不多。您可能也知道,J2EE 规范禁止在运行在 J2EE 容器中的受控代码中创建线程。(在实践中,这个规则很少得以贯彻执行,甚至还有些组件因为破坏此规则而被人们熟知,例如Quartz sheduler。)然而,J2EE 规范根本没有讨论并行执行,因此,需要实现并行执行的开发人员只有依靠他们自己。

  在实践中,一般来说,如果要与 J2EE 规范兼容,您只有一个可用的选择:使用队列将同步调用拆分成多个异步任务。这种方法可以解决问题,但是实现起来相当复杂;它要求定义目标、创建消息并编写MDB代码。更糟的是,如果您承担不起丢失那些可能需要访问另外的资源管理器的消息,还必须使用持久队列。(一个例子是,代码在 WebLogic 平台上运行,但却需要使用 IBM MQ 来排队。从这里开始,这是到 XA 事务的一个步骤。)

  另外一个困难随之产生,即在继续执行主程序流之前,您需要等待一组异步子任务的完成。您不久就会看到,使用新的 Work Manager 框架能方便地实现并行执行。

适合应用程序服务器规范的 Work Manager

  所有这些复杂性似乎很快就会成为过去,因为 J2EE 服务器市场的两大供应商IBM 和 BEA 正在联合研发一种规范,该规范为任务的并发执行提供简单的、容器可管理的编程模型。该行动被称为CommonJ,其中一个部分是Work Manager for Application Servers规范,即现在可得到的JSR 237。该规范在2003年首次公布,而IBM 的 WebSphere Application Server 自企业版5.0就开始支持任务的并行执行,该版本是编程模型扩展(Programming Model Extensions,PME)的一部分。在 WebSphere 文档中,该功能有时被称为“异步 bean”。在 WebSphere 6.0中,所有的版本都会支持并发执行。Bea Weblogic Server 在BEA WebLogic Server 9.0中提供对应的功能。有了 BEA 和 IBM 的支持,该规范在不久的将来极有可能至少成为事实上的标准。

使用 Work Manager 功能

  让我们来看看如何开始使用这个新功能。下面这个例子是在 WebLogic Server 9.0 beta 版本上测试的。为了简单起见,我们在这个例子中使用一个 servlet 作为执行起点,但是如果您的应用程序入口点是一个会话 bean 或者消息驱动bean,其逻辑也是适用的。

  我们假设一个虚构的用例,即一个 servlet 接收了一个单词列表,需要将这些单词从一种语言翻译成另一种语言(例如,通过使用 Google 的语言工具或某些类似的远程服务)。这些单词可能来自于 HTML 页面中的多重选择列表。servlet 代码在向客户端返回结果之前,必须翻译所有单词。我们不会为了此练习而编写代码来实现真正的翻译,我们真正要做的是使用具有可配置延迟的哑翻译器,来模拟对翻译服务的远程调用。

Translator 接口及其实现

  让我们来为Translator定义接口:

public interface Translator {
public String getSource();
public void translate();
public String getTranslation();
}

  您会看到,Translator 被设计为“有状态”(stateful)。假定您的代码需要创建Translator 实例,将单词作为构造器调用的参数传递从而翻译,然后调用translate()方法,最终通过调用 getTranslation()方法获得翻译后的单词。辨证地说,对于翻译任务来说,这种设计并非是最佳的。而且在这个例子中,很可能只需要使用一个单独的字符串 translate(字符串) 方法就足以应付了。但是,您很快就会看到,当以并行方式执行多个翻译时,它会和Work接口结合得很好。对我们的例子来说,我们将使用一个非常简单的DummyTranslator 实现。但是首先我们要定义AbstractTranslator来封装在各种实现中常见的域和方法。

public abstract class AbstractTranslator implements Translator {
protected final String source;
protected String translation;
public AbstractTranslator(String source) {
this.source = source;
}
public String getSource() {
return this.source;
}
public String getTranslation() {
return this.translation;
}
public String toString() {
return "source="+getSource()+", translation="+getTranslation();
}
}

  现在,任何的具体实现只需要执行translate()方法:

public class DummyTranslator extends AbstractTranslator {
private final long delay;
public DummyTranslator(String source, long delay) {
super(source);
this.delay = delay;
}
public void translate() {
// delay to simulate network call
try {
Thread.sleep(this.delay);
}
catch (InterruptedException ignore) { }
this.translation = this.source+"_tr";
}
}

  现在我们已经准备好实现我们的 servlet 了,首先我们针对惯用的串行执行,然后再针对并行任务执行。

串行实现

  为了简单起见,我们首先创建一个AbstractServlet,它封装所有有关HttpServlet 的代码,并且将真正的翻译工作委托给子类(策略模式)。

public abstract class AbstractServlet extends HttpServlet {
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws IOException, ServletException  {
logger.info("begin");
response.setContentType("text/plain");
PrintWriter writer = resp.getWriter();
long start = System.currentTimeMillis();
List input = getClientInput();
List result = null;
try {
result = doTranslation(input);
}
catch (Exception e) {
throw new ServletException(e);
}
long stop = System.currentTimeMillis();
logger.info("done in "+(stop-start));
writer.print("done in "+(stop-start)+"/n"+result);
}
// actual translation logic goes here
protected abstract List doTranslation(List input) throws Exception;
// let's hardcode list for our test
// in real life this method should extract parameters from HttpServletRequest
private List getClientInput() {
return Arrays.asList(new String[]{"one", "two", "three", "four", "five"});
}
}

  最后,这就是串行执行的代码:

public class ServletSequential extends AbstractServlet {
private static final long DELAY = 10 * 1000; // 10 sec
protected List doTranslation(List input) {
List result = new ArrayList();
for (Iterator iter = input.iterator(); iter.hasNext();) {
String source = (String) iter.next();
Translator translator = new DummyTranslator(source, DELAY);
translator.translate();
result.add(translator.getTranslation());
}
return result;
}
}

  当这个 servlet 执行时,它会花费50秒来“翻译”我们将其作为一个参数的5个单词(每次调用DummyTranslator.translate() 花费10秒钟)。让我们来研究一下如何重写这个例子,以利用WorkManager的功能。

修改代码以并行执行

  要修改代码以利用并行执行,第一步是修改Translator来实现Work接口。为此有两种方式:可以修改AbstractTranslator 来实现 Work,以补充 Translator 接口;或者可以提供某种包装类(wrapper class),它们可以使用 Translator 实例来实现Work,即代理方法调用。我们将采用第二种方法,它允许我们无需修改就可以重用已有的DummyTranslator。

public class WorkTranslatorWrapper implements Translator, Work {
private final Translator impl;
public WorkTranslatorWrapper(Translator impl) {
this.impl = impl;
}
public String getSource() {
return this.impl.getSource();
}
public String getTranslation() {
return this.impl.getTranslation();
}
public void translate() {
this.impl.translate();
}
public void release() {
}
public boolean isDaemon() {
return false;
}
public void run() {
translate();
}
public String toString() {
return this.impl.toString();
}

  正如您看到的,WorkTranslatorWrapper为定义在 Translator 接口中的所有方法提供了实现,它作为一个构造器参数而传递,为到达具体的Translator 实现代为代理这些方法。为了满足 Work 接口的需要,我们定义了三个新的方法(Work 接口反过来继承了Runnable)。run()方法是 Work 执行的入口点;在本例中我们只是将它重定向到translate() 方法。release()方法可以用来设置任何变量来终止 run()方法中的主循环(如果有的话),然后几乎以与Java 线程中推荐的完全相同的方式返回。如果 Work 能够比调度它的 servlet 请求或者 EJB 方法坚持得更久,isDaemon() 方法应该返回true。如果返回了 false,Work持续时间通常不能长于提交容器方法的时间。如果使用的资源在方法的持续期里才有效,那么该提交方法(submitting method)应该一直等到暂活的(short-lived)(non-daemon)工作完成。对我们的例子来说,采用non-daemon是正确的方法,因为我们要等到所有的结果都可用才向客户端返回结果。

  既然实现了 Work,我们就可以重写 servlet 代码来使用WorkManager了,但首先我们需要在容器中定义WorkManager。

在容器中定义 WorkManager

  Work Manager 其实是容器资源,就像 JMS 队列和连结池。您可以使用Administration Console来配置这些组件。其实, WorkManager 就是一个带有容量参数的线程池,其中有可分配的最大线程数目和最小线程数目、响应时间策略等等。不过,所有这些选项在 beta 版本中都是无法通过控制台来修改的,因此,所有的配置归结为为新建的 WorkManager 提供一个名称。在本例中,我们使用MyWorkManager。


图1. 在 WebLogic 9.0上配置WorkManager

  当完成配置后,重新启动 WebLogic Sever,启动时,您就会发现(要么在控制台要么在日志文件里面,这依赖于您的日志设置)已经为应用程序创建了 MyWorkManager 。

<Mar 31, 2005 5:42:47 PM EST> <WorkManager> <Creating WorkManager from "MyWorkManager" WorkManagerMBean for application "workman.war">

  现在,刚刚定义的MyWorkManager就存在于 JNDI 树中了。该容器可以支持任意数量的独立WorkManager实例。获得一个WorkManager实例的主要方法是在本地 Java 环境中使用一个 JNDI 搜索(即:java:comp/env/wm/[work manager名称])。因此,Work Manager 是在部署时配置的,其方法是使用部署描述符作为 resource-ref。对特定 Work Manager(例如,wm/MyWorkManager)的每次 JNDI 搜索均返回该WorkManager的一个共享实例。因为WorkManager是线程安全(thread-safe)的,所以查询就可以一次性完成,通常在 init()方法(servlet)或ejbCreate()方法(session EJB)中实现。

  推荐的配置方法是在相应的标准部署描述符web.xml (对于EJB,是ejb-jar.xml)中定义resource-ref:

 <web-app>
...
<resource-ref>
<res-ref-name>wm/MyWorkManager</res-ref-name>
<res-type>commonj.work.WorkManager</res-type>
<res-auth>Container</res-auth>
<res-sharing-scope>Shareable</res-sharing-scope>
</resource-ref>
...
</web-app>

  同样,也在特定于 WebLogic 的部署描述符weblogic.xml (或weblogic-ejb-jar.xml)中定义resource-ref。

  <weblogic-web-app>
...
<reference-descriptor>
<resource-description>
<res-ref-name>wm/MyWorkManager</res-ref-name>
<jndi-name>MyWorkManager</jndi-name>
</resource-description>
</reference-descriptor>
...
</weblogic-web-app>

修改 servlet 来使用并行执行

  下面就是ServletParallel 类的代码,它继承了 AbstractServlet 类。请注意对WorkManager的搜索在 init() 方法中是如何实现的。代码的大体结构与ServletSequential 非常相似:doTranslation()方法包含了与输入列表相同的循环,但是不是直接执行 Translator,而是创建了一个 WorkTranslatorWrapper实例,然后通过该实例调用WorkManager 的schedule() 方法。调用的schedule()方法立即返回,我们需要保存作为结果的 WorkItem(通过将它添加到任务列表中)。在所有的任务都安排好后,执行会在调用WorkManager.waitForAll(Collection, long)时阻塞,该方法使用 WorkItem集合用于我们要等待的任务。第二个参数指定以毫秒为单位的超时,我们有两个预定义的常量: WorkManager.IMMEDIATE,它用来指定方法应该立即返回(它与传递“0”是同样的效果); WorkManager.INDEFINITE,它表明没有超时一直要等到所有任务都完成。

public class ServletParallel extends AbstractServlet {
private WorkManager workManager;
public void init(ServletConfig servletConfig) throws ServletException {
try {
InitialContext ctx = new InitialContext();
this.workManager = (WorkManager)ctx.lookup("java:comp/env/wm/MyWorkManager");
}
catch (Exception e) {
throw new ServletException(e);
}
}
protected List doTranslation(List input) throws Exception {
List result = new ArrayList();
List jobs = new ArrayList();
// create translators and schedule execution
for (Iterator iter = input.iterator(); iter.hasNext();) {
String source = (String) iter.next();
Translator translator = new DummyTranslator(source, 10 * 1000);
// schedule
Work work = new WorkTranslatorWrapper(translator);
WorkItem workItem = this.workManager.schedule(work);
jobs.add(workItem);
}
logger.info("All jobs scheduled");
// wait for all jobs to complete
this.workManager.waitForAll(jobs, WorkManager.INDEFINITE);
// extract results
for (Iterator iter = jobs.iterator(); iter.hasNext();) {
WorkItem workItem = (WorkItem) iter.next();
Translator translator = (Translator) workItem.getResult();
result.add(translator.getTranslation());
}
return result;
}
}

  请注意当任务完成时结果是如何抽取的:我们在WorkItem 集合上循环调用 getResult()方法,该方法返回相应任务的实例。在本例中,这些可以抛给Translator。

  该 servlet 如果用和ServletSequential 相同的输入参数执行,那么就会以明显快的速度完成执行(回想一下:在顺序执行的情况下,耗时50秒)。在我的计算机上,执行大约耗时25秒到30秒。但是,结果肯定会随所使用的特定WorkManager的配置情况、服务器负载以及其他因素而有所不同。WebLogic Server 9同样也优化了线程的使用,并在 Work Manager 之间共享它们。此外,它确保请求得到公平处理。

Work 的生命周期和生命周期事件

  现在,让我们更加仔细地研究 Work Manager 规范提供的功能。每个 Work 实例都有一个明确定义的生命周期。它定义了如下的状态:

  • ACCEPTED接受——定义为WorkEvent.WORK_ACCEPTED的常量,表明Work已被接受,可以调度了。
  • REJECTED拒绝——定义为WorkEvent.WORK_REJECTED的常量,表明已接受的Work无法启动(很可能是因为WorkManager或应用服务器本身的问题)
  • STARTED开始——定义为WorkEvent.WORK_STARTED的常量,表明Work已经开始执行。
  • COMPLETED完成——定义为WorkEvent.WORK_COMPLETED的常量,表明Work已经完成执行。


图 2. Work 的状态图

  您可以随时调用WorkItem.getStatus()方法检索经过调度的 Work 的当前状态。当您不想等待所有任务的完成时,这个功能尤其有用。如果您对任何任务的完成感兴趣,您可以使用WorkManager.waitForAny(Collection, timeout)方法,或者在一个循环中调用 Thread.sleep(long)方法,并且可以通过迭代整个 WorkItem 集合并检查单个的任务状态来鉴定有多少任务已经完成了。

  这个规范也提供了当 Work 实例改变它们的生命周期状态时通知应用程序的方法。当work正在调度时,可以指定一个WorkListener。WorkManager要为各种 work 事件(例如,接受、拒绝、开始、完成)回调WorkListener实例。请注意,WorkListener 实例始终与使用WorkManager调度work 的线程在相同的 Java 虚拟机(JVM)中执行。WorkListener 类可以以独立类的形式实现,或者以 Work 类的一部分的形式来实现。下面就是listener的一个简单实现,当不同的事件发生时,它会记录一条消息。

public class TranslatorWorkListener implements WorkListener {
public void workAccepted(WorkEvent workEvent) {
logger.info("work accepted: "+workEvent.getWorkItem());
}
public void workRejected(WorkEvent workEvent) {
logger.info("work rejected: "+workEvent.getWorkItem());
}
public void workStarted(WorkEvent workEvent) {
logger.info("work started: "+workEvent.getWorkItem());
}
public void workCompleted(WorkEvent workEvent) {
logger.info("work completed: "+workEvent.getWorkItem());
}
}

  然后可以更改上述ServletParallel代码中的doTranslation()实现,来将一个TranslatorListener 传递到 WorkManager:

protected List doTranslation(List input) throws Exception {
...
TranslatorWorkListener listener = new TranslatorWorkListener();
for (Iterator iter = input.iterator(); iter.hasNext();) {
String source = (String) iter.next();
Translator translator = new DummyTranslator(source, 10 * 1000);
// schedule
Work work = new WorkTranslatorWrapper(translator);
WorkItem workItem = this.workManager.schedule(work, listener);
jobs.add(workItem);
}
logger.info("All jobs scheduled");
...
}

  如果您运行修改后的代码,您会在日志文件(或者控制台,这依赖于 WebLogic 以及您的日志记录器的配置情况)中看到与下面内容相似的信息:

22:42:12 - begin
22:42:12 - work accepted: executing: source=one, translation=null
22:42:12 - work accepted: executing: source=two, translation=null
22:42:12 - work accepted: executing: source=three, translation=null
22:42:12 - work accepted: executing: source=four, translation=null
22:42:12 - work accepted: executing: source=five, translation=null
22:42:12 - All jobs scheduled
22:42:20 - work started: executing: source=one, translation=null
22:42:24 - work started: executing: source=two, translation=null
22:42:28 - work started: executing: source=three, translation=null
22:42:30 - work started: executing: source=four, translation=null
22:42:30 - work completed: executing: source=one, translation=one_tr
22:42:30 - work started: executing: source=five, translation=null
22:42:34 - work completed: executing: source=two, translation=two_tr
22:42:38 - work completed: executing: source=three, translation=three_tr
22:42:40 - work completed: executing: source=four, translation=four_tr
22:42:40 - work completed: executing: source=five, translation=five_tr
22:42:40 - done in 27641

  在上述定义的TranslatorWorkListener中有个小窍门;它只是将 WorkItem 打印到日志中,并且因为重写了AbstractTranslator 中的 toString()方法,就看到所有那些“source=one, translation=null”的代码行,它们识别特定的任务。事实上,如果需要在WorkListener 中将 WorkItem 与 Work 对象关联,您必须将每个 WorkItem 传递到该监听器(listener),并且要保存它,以便匹配从 WorkEvent 获得的WorkItem。(getResult()方法调用workEvent.getWorkItem()时会一直返回null,直到任务变成COMPLETED状态。)下面的代码阐明了这个技巧:

public class TranslatorWorkListener implements WorkListener {
protected Map workMap =
Collections.synchronizedMap(new HashMap());
public void workAccepted(WorkEvent workEvent) {
logger.info("work accepted: "
+getTranslator(we.getWorkItem()).getSource());
}
public void workRejected(WorkEvent workEvent) {
logger.info("work rejected: "
+removeTranslator(we.getWorkItem()).getSource());
}
public void workStarted(WorkEvent workEvent) {
logger.info("work started: "
+getTranslator(we.getWorkItem()).getSource());
}
public void workCompleted(WorkEvent workEvent) {
logger.info("work completed: "
+removeTranslator(we.WorkItem()).getSource());
}
public void addTranslator(WorkItem wi, Translator t) {
workMap.put(wi, t);
}
public Work getTransaltor(WorkItem wi) {
return (Translator)workMap.get(wi);
}
public Work removeTranslator(WorkItem wi) {
return (Translator)workMap.remove(wi);
}
}

  为了使用这个方法,要修改 servlet 代码,在调用 schedule() 方法之后紧跟着就调用TranslatorWorkListener.addTranslator(WorkItem, Translator)方法。

异常处理

  到目前为止,我们都假设我们的任务始终成功地执行,并且从不抛出异常。现实中,情况当然并不总是如此。在我们虚构的用例中,就可能会出现使翻译不能成功执行的网络问题,或者单词可能拼写错误或从字典中找不到。当我们从 Work 对象的 run()方法中抛出一个异常时,会怎么样?首先,注意 run()方法的签名并没有定义任何已检查的异常,因此,我们必须使用RuntimeException 的一个实例,或者使用它的任意子类。为了进一步讨论异常的使用,让我们首先定义一个TranslationException类:

public class TranslationException extends RuntimeException {
public TranslationException(String message) {
super(message);
}
public TranslationException(String message, Throwable cause) {
super(message, cause);
}
}

现在,我们还要创建Translator 的另一个实现,它会在50%的案例中随机地抛出一个TranslationException(异常)。

public class DummyTranslatorWithError extends AbstractTranslator {
private final long delay;
public DummyTranslatorWithError(String source, long delay) {
super(source);
this.delay = delay;
}
public void translate() {
// delay to simulate network call
try {
Thread.sleep(this.delay);
}
catch (InterruptedException ignore) { }
// randomly throw Exception
if (Math.random() > 0.5) {
throw new TranslationException("Cannot translate "+getSource());
}
this.translation = this.source+"_tr";
}
}

  如果在ServletParallel的实现中将DummyTranslator替换为DummyTranslatorWithError,并且运行它,有时就会在返回结果时得到异常。所发生的情况是,容器截获任何从任务抛出的异常,并且当调用WorkItem.getResult() 时再次抛出该异常。我们可以使用这种知识来修改ServletParallel实现,来容纳错误处理:

protected List doTranslation(List input) throws Exception {
...
TranslatorWorkListener listener = new TranslatorWorkListener();
for (Iterator iter = input.iterator(); iter.hasNext();) {
String source = (String) iter.next();
Translator translator = new DummyTranslatorWithError(source, 10 * 1000);
// schedule
Work work = new WorkTranslatorWrapper(translator);
WorkItem workItem = this.workManager.schedule(work, listener);
jobs.add(workItem);
}
logger.info("All jobs scheduled");
this.workManager.waitForAll(jobs, WorkManager.INDEFINITE);
// extract results
for (Iterator iter = jobs.iterator(); iter.hasNext();) {
WorkItem workItem = (WorkItem) iter.next();
try {
// if the Work threw an exception during run
// then the exception is rethrown here
Translator translator = (Translator) workItem.getResult();
result.add(translator.getTranslation());
}
catch (Exception e) {
result.add(e.getMessage());
}
}
return result;
}

  请注意,如果 Work 在执行时期抛出一个异常,那么就不能通过执行WorkItem.getResult()调用来获得原始的 Work 实现。如果需要将失败的任务与原始Translator 关联,该应用程序可以维护一个 WorkItem对象与Translator对象之间的Map。

安全性和事务上下文传播

  Work Manager 规范的当前版本(1.1)不包括从调用者线程向调度任务的安全性和事务上下文传播,这也没什么。在当前所有的实现中,安全性上下文是传播的,而事务上下文则不然,而它是有意义的,只要您想想,假如一个应用程序开始了一个新的事务,调度任务,然后提交事务而不等待任务完成,会怎么样呢?另一方面,任务可以自主控制开始新事务、提交事务或者回滚该事务。任务也可以从对象的父类 JNDI comp:namespace 中查询对象。

结束语

  在本文中,我们创建了一个可实行的例子,使用 Work Manager 规范在 servlet 容器中实现任务的并行执行。我们上面讨论的所有内容在 EJB 容器中也适用。现在,随着BEA WebLogic 9.0的即将发布,开发人员有了一套方便、简单而强大的 API,它能从主执行线程启动和运行任意数目的并行进程,同时具备一个灵活的同步机制,并支持生命周期事件。

  我们的讨论没有涵盖该规范的所有方面,甚至对 WorkManger 领域的讨论也不详尽。例如,还有另一种支持远程任务执行的方法。如果 WorkManager 实现支持 Remoteable WorkManager,那么就可以将 Work 发送到应用程序集群的某个远程成员以执行。目前 WebLogic 或 WebSphere 平台还不支持该功能,不过如果要求负载平衡的话,将很有希望出现这个功能。

  此外,该规范还支持 Timer(计时器),这比现有的 JMX 中的timer规范和 EJB 2.1中的timer service更加灵活。我们希望在后续文章中讨论这个问题。

其他读物

原文出处

http://dev2dev.bea.com/pub/a/2005/05/parallel_tasks.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章