Hadoop 平台上的数据处理往往由多个阶段组成,例如先用 MapReduce 做数据清洗,再用 Hive 做汇总统计,最后用 Sqoop 导入关系型数据库。Oozie 的作用就是把这些环节按依赖顺序编排起来,形成一个可调度、可监控的工作流。workflow.xml 正是这个工作流的定义文件,它以 XML 格式描述工作流从哪个节点开始、执行哪些动作、成功或失败后如何跳转。与其把 workflow.xml 理解成普通配置文件,不如把它看作 Oozie 调度引擎的动作蓝图,引擎会根据这份蓝图解析节点关系并驱动任务执行。

在实际使用中,workflow.xml 需要与 job.properties 配合。job.properties 中定义集群相关的参数,例如 JobTracker 地址、NameNode 地址、输入输出路径等,workflow.xml 通过 EL 表达式读取这些参数。这样同一个工作流定义可以复用于不同环境,不需要因为集群变更而反复修改 XML。下面从文件结构、动作配置、控制流和调试方法几个方面展开。
一、workflow.xml 的核心节点与结构
一个合法的 workflow.xml 必须包含 <workflow-app> 根节点,并声明 Oozie 工作流命名空间。根节点内部至少需要 <start> 节点、一个或多个 <action> 节点以及 <end> 节点。<start> 节点的 to 属性指定工作流执行的第一个动作,<end> 节点表示正常结束,而 <kill> 节点用于处理失败或异常路径。虽然 <kill> 不是必需的,但强烈建议配置,否则任务失败时容易留下不清晰的状态。
动作节点是工作流的实际执行单元,常见类型包括 <map-reduce>、<hive>、<pig>、<sqoop>、<shell> 等。每种动作节点内部都有对应的子元素来描述具体任务参数。例如 <map-reduce> 节点内部必须指定 <job-tracker> 和 <name-node>,还要通过 <configuration> 传入 MapReduce 作业的各项配置。动作节点通过 <ok> 和 <error> 两个子节点声明成功和失败后的跳转目标,这两个子节点都使用 to 属性指向其他节点的名称。
下面是一个最小化示例,它展示了 workflow.xml 的基本骨架。需要注意的是,其中的 <configuration> 通常还需要补充真正的作业参数,这里为了结构清晰做了省略。
<workflow-app xmlns="uri:oozie:workflow:0.5" name="minimal-wf">
<start to="first-job"/>
<action name="first-job">
<map-reduce>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
</map-reduce>
<ok to="end"/>
<error to="fail"/>
</action>
<kill name="fail">
<message>MapReduce 任务执行失败</message>
</kill>
<end name="end"/>
</workflow-app>
在这个骨架中,工作流从 first-job 开始执行。如果 MapReduce 任务成功,就跳转到 end 正常退出;如果失败,则进入 kill 节点并输出失败消息。这个简单的跳转逻辑是所有 Oozie 工作流的基础,更复杂的流程只是在这个结构上增加动作节点和控制节点。
二、用 workflow.xml 定义一个完整 Hadoop 工作流
假设现在有一个经典的单词计数场景:先从 HDFS 读取原始文本,执行 MapReduce 统计词频,再根据统计结果运行一个 Hive 查询生成汇总表。我们需要在 workflow.xml 中依次定义两个动作节点,并用 <ok> 的 to 属性把它们串起来。为了便于维护,集群相关的连接地址都使用 EL 表达式,由 job.properties 提供具体值。
先定义 MapReduce 动作。Oozie 的动作节点要求将任务所需参数写入 <configuration> 内的 <property> 中,这与 Hadoop 配置文件的结构类似。通常需要指定 Mapper 类、Reducer 类、输入目录和输出目录。如果输出目录已经存在,MapReduce 作业会失败,因此可以在动作执行前使用 <prepare> 节点清理旧数据。下面是一个配置示例:
<action name="wordcount-job">
<map-reduce>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<prepare>
<delete path="${nameNode}/user/hadoop/output"/>
</prepare>
<configuration>
<property>
<name>mapred.mapper.class</name>
<value>com.example.WordCountMapper</value>
</property>
<property>
<name>mapred.reducer.class</name>
<value>com.example.WordCountReducer</value>
</property>
<property>
<name>mapred.input.dir</name>
<value>${nameNode}/user/hadoop/input</value>
</property>
<property>
<name>mapred.output.dir</name>
<value>${nameNode}/user/hadoop/output</value>
</property>
</configuration>
</map-reduce>
<ok to="hive-summary"/>
<error to="kill-wordcount"/>
</action>
接下来定义 Hive 动作。Oozie 执行 Hive 任务时通常借助 HiveServer2 或本地 metastore,需要在动作节点中配置 <hive> 子节点,包含 <job-tracker>、<name-node>、<script> 等元素。Hive 查询脚本需要放在 HDFS 上,动作节点通过 <script> 指定脚本路径,并可通过 <param> 传递参数。示例如下:
<action name="hive-summary">
<hive xmlns="uri:oozie:hive-action:0.5">
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<script>${nameNode}/user/hadoop/scripts/summary.sql</script>
<param>output_dir=${nameNode}/user/hadoop/output</param>
</hive>
<ok to="end"/>
<error to="kill-hive"/>
</action>
除了动作节点本身的配置,还需要在 job.properties 中定义 jobTracker、nameNode 等参数。一个常见的 job.properties 文件内容如下:
nameNode=hdfs://namenode:8020
jobTracker=jobtracker:8032
oozie.wf.application.path=${nameNode}/user/hadoop/oozie-apps/wordcount
然后将 workflow.xml 和 lib 目录上传到 HDFS 的对应路径,使用 oozie job 命令提交:
oozie job -oozie http://localhost:11000/oozie -config job.properties -run
这样,一个包含 MapReduce 和 Hive 两个阶段的 Hadoop 工作流就定义完成。Oozie 会根据 workflow.xml 中的跳转关系依次执行任务,任一环节失败则进入对应的 kill 节点并终止后续动作。
三、使用控制流节点处理分支与并行
真实的数据处理流程往往不是简单的线性顺序,可能需要根据上一个任务的输出决定走哪条分支,也可能需要并行执行多个互不依赖的任务。Oozie 提供了 <decision> 节点实现条件分支,以及 <fork> 和 <join> 节点实现并行与汇聚。
<decision> 节点内部通过 <switch> 块定义多个 <case> 条件和一个 <default> 路径。条件表达式使用 Oozie 的 EL 函数,例如可以通过 wf:actionData() 获取前面动作的输出信息,或者通过 fs:dirSize() 判断某个 HDFS 目录的大小。下面示例根据 MapReduce 输出目录的大小决定走大数据分支还是小数据分支:
<decision name="check-output-size">
<switch>
<case to="large-job">${fs:dirSize('/user/hadoop/output') gt 1073741824}</case>
<default to="small-job"/>
</switch>
</decision>
并行场景下,<fork> 节点可以同时启动多个动作,例如一个清洗任务结束后,同时运行统计分析和数据导出任务。每个并行分支结束后都要进入 <join> 节点,只有所有分支都到达 join 节点后,工作流才会继续向下执行。下面是一个结构示意:
<fork name="parallel-start">
<path start="stats-job"/>
<path start="export-job"/>
</fork>
<action name="stats-job">
<map-reduce>...</map-reduce>
<ok to="join-all"/>
<error to="kill-fail"/>
</action>
<action name="export-job">
<sqoop>...</sqoop>
<ok to="join-all"/>
<error to="kill-fail"/>
</action>
<join name="join-all" to="next-step"/>
需要注意,如果 fork 的任一分支失败,整个工作流通常也会被标记为失败,因此要为每个并行分支配置独立的 error 跳转,并在 kill 节点中记录清晰的消息。控制流节点让 workflow.xml 的表达能力大幅提升,可以用声明式方式描述复杂的数据流水线,而不需要在外部脚本中写大量判断逻辑。
四、常见错误与调试建议
使用 Oozie 定义 Hadoop 工作流时,最常遇到的问题往往不是逻辑错误,而是配置层面的细节遗漏。比如 <action> 节点的 <ok> 或 <error> 指向了一个不存在的节点名称,这时候工作流在执行前校验时就会报错,提示找不到 transition 目标。又比如在 <configuration> 中使用了 mapred.input.dir 这样的旧属性,但实际集群运行的是 YARN 时代的 Hadoop,参数名可能需要改为 mapreduce.input.fileinputformat.inputdir。所以编写完 workflow.xml 后,先用 oozie validate workflow.xml 命令做一次静态检查,可以避免很多低级错误。
另一个高频问题是 EL 表达式无法解析。Oozie 在 workflow.xml 中使用 ${nameNode} 这类占位符时,会从 job.properties 中查找对应值。如果变量名拼写不一致,或者没有在 job.properties 中定义,任务提交时就会报 EL_ERROR 或变量未定义错误。建议把所有环境相关参数集中到 job.properties,并在 workflow.xml 中统一使用变量引用。这样切换测试环境和生产环境时,只需修改 properties 文件,不需要改动 XML 定义。
调试执行失败的任务时,优先查看 Oozie 的 job 日志和具体动作的 Hadoop 日志。通过 oozie job -info 任务ID 可以查看工作流的执行状态和当前节点,通过 oozie job -log 任务ID 可以获取更详细的错误堆栈。如果 MapReduce 任务本身失败,需要进入 ResourceManager 或 JobHistoryServer 页面查看 mapper 和 reducer 的异常信息。很多情况下,任务失败是因为依赖的 jar 包没有放到 HDFS 的 lib 目录中,导致运行节点找不到类。将依赖 jar 统一上传到 workflow.xml 所在目录的 lib 子目录,Oozie 会自动分发这些依赖。
最后提醒一点,workflow.xml 只负责定义工作流本身,如果还需要按时间或数据到达条件周期性地触发工作流,就需要配合 Oozie 的 coordinator 使用。coordinator.xml 中可以引用 workflow.xml,并定义数据集、频率和触发条件。对于一次性任务,直接提交 workflow.xml 即可;对于每天定时跑的批处理,建议把 workflow 纳入 coordinator 管理,这样可以在数据到达后自动启动,减少人工干预。
Oozieworkflow.xmlHadoop工作流修改时间:2026-10-05 04:47:02