Oozie工作流引擎核心机制与实践指南
Oozie概述
Apache Oozie是一个基于有向无环图(DAG)的工作流调度系统,用于管理Hadoop作业的依赖关系与执行顺序。其核心由控制流节点(如start、end、fork、join)和动作节点(如MapReduce、Shell、Hive)构成,支持复杂业务场景的自动化编排。
主要功能模块
- Workflow:定义顺序执行或并行分支(fork/join)的作业流程。
- Coordinator:基于时间或数据事件触发Workflow的定时执行。
- Bundle:将多个Coordinator应用绑定为逻辑单元统一管理。
- job.properties:集中配置全局参数,如HDFS路径、数据库连接等。
配置示例(Bundle)详见:参考链接
工作流生命周期
Oozie中的工作流作业存在以下状态转换:
| 状态 | 说明 |
|---|---|
| PREP | 作业已创建但未启动,处于预备阶段。 |
| RUNNING | 作业处于执行中,可能因异常终止或挂起。 |
| SUSPENDED | 运行中的作业被挂起,需恢复或杀死操作才能继续。 |
| SUCCEEDED | 成功到达end节点,流程正常完成。 |
| KILLED | 在PREP、RUNNING或SUSPENDED状态下被强制终止。 |
| FAILED | 因未捕获异常导致作业异常终止。 |
控制流节点
控制节点负责管理流程的拓扑结构,包括:
- start:工作流入口。
- end:工作流结束点。
- kill:强制终止并输出错误信息。
- decision:基于条件选择执行路径(类似if-else)。
- fork/join:并行分支与同步汇合。
常用动作节点
Ssh Action
<action name="ssh_task">
<ssh>
<host>user@192.168.1.100</host>
<command>/usr/bin/python3</command>
<args>/home/data/script.py</args>
<args>--config=prod</args>
<capture-output/>
</ssh>
<ok to="next_node"/>
<error to="fail_node"/>
</action>
Email Action
<action name="alert_email">
<email xmlns="uri:oozie:email-action:0.1">
<to>admin@company.com</to>
<subject>Job ${jobName} Failed on ${yesterday}</subject>
<body>Error: ${wf:errorMessage(wf:lastErrorNode())}</body>
</email>
<ok to="end"/>
<error to="end"/>
</action>
Shell Action(通过SSH执行)
<action name="run_shell">
<ssh xmlns="uri:oozie:ssh-action:0.2">
<host>192.168.10.241</host>
<command>/bin/bash</command>
<arg>${sshUri}/preparam.sh</arg>
<arg>${mysql_host}</arg>
<arg>${mysql_user}</arg>
<capture-output/>
</ssh>
<ok to="next"/>
<error to="fail"/>
</action>
Hive Action
<action name="hive_etl">
<hive xmlns="uri:oozie:hive-action:0.2">
<job-tracker>node01:8021</job-tracker>
<name-node>node02:8020</name-node>
<prepare>
<delete path="${output_dir}"/>
</prepare>
<configuration>
<property>
<name>mapred.compress.map.output</name>
<value>true</value>
</property>
</configuration>
<script>transform.q</script>
<param>Input=/data/source</param>
<param>Output=${output_dir}</param>
</hive>
<ok to="post_process"/>
<error to="cleanup"/>
</action>
Sqoop Action
<action name="sqoop_import">
<sqoop xmlns="uri:oozie:sqoop-action:0.2">
<job-tracker>node01:8021</job-tracker>
<name-node>node02:8020</name-node>
<arg>import</arg>
<arg>--connect</arg>
<arg>jdbc:mysql://dbhost:3306/mydb</arg>
<arg>--table</arg>
<arg>orders</arg>
<arg>--target-dir</arg>
<arg>hdfs://cluster/data/orders</arg>
<arg>-m</arg>
<arg>4</arg>
</sqoop>
<ok to="validate"/>
<error to="cleanup"/>
</action>
FS Action(文件操作)
<action name="hdfs_ops">
<fs>
<delete path='/tmp/backup'/>
<mkdir path='/data/archive'/>
<move source='/data/tmp' target='/data/final'/>
<chmod path='/data/final' permissions='755' dir-files='true'/>
<touchz path='/data/.done'/>
</fs>
<ok to="next"/>
<error to="fail"/>
</action>
Java Action
执行单一Map任务的自定义Java程序。
<action name="java_processor">
<java>
<job-tracker>cluster-rm:8032</job-tracker>
<name-node>cluster-nn:8020</name-node>
<main-class>com.example.DataTransformer</main-class>
<java-opts>-Xmx2048m -Dconfig=prod</java-opts>
<arg>--input</arg>
<arg>hdfs://data/input</arg>
<arg>--output</arg>
<arg>hdfs://data/output</arg>
<capture-output/>
</java>
<ok to="end"/>
<error to="fail"/>
</action>
Sub-workflow(嵌入子流程)
<action name="child_workflow">
<sub-workflow>
<app-path>${wf_base}/etl_pipeline</app-path>
<propagate-configuration/>
</sub-workflow>
<ok to="merge"/>
<error to="fail"/>
</action>
常见调度文件模板
workflow.xml
<workflow-app xmlns="uri:oozie:workflow:0.5" name="${jobName}">
<global>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<configuration>
<property>
<name>mapred.job.queue.name</name>
<value>${queueName}</value>
</property>
<property>
<name>oozie.launcher.mapred.job.queue.name</name>
<value>${launcherQueue}</value>
</property>
</configuration>
</global>
<start to="step1"/>
<action name="step1">
<ssh xmlns="uri:oozie:ssh-action:0.2">
<host>192.168.1.50</host>
<command>/bin/bash</command>
<arg>/app/scripts/init.sh</arg>
<capture-output/>
</ssh>
<ok to="parallel_start"/>
<error to="kill_job"/>
</action>
<fork name="parallel_start">
<path start="sub_task_a"/>
<path start="sub_task_b"/>
</fork>
<action name="sub_task_a">
<sub-workflow>
<app-path>${wf_base}/task_a</app-path>
<propagate-configuration/>
</sub-workflow>
<ok to="join_point"/>
<error to="kill_job"/>
</action>
<action name="sub_task_b">
<sub-workflow>
<app-path>${wf_base}/task_b</app-path>
<propagate-configuration/>
</sub-workflow>
<ok to="join_point"/>
<error to="kill_job"/>
</action>
<join name="join_point" to="final_step"/>
<action name="final_step">
<ssh>
<host>192.168.1.50</host>
<command>/bin/bash</command>
<arg>${ssh_path}/postprocess.sh</arg>
<capture-output/>
</ssh>
<ok to="end"/>
<error to="kill_job"/>
</action>
<kill name="kill_job">
<message>Job failed: ${wf:errorMessage(wf:lastErrorNode())}</message>
</kill>
<end name="end"/>
</workflow-app>
coordinator.xml
<coordinator-app name="${jobName}" frequency="0 30 1 * *" start="${start}" end="${end}" timezone="GMT+0800"
xmlns="uri:oozie:coordinator:0.4">
<action>
<workflow>
<app-path>${wfPath}</app-path>
<configuration>
<property>
<name>ds</name>
<value>${coord:formatTime(coord:dateOffset(coord:nominalTime(), -1, 'DAY'), 'yyyy-MM-dd')}</value>
</property>
<property>
<name>mysql_host</name>
<value>${mysql_host}</value>
</property>
<property>
<name>mysql_user</name>
<value>${mysql_user}</value>
</property>
</configuration>
</workflow>
</action>
</coordinator-app>
job.properties
nameNode=hdfs://mycluster
jobTracker=hadoop02:8042
queueName=default
launcherQueue=oozie
jobName=monthly_etl
mysql_host=192.168.1.200
mysql_user=etl_user
mysql_pwd=secret123
gn=prod
timezone=GMT+8
oozie.use.system.libpath=true
ssh_path=/app/scripts
wfPath=${nameNode}/user/oozie/workflows/etl
baseUri=${nameNode}/user/oozie/workflows/etl
oozie.coord.application.path=${wfPath}
start=2023-01-01T03:30+0800
end=2099-12-31T23:00+0800
EL表达式详解
常用时间运算表达式:
- 计算昨天:
${coord:formatTime(coord:dateOffset(coord:nominalTime(), -1, 'DAY'), 'yyyy-MM-dd')} - 计算上月:
${coord:formatTime(coord:dateOffset(coord:nominalTime(), -1, 'MONTH'), 'yyyy-MM')}
注意:时间单位(DAY, MONTH)需大写。
| 表达式 | 功能 |
|---|---|
| ${coord:minutes(n)} | 返回n分钟后的时间点 |
| ${coord:hours(n)} | 返回n小时后的时间点 |
| ${coord:days(n)} | 返回n天后的时间点 |
| ${coord:months(n)} | 返回n个月后的时间点 |
| ${coord:endOfDays(n)} | 返回n天后的当天最晚时间 |
| ${coord:endOfMonths(1)} | 返回当月最后一天的最晚时间 |
| ${coord:current(n)} | 返回第n个dataset实例的创建时间 |
| ${coord:dataIn(String)} | 解析输入事件的URI |
| ${coord:dataOut(String)} | 解析输出事件的URI |
| ${coord:offset(n, unit)} | 基于名义时间偏移n个单位(MINUTE/HOUR/DAY/MONTH/YEAR) |
| ${coord:hoursInDay(n)} | 第n天的小时数(n可为负) |
| ${coord:daysInMonth(n)} | 第n个月的天数(n可为负) |
| ${coord:tzOffset()} | 时区偏移分钟数 |
| ${coord:latest(n)} | 最近第n个可用的dataset实例 |
| ${coord:future(n, limit)} | 未来第n个dataset实例(limit限制个数) |
| ${coord:nominalTime()} | 基于频率计算的名义时间(如每天固定时刻) |
| ${coord:actualTime()} | 动作实际创建的时间 |
| ${coord:user()} | 启动此协调器的用户名 |
| ${coord:dateOffset(base, instance, unit)} | 计算 base + instance * unit 的时间 |
| ${coord:formatTime(timestamp, pattern)} | 按pattern格式化时间戳 |
参考资料:详细说明