跟着示例學Oozie

在前一篇文章《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


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