在前一篇文章《Oozie簡介》中,我們已經描述了Oozie工作流服務器,並且展示了一個非常簡單的工作流示例。我們還描述了針對Oozie的工作流的部署和配置,以及用來啓動、停止和監控Oozie工作流的工具。
在本文中,我們會描述一個更加複雜的例子,通過它我們可以討論更多Oozie特性,並演示如何來使用它們。
定義過程
我們在此描述的工作流會實現汽車GPS探測數據的獲取過程。我們每個小時都會以文件的形式把探測數據傳遞到指定的HDFS目錄中[1],其中包含有這個小時之內的所有探測數據。探測數據的獲取是每天針對一天內所有的24個文件完成的。如果文件的數量是24,那麼獲取過程就會啓動。否則:
- 當天什麼都不做
- 對前一天——最多到7天,發送剩下的內容到探測數據提供程序
- 如果目錄的存在時間已達到7天,那麼就獲取所有可用的探測數據文件。
過程的總體實現請見圖1
圖1: 過程圖
在此,主流程(數據獲取流程)首先會爲今天以及之前的六天計算出目錄的名稱,然後啓動(fork)七個目錄的子過程(子流程)。待所有子過程的狀態都變成終止之後,join步驟就會把控制權交給end狀態。
子過程啓動時,首先會獲得關於目錄的信息——它的日期以及文件數量。基於這條信息,它會決定是獲取數據還是把數據歸檔,或者發送剩下的郵件,或者不做任何工作。
Directory子過程實現
以下代碼負責實現的是directory子過程(代碼1)。
<workflow-app xmlns='uri:oozie:workflow:0.1' name='processDir'>
<start to='getDirInfo' />
<!-- STEP ONE -->
<action name='getDirInfo'>
<!--writes 2 properties: dir.num-files: returns -1 if dir doesn't exist,
otherwise returns # of files in dir dir.age: returns -1 if dir doesn't exist,
otherwise returns age of dir in days -->
<java>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<main-class>com.navteq.oozie.GetDirInfo</main-class>
<arg>${inputDir}</arg>
<capture-output />
</java>
<ok to="makeIngestDecision" />
<error to="fail" />
</action>
<!-- STEP TWO -->
<decision name="makeIngestDecision">
<switch>
<!-- empty or doesn't exist -->
<case to="end">
${wf:actionData('getDirInfo')['dir.num-files'] lt 0 ||
(wf:actionData('getDirInfo')['dir.age'] lt 1 and wf:actionData('getDirInfo')['dir.num-files'] lt 24)}
</case>
<!-- # of files >= 24 -->
<case to="ingest">
${wf:actionData('getDirInfo')['dir.num-files'] gt 23 ||
wf:actionData('getDirInfo')['dir.age'] gt 6}
</case>
<default to="sendEmail"/>
</switch>
</decision>
<!--EMAIL-->
<action name="sendEmail">
<java>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<main-class>com.navteq.oozie.StandaloneMailer</main-class>
<arg>[email protected]</arg>
<arg>[email protected]</arg>
<arg>${inputDir}</arg>
<arg>${wf:actionData('getDirInfo')['dir.num-files']}</arg>
<arg>${wf:actionData('getDirInfo')['dir.age']}</arg>
</java>
<ok to="end" />
<error to="fail" />
</action>
<!--INGESTION -->
<action name="ingest">
<java>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<prepare>
<delete path="${outputDir}" />
</prepare>
<configuration>
<property>
<name>mapred.reduce.tasks</name>
<value>300</value>
</property>
</configuration>
<main-class>com.navteq.probedata.drivers.ProbeIngest</main-class>
<arg>-conf</arg>
<arg>action.xml</arg>
<arg>${inputDir}</arg>
<arg>${outputDir}</arg>
</java>
<ok to=" archive-data" />
<error to="ingest-fail" />
</action>
<!-- Archive Data -->
<action name="archive-data">
<fs>
<move source='${inputDir}' target='/probe/backup/${dirName}' />
<delete path = '${inputDir}' />
</fs>
<ok to="end" />
<error to="ingest-fail" />
</action>
<kill name="ingest-fail">
<message>Ingestion failed, error message[${wf:errorMessage(wf:lastErrorNode())}]</message>
</kill>
<kill name="fail">
<message>Java failed, error message[${wf:errorMessage(wf:lastErrorNode())}]</message>
</kill>
<end name='end' />
</workflow-app>
代碼1: Directory子過程
這個子過程的start節點會觸發自定義的java節點,這個節點會獲得目錄信息(代碼2)。
package com.navteq.oozie;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.GregorianCalendar;
import java.util.Properties;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
public class GetDirInfo {
private static final String OOZIE_ACTION_OUTPUT_PROPERTIES = "oozie.action.output.properties";
public static void main(String[] args) throws Exception {
String dirPath = args[0];
String propKey0 = "dir.num-files";
String propVal0 = "-1";
String propKey1 = "dir.age";
String propVal1 = "-1";
System.out.println("Directory path: '"+dirPath+"'");
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(conf);
Path hadoopDir = new Path(dirPath);
if (fs.exists(hadoopDir)){
FileStatus[] files = FileSystem.get(conf).listStatus(hadoopDir);
int numFilesInDir = files.length;
propVal0 = Integer.toString(numFilesInDir);
long timePassed, daysPassedLong;
int daysPassed;
String dirName = hadoopDir.getName();
String[] dirNameArray = dirName.split("-");
if (dirNameArray.length == 3) {
int year = Integer.valueOf(dirNameArray[0]);
int month = Integer.valueOf(dirNameArray[1]) - 1; //months are 0 based
int date = Integer.valueOf(dirNameArray[2]);
GregorianCalendar dirCreationDate = new GregorianCalendar(year, month, date);
timePassed = (new GregorianCalendar()).getTimeInMillis() - dirCreationDate.getTimeInMillis();
daysPassed = (int) = timePassed / 1000 / 60 / 60 / 24;
propVal1 = Integer.toString(daysPassed);
}
}
String oozieProp = System.getProperty(OOZIE_ACTION_OUTPUT_PROPERTIES);
if (oozieProp != null) {
File propFile = new File(oozieProp);
Properties props = new Properties();
props.setProperty(propKey0, propVal0);
props.setProperty(propKey1, propVal1);
OutputStream os = new FileOutputStream(propFile);
props.store(os, "");
os.close();
} else {
throw new RuntimeException(OOZIE_ACTION_OUTPUT_PROPERTIES + " System property not defined");
}
}
}
代碼2: 獲得目錄信息的節點
這個類會獲得目錄名作爲輸入的參數,並首先檢查該目錄是否存在。如果目錄不存在,那麼存在時間(age)和文件數量都會返回-1,否則,這兩個值就會返回給子過程。
子過程的下一步是一個switch(決定)聲明,它會決定如何處理目錄。如果目錄不存在(文件數 < 0),或者是當前日期(存在時間 < 1)並且文件數量少於24(文件數 < 24),那麼子過程就會直接轉換到終止狀態。如果所有文件都位於子目錄中(文件數 > 23)或者目錄是在至少七天前創建的(存在時間 > 6),那麼就會有如下操作:
- 使用現存的Map/reduce程序[2]獲取數據
- 目錄會備份在數據歸檔中,然後刪除
對action節點的其它配置
獲取動作向你展示了另外一些Oozie配置參數,包括:
- Prepare——如果出現了prepare參數,就意味着在啓動作業(job)之前會刪除路徑列表。這應該專門用於清理目錄。刪除操作會在fs.default.name文件系統中執行。
- Configuration——如果出現了configuration元素,它其中就會包含針對Map/Reduce 作業的JobConf屬性。它不僅可以用於map/reduce動作, 而且還可以用於啓動map/reduce作業的java動作。
如果不是以上兩種情況,那麼子過程就會發送剩餘的郵件,然後退出。郵件是作爲另一個java主類實現的(代碼3)。
package com.navteq.oozie;
import java.util.Properties;
import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
public class StandaloneMailer {
private static String _mServer = "imailchi.navtech.com";
private static Properties _props = null;
private StandaloneMailer(){}
public static void init(String mServer){
_mServer = mServer;
_props = new Properties();
_props.setProperty("mail.smtp.host", _mServer);
}
public static void SendMail(String subject, String message, String from, String to) throws Exception {
// create some properties and get the default Session
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);
String [] recipients = new String[] {to};
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);
}
public static void main (String[] args) throws Exception {
if (args.length ==5){
init(_mServer);
StringBuilder subject = new StringBuilder();
StringBuilder body = new StringBuilder();
subject.append("Directory ").append(args[2]).append(" contains").append(args[3]).append(" files.");
body.append("Directory ").append(args[2]).append(" is ").append(args[4]).
append(" days old and contains only ").append(args[3]).append(" files instead of 24.");
SendMail(subject.toString(), body.toString(), args[0], args[1]);
}
else {
throw new Exception("Invalid number of parameters provided for email");
}
}
}
列表3: 發送提醒郵件
這是使用了javax.mail API的簡單實現,用於發送郵件。
主過程的實現
我們已經實現了子過程,然後,對主過程的實現就變得非常簡單了(列表4)[3]。
<workflow-app xmlns='uri:oozie:workflow:0.1' name='processDirsWF'>
<start to='getDirs2Process' />
<!-- STEP ONE -->
<action name='getDirs2Process'>
<!--writes 2 properties: dir.num-files: returns -1 if dir doesn't exist,
otherwise returns # of files in dir dir.age: returns -1 if dir doesn't exist,
otherwise returns age of dir in days -->
<java>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<main-class>com.navteq.oozie.GenerateLookupDirs</main-class>
<capture-output />
</java>
<ok to="forkSubWorkflows" />
<error to="fail" />
</action>
<fork name="forkSubWorkflows">
<path start="processDir0"/>
<path start="processDir1"/>
<path start="processDir2"/>
<path start="processDir3"/>
<path start="processDir4"/>
<path start="processDir5"/>
<path start="processDir6"/>
<path start="processDir7"/>
</fork>
<action name="processDir0">
<sub-workflow>
<app-path>hdfs://sachicn001:8020/user/gtitievs/workflows/ingest</app-path>
<configuration>
<property>
<name>inputDir</name>
<value>hdfs://sachicn001:8020/user/data/probedev/files/${wf:actionData('getDirs2Process')['dir0']}</value>
</property>
<property>
<name>outputDir</name>
<value>hdfs://sachicn001:8020/user/gtitievs/probe-output/${wf:actionData('getDirs2Process')['dir0']}</value>
</property>
<property>
<name>jobTracker</name>
<value>${jobTracker}</value>
</property>
<property>
<name>nameNode</name>
<value>${nameNode}</value>
</property>
<property>
<name>activeDir</name>
<value>hdfs://sachicn001:8020/user/gtitievs/test-activeDir</value>
</property>
<property>
<name>dirName</name>
<value>${wf:actionData('getDirs2Process')['dir0']}</value>
</property>
</configuration>
</sub-workflow>
<ok to="joining"/>
<error to="fail"/>
</action>
….
<action name="processDir7">
<sub-workflow>
<app-path>hdfs://sachicn001:8020/user/gtitievs/workflows/ingest</app-path>
<configuration>
<property>
<name>inputDir</name>
<value>hdfs://sachicn001:8020/user/data/probedev/files/${wf:actionData('getDirs2Process')['dir7']}</value>
</property>
<property>
<name>outputDir</name>
<value>hdfs://sachicn001:8020/user/gtitievs/probe-output/${wf:actionData('getDirs2Process')['dir7']}</value>
</property>
<property>
<name>dirName</name>
<value>${wf:actionData('getDirs2Process')['dir7']}</value>
</property>
</configuration>
</sub-workflow>
<ok to="joining"/>
<error to="fail"/>
</action>
<join name="joining" to="end"/>
<kill name="fail">
<message>Java failed, error message[${wf:errorMessage(wf:lastErrorNode())}]</message>
</kill>
<end name='end' />
</workflow-app>
代碼4: 數據獲取主過程
這個過程首先會觸發java節點,計算需要處理的目錄列表(列表5),然後對每個目錄執行子過程,從而處理給定的目錄。
package com.navteq.oozie;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Properties;
public class GenerateLookupDirs {
public static final long dayMillis = 1000 * 60 * 60 * 24;
private static final String OOZIE_ACTION_OUTPUT_PROPERTIES = "oozie.action.output.properties";
public static void main(String[] args) throws Exception {
Calendar curDate = new GregorianCalendar();
int year, month, date;
String propKey, propVal;
String oozieProp = System.getProperty(OOZIE_ACTION_OUTPUT_PROPERTIES);
if (oozieProp != null) {
File propFile = new File(oozieProp);
Properties props = new Properties();
for (int i = 0; i<8; ++i) {
year = curDate.get(Calendar.YEAR);
month = curDate.get(Calendar.MONTH) + 1;
date = curDate.get(Calendar.DATE);
propKey = "dir"+i;
propVal = year + "-" +
(month < 10 ? "0" + month : month) + "-" +
(date < 10 ? "0" + date : date);
props.setProperty(propKey, propVal);
curDate.setTimeInMillis(curDate.getTimeInMillis() - dayMillis);
}
OutputStream os = new FileOutputStream(propFile);
props.store(os, "");
os.close();
} else {
throw new RuntimeException(OOZIE_ACTION_OUTPUT_PROPERTIES + " System property not defined");
}
}
}
代碼5: 目錄計算程序
結論
在這篇文章中,我們向你展示了一個更復雜的完整的工作流示例,它讓我們可以演示更多的Oozie特性以及對它們的應用。在下一篇文章中,我們會討論構建可重用的Oozie組件庫,並使用自定義的節點擴展Oozie。
致謝
非常感謝我們在Navteq的同事Gregory Titievsky,他爲我們實現了大部分代碼。
關於作者
Boris Lublinsky是NAVTEQ公司的首席架構師,在這家公司中他的工作是爲大型數據管理和處理、SOA以及實現各種NAVTEQ的項目定義架構的願景。他還是InfoQ的SOA編輯,以及OASIS的SOA RA工作組的參與者。Boris是一位作者,還經常發表演講,他最新的一本書是《Applied SOA》。
Michael Segel在過去二十多年間一直與客戶寫作,識別並解決他們的業務問題。Michael已經作爲多種角色、在多個行業中工作過。他是一位獨立顧問,總是期望能夠解決所有有挑戰的問題。Michael擁有俄亥俄州立大學的軟件工程學位。
參考信息
[1] 目錄的名稱是蒐集這條數據的日期。
[2] 這是已經存在的程序,對它的描述與本文無關。
[3] 在此省略了一些重複代碼。
查看英文原文:Oozie by Example