扩展oozie
在我们以前的文章[1、2]中,我们描述了Oozie工作流服务器,并提供了几个工作流示例。 我们还描述了Oozie工作流的部署和配置以及用于启动,停止和监视Oozie工作流的工具。
在本文中,我们将展示如何利用Oozie的可扩展性来实现自定义编排语言扩展。
为什么要使用自定义节点?
正如我们在[1]中所解释的那样,Oozie开箱即用地提供了一种“最小”工作流语言,该语言仅包含少数控件和动作节点。 尽管动作节点之一是java动作节点,它允许使用Oozie工作流程中的main方法调用任何java类,但这种方法并不总是最佳方法。 原因之一是,Java操作在Hadoop集群中作为map-reduce任务与单个Mapper任务一起执行。 一方面,这提供了许多优点:
- 内置的可伸缩性和对map / reduce框架的故障转移支持减轻了将这些功能内置到Oozie中的必要性。
- 外部化执行使Oozie引擎更轻巧,从而可以支持更多同时运行的进程
另一方面,此方法有一些缺点:
- 作为映射器任务启动每个Java节点会导致在Hadoop集群中启动新JVM的开销。
- 外部化java类执行将创建额外的网络流量,以使该执行与Oozie服务器同步。
- 从Java代码传递参数变得相当昂贵的操作
尽管在运行相对较长(几分钟,几小时)的map / reduce或Pig作业的情况下,其优势远远超过了缺点,但在简单的Java节点(请参见[2])的情况下,外部化执行的开销要难得多证明。 因此,使用自定义动作节点的原因之一是直接在Oozie执行上下文中支持轻量级Java类[1]的执行。
使用自定义动作的另一个原因是为了改善工作流的语义/可读性。 由于Oozie是用于支持基于Hadoop的编排的工作流引擎,因此其语言语义非常以Hadoop执行为中心-Hadoop文件系统,map / reduce,Pig等。此语义套件非常适合Hadoop开发人员,但并没有过多提及功能给定动作的 可以提出与业务相关的动作本身的命名约定,但这只能部分解决问题。动作命名仅反映给定流程的语义,而不是整个主题领域,也不能解决动作问题参数,这些参数仅对开发人员有意义。
幸运的是,Oozie支持一种非常优雅的可扩展机制–自定义动作节点[3],可以轻松解决这两个问题。 自定义动作节点允许使用其他动作(动词)扩展Oozie的语言。 Oozie动作节点可以是同步或异步的。
- 同步节点-由Oozie内联执行,Oozie在继续进行之前等待这些节点的完成。 这些节点适用于轻量级任务,例如自定义计算,FileSystem move,mkdir,delete。
- 异步节点–由Oozie启动,但在Oozie引擎外部执行,监视正在执行的动作是否完成。 这是通过操作的回调或Oozie轮询操作状态来完成的。
实施Oozie自定义操作处理程序
在此示例中,我们将把[2]中显示的独立邮件程序转换为自定义电子邮件操作(清单1)。
package com.navteq.assetmgmt.oozie.custom;import java.util.Properties;
import java.util.StringTokenizer;
import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.apache.oozie.ErrorCode;
import org.apache.oozie.action.ActionExecutor;
import org.apache.oozie.action.ActionExecutorException;
import org.apache.oozie.action.ActionExecutorException.ErrorType;
import org.apache.oozie.client.WorkflowAction;
import org.apache.oozie.util.XmlUtils;
import org.jdom.Element;
import org.jdom.Namespace;
public class EmailActionExecutor extends ActionExecutor {
private static final String NODENAME = "eMail";
private static final String SUCCEEDED = "OK";
private static final String FAILED = "FAIL";
private static final String KILLED = "KILLED";
private static final String DEFAULMAILSERVER = "imailchi.navtech.com";
private static final String EMAILSERVER = "emailServer";
private static final String SUBJECT = "emailSubject";
private static final String MESSAGE = "emailBody";
private static final String FROM = "emailFrom";
private static final String TO = "emailTo";
public EmailActionExecutor() {
super(NODENAME);
}
@Override
public void check(Context context, WorkflowAction action) throws ActionExecutorException {
// Should not be called for synch operation
throw new UnsupportedOperationException();
}
@Override
public void end(Context context, WorkflowAction action)throws ActionExecutorException {
String externalStatus = action.getExternalStatus();
WorkflowAction.Status status = externalStatus.equals( SUCCEEDED ) ?
WorkflowAction.Status. OK : WorkflowAction.Status. ERROR ;
context.setEndData(status, getActionSignal(status));
}
@Override
public boolean isCompleted(String arg0) {
return true;
}
@Override
public void kill(Context context, WorkflowAction action) throws ActionExecutorException {
context.setExternalStatus(KILLED);
context.setExecutionData(KILLED, null);
}
@Override
public void start(Context context, WorkflowAction action) throws ActionExecutorException {
// Get parameters from Node configuration
try{
Element actionXml = XmlUtils.parseXml(action.getConf());
Namespace ns = Namespace.getNamespace("uri:custom:email-action:0.1");
String server = actionXml.getChildTextTrim(EMAILSERVER, ns);
String subject = actionXml.getChildTextTrim(SUBJECT, ns);
String message = actionXml.getChildTextTrim(MESSAGE, ns);
String from = actionXml.getChildTextTrim(FROM, ns);
String to = actionXml.getChildTextTrim(TO, ns);
// Check if all parameters are there
if(server == null)
server = DEFAULMAILSERVER;
if((message == null) || (from == null) || (to == null))
throw new ActionExecutorException(ErrorType.FAILED,
ErrorCode.E0000.toString(), "Not all parameters are defined");
// Execute action synchronously
SendMail(server, subject, message, from, to);
context.setExecutionData(SUCCEEDED, null);
}
catch(Exception e){
context.setExecutionData(FAILED, null);
throw new ActionExecutorException(ErrorType.FAILED,
ErrorCode.E0000.toString(), e.getMessage());
}
}
// Sending an email
public void SendMail(String server, String subject, String message,
String from, String to) throws Exception {
// create some properties and get the default Session
Properties props = new Properties();
props.setProperty("mail.smtp.host", server);
Session session = Session.getDefaultInstance(props, null);
// create a message
Message msg = new MimeMessage(session);
// set the from and to address
InternetAddress addressFrom = new InternetAddress(from);
msg.setFrom(addressFrom);
// To is a comma separated list
StringTokenizer st = new StringTokenizer(to, ",");
String [] recipients = new String[st.countTokens()];
int rc = 0;
while(st.hasMoreTokens())
recipients[rc++] = st.nextToken();
InternetAddress[] addressTo = new InternetAddress[recipients.length];
for (int i = 0; i < recipients.length; i++){
addressTo[i] = new InternetAddress(recipients[i]);
}
msg.setRecipients(Message.RecipientType.TO, addressTo);
// Setting the Subject and Content Type
msg.setSubject(subject);
msg.setContent(message, "text/plain");
Transport.send(msg);
}
}
清单1:电子邮件自定义操作
此实现扩展了ActionExecutor [2]类(由Oozie提供)并覆盖了所需的方法。 因为发送电子邮件是非常快速的操作,所以我们决定将其实现为同步操作处理程序,这意味着它是在Oozie执行上下文中执行的。
我们的实现(清单1)遵循Oozie文档并实现所有必需的方法:
- 任何自定义操作处理程序都不需要参数构造函数。 此构造函数注册将在工作流XML内使用的动作处理程序名称(使用动作名称调用super)。
- InitActionType [3]方法可用于在执行操作时注册可能的异常及其类型和错误消息,并对执行程序本身进行初始初始化。
- Start方法用于开始执行动作。 因为我们已经实现了同步动作,所以整个动作都在这里执行。 Oozie使用两个参数Context和WorkflowAction调用此方法。 上下文提供对Oozie工作流执行上下文的访问,该上下文除其他内容外还包含工作流变量,并提供用于操作它们的非常简单的API(set,get) [4] 。 WorkflowAction提供Oozie对当前操作的定义。
- 检查方法,Oozie使用它来检查动作的状态。 永远不要调用同步动作。
- Kill方法,用于杀死正在运行的作业或动作。
- End方法用于完成操作后可能需要进行的任何清理或处理。 它还必须设置执行结果。
部署和使用Oozie自定义操作处理程序
使用自定义动作执行实现后,有必要为我们的新电子邮件动作[5]定义XML模式(清单2)
<?xml version = "1.0" encoding = "UTF-8" ?>
< xs:schema xmlns:xs = "http://www.w3.org/2001/XMLSchema"
xmlns:email = "uri:custom:email-action:0.1"
elementFormDefault = "qualified"
targetNamespace = "uri:custom:email-action:0.1" >
< xs:complexType name = "EMAIL" >
< xs:sequence >
< xs:element name = "emailServer" type = "xs:string" minOccurs = "0" maxOccurs = "1" />
< xs:element name = "emailSubject" type = "xs:string" />
< xs:element name = "emailFrom" type = "xs:string" />
< xs:element name = "emailTo" type = "xs:string" />
< xs:element name = "emailBody" type = "xs:string" />
</ xs:sequence >
</ xs:complexType >
< xs:element name = "eMail" type = "email:EMAIL" ></ xs:element >
</ xs:schema >
清单2:电子邮件组件的XML模式
自定义操作代码和XML模式都必须打包在一个jar中,例如emailAction。 罐。 Oozie的oozie-setup.sh脚本可用于使用以下命令(清单3)将此(和其他)罐子添加到Oozie的战争中:
$ bin/oozie-setup.sh -jars emailAction.jar:mail.jar (See Adding Jars to Oozie)
清单3:部署命令
将罐子添加到Oozie 请注意,Cloudera建议的oozie-setup.sh命令行将重建您的Oozie war文件,如果您使用网页来监视作业,则将丢失Java脚本扩展名。 在测试中,我们很难同时包含-extjs和-jars选项。 为了方便起见,我们将jar文件复制到$ {CATALINA_BASE} / webapps / oozie / WEB-INF / lib中,其中$ {CATALINA_BASE}是/ var / lib / oozie / oozie-server。 请注意,如果有人重建战争文件,则可能会丢失这些扩展名,因此必须手动添加。 为了进行测试,我们建议复制您的jar文件,但是为了生产和实施,我们建议将jar文件添加到war文件中。 |
现在,我们需要在Oozie运行时中注册有关自定义执行程序的信息。 这是通过扩展oozie-site.xml [6]完成的 。 可以通过在oozie配置文件oozie-site.xml(清单4)中添加/更改“ oozie.service.ActionService.executor.ext.classes” [7]来完成自定义操作本身的注册。
……………………………………
<property>
<name>oozie.service.ActionService.executor.ext.classes</name>
<value>com.navteq.assetmgmt.oozie.custom. EmailActionExecutor </value>
</property>
……………………………………
清单4:定制执行配置
应该将新操作(清单2)的XML模式添加到oozie-site.xml中的属性“ oozie.service.WorkflowSchemaService.ext.schemas” [8] (清单5)。
………………………………………
<property>
<name>oozie.service.SchemaService.wf.ext.schemas</name>
<value> emailAction.xsd</value>
</property>
…………………………………
清单5:定制模式配置
最后,一旦重新启动Tomcat,就可以在工作流中使用自定义操作节点。
为了测试我们的实现,我们创建了一个简单的工作流(清单6),该工作流使用执行程序发送电子邮件。
<!--
Copyright (c) 2011 NAVTEQ! Inc. All rights reserved.
Test emailOozie Script
-->
<workflow-app xmlns = 'uri:oozie:workflow:0.1' name = 'emailTester' >
<start to = 'simpleEmail' />
<action name = 'simpleEmail' >
<eMail xlmns =“ uri:custom:email-action:0.1”>
<emailSubject> test </emailSubject>
<emailFrom> mike.segel @<mycompany>.com </emailFrom>
<emailTo> boris.lublinsky @<mycompany>.com </emailTo>
<emailMessage> This is a test message, if you can see this, Mikey did something right! :) </emailMessage>
</eMail>
<error to = "fail" />
<ok to = "end" />
</action>
<kill name = "fail" >
<message> Workflow failed, error message[${wf:errorMessage(wf:lastErrorNode())}] </message>
</kill>
<end name = 'end' />
</workflow-app>
清单6:使用电子邮件自定义操作执行器的简单工作流
结论
在本文中,我们展示了如何通过创建自定义动作执行程序来扩展Oozie。 这样做可以定义和实现与部门/企业功能一致的部门/企业特定的Oozie方言(域特定语言)。 这种领域特定的语言可以简化给定部门/企业的构建过程,并提高其可读性。
尽管Oozie仍相对不成熟,但它提供了一个基本的框架来处理由多个映射/归约作业组成的流程,并且能够向整个业务流程中添加非映射归约作业。 随着越来越多的用户使用Oozie并提供反馈,我们相信它有可能成为Hadoop环境的强大组成部分。
与我们在这三篇小文章中介绍的内容相比,Oozie的意义要大得多。 我们希望他们能使您对Oozie工作流引擎产生足够的兴趣,并为您进一步进行Oozie探索提供良好的起点。
参考资料
关于作者
Boris Lublinsky是NAVTEQ的首席架构师,在那里他致力于为大数据管理和处理以及SOA定义体系结构远景,并实施各种NAVTEQ项目。 他还是InfoQ的SOA编辑器,并且是OASIS中SOA RA工作组的参与者。 鲍里斯(Boris)是他的最新著作《应用的SOA》的作者兼经常演讲者。
迈克尔·塞格尔(Michael Segel )在过去20多年中一直与客户合作,以识别和解决他们的业务问题。 迈克尔曾在多个行业担任过多个职务。 他是一位独立顾问,他一直致力于解决任何具有挑战性的问题。 Michael拥有俄亥俄州立大学的软件工程学位。
[1]此类的示例可以是各种计数器操作,简单的计算等。
[2]所有Oozie动作执行者(属于Oozie发行版)都是通过扩展此类来实现的
[3]在我们的实现中,我们使用默认实现,这就是为什么它不出现在代码中的原因。 查看Oozie源代码[4],以了解如何在现有Oozie操作处理程序中实现此方法。
[4]配置自定义执行程序有两个选项-工作流变量和/或操作配置。 在我们的示例中,我们显示了后者,但实际上,它始终是两者的结合
[5]确保不仅定义复杂类型定义,还定义元素定义。 这就是OOzie期望的
[6]在oozie发行版中通常称为oozie-default.xml。
[7]对于多个执行程序,类名称应以逗号分隔。
[8]对于多个执行,可以使用多个模式的逗号分隔列表。
扩展oozie