最近半年多一直在做hive相关的开发工作,并且使用Oozie做为hive工作流的引擎,用于管理Hadoop任务。Oozie的任务流包括:croodinator、workflow。workflow用于描述任务的执行顺序,croodinator用于定义oozie的定时任务。workflow定义了两种结点:
- 控制流结点:主要包括start、end、fork、join等,其中fork、join成对出现,在fork展开。分支,最后在join结点汇聚。
- Action结点:包括Hadoop任务、SSH、HTTP、EMAIL、OOZIE子任务等。
workflow.xml用于配置workflow任务动作,当job的脚本较多,解读起来比较困难,并且出现并发的时,解析就更困难了。近期在做hadoop的旧job的优化,涉及比较多Job,其中大多数,都是其他同事开发的,而workflow的解读又工作过程中不得不面对的繁琐工作。于是闲暇之余,写了一个workflow.xml文件解析工具:输入job的名称,能显示该job的流程图。需要解决的问题主要有两方面 :
- job的workflow.xml文件的读取、解析。
- 结点视图的绘制。
xml文件的解析,使用dom4j包就能轻松解决。workflow文件的可以直接从svn主干上读取。由于本人对基于j2ee的web开发比较熟悉,最终决定用网页展示结点视图。网上找了个html的绘图插件raphael,用于网页上失量图形。编程思路有了,下面是开始代码的实现,整个流程如下:
- svn job代码下载:svn主要的代码不定期的会从其分支合入新代码,需要写一个定时器,每天去全量的同步svn的代码。
- dom4j解析workflow.xml,抽象结点对象,视图数据准备阶段。
- 将视力数据利用Freemarker模板工具,解析到客户端,客户端根据结点数据,绘制结点。
job搜索
为了尽量简化用户的操作,在网而上做了一个搜索job的功能,使用jquery 的autocomplete方法实现:当用户输入job关键字时,模糊搜索svn上的的job,显示配置上的job,简化用户输入。
workflow数据解析
编码的核心工作是第2步,xml文件的解析,使用dom4j很容易就能将workflow.xml解析成结点的链表。每个结点,保存着下一步要执行的结点链表。困难的是,要把链表数据,转化视图展示的坐标,输出到客户端。我们不妨将50px视为一个坐标单位,结点只显示边框,在边框中显示结点名,并且结点与结点之前横向与纵向都间隔一个单位。下面分享我的坐标转化算法:
结点的高度占一个单位,宽度与结点名文本长度相关(5个字符占一个单位)。利用递归算法,从开始结点遍历结点列表,直到结束结点。我们不妨将结点的上一步执行结点叫结点的前结点,下一步执行结点,叫做结点的后结点。
结点横坐标=MAX(前结点的横坐标+结点宽度+1)
结点横坐标=MAX(前结点的横坐标+结点宽度+1)
结点纵坐标=MAX(前结点的纵坐标)
使用递归的算法,当视图中加入新的结点时,可能会引起视图中的结点的前结点、后结点个数的改变,需要重新调整结点的横、纵坐标。
代码实现
核心代码如下:
OozieHelper.java
package com.lxr.oozie.workflow;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.dom4j.Document;
import org.dom4j.Element;
public class OozieHelper {
private Document document;
private Element root = null;
private List<WorkflowNode> workflowNodes = new ArrayList<WorkflowNode>();
private Map<String, WorkflowNode> workflowNodeMap = new HashMap<String, WorkflowNode>();
public OozieHelper(Document document) {
this.document = document;
}
private Element getNode(String tag) {
return root.element(tag);
}
private Element getNode(String attr, String value) {
List<Element> nodes = root.elements();
for (Element el : nodes) {
if (value.equals(el.attributeValue(attr))) {
return el;
}
}
return null;
}
private Element getNodeByName(String name) {
return getNode("name", name);
}
private String getNextNodeName(Element node) {
String tagName = node.getName();
if ("action".equals(tagName)) {
Element element = node.element("ok");
return element.attributeValue("to");
} else {
return node.attributeValue("to");
}
}
private List<Element> getNextNodes(Element node) {
if (null == node) {
return null;
}
List<Element> nextNodes = new ArrayList<Element>();
String nextNodeName = getNextNodeName(node);
if (null != nextNodeName) {
Element nextNode = getNodeByName(nextNodeName);
String tagName = nextNode.getName();
if ("fork".equalsIgnoreCase(tagName)) {
List<Element> elements = nextNode.elements();
for (Element el : elements) {
nextNodeName = el.attributeValue("start");
if (null != nextNodeName) {
nextNode = getNodeByName(nextNodeName);
if (null != nextNode) {
nextNodes.add(nextNode);
}
}
}
} else if ("join".equals(tagName)) {
nextNode = getNodeByName(nextNode.attributeValue("to"));
nextNodes.add(nextNode);
} else {
nextNodes.add(nextNode);
}
}
return nextNodes;
}
private void adjustToAddNode(WorkflowNode workflowNode) {
for (WorkflowNode node : workflowNodes) {
if (node.equals(workflowNode)) {
return;
}
}
workflowNodes.add(workflowNode);
}
private void genNextWorkflowNodes(WorkflowNode parent) {
List<Element> nextNodes = getNextNodes(parent.getElement());
if (0 != nextNodes.size()) {
for (int i = 0, len = nextNodes.size(); i < len; i++) {
Element el = nextNodes.get(i);
String nodeName = el.attributeValue("name");
WorkflowNode subWorkflowNode = workflowNodeMap.get(nodeName);
int subX = parent.getX() + parent.getLength() + 1;
int subY = parent.getY() + 2;
if (null == subWorkflowNode) {
subWorkflowNode = new WorkflowNode(el);
subWorkflowNode.setName(nodeName);
subWorkflowNode.setX(subX);
subWorkflowNode.setY(subY);
genNextWorkflowNodes(subWorkflowNode);
workflowNodes.add(subWorkflowNode);
// adjustToAddNode(subWorkflowNode);
} else {
subWorkflowNode.setX(Math.max(subX, subWorkflowNode.getX()));
if (subY > subWorkflowNode.getY()) {
subWorkflowNode.adjustNextNodesY(subY - subWorkflowNode.getY());
subWorkflowNode.setY(subY);
}
}
subWorkflowNode.previousNodes().add(parent);
parent.nextNodes().add(subWorkflowNode);
workflowNodeMap.put(nodeName, subWorkflowNode);
}
}
}
public List<WorkflowNode> parse() {
root = document.getRootElement();
Element startNode = getNode("start");
if (null != startNode) {
WorkflowNode start = new WorkflowNode(startNode);
start.setName("start");
start.setX(0);
start.setY(0);
workflowNodes.add(start);
genNextWorkflowNodes(start);
} else {
System.out.println("未找到开始结点。");
}
return workflowNodes;
}
}
view-workflow.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Oozie Workflow - ${jobName}</title>
<script type="text/javascript" src="../js/jquery.min.js"></script>
<script type="text/javascript" src="../js/raphael/raphael.min.js"></script>
<script type="text/javascript" src="../js/raphael/raphael.ext.js"></script>
<script type="text/javascript" src="../js/oozie.wf.draw.js"></script>
</head>
<body>
<label style="position: absolute;">job名称:${jobName}</label>
<div class="workflow-holder" id="${jobName}" margin-left="100"
margin-top="100" grid-padding-left="15" grid-padding-top="10"
grid-width="50" grid-height="25">
<#list nodes as node>
<div id="${node.name}" class="branch-node workflow-node"
xx="${node.x}" yy="${node.y}" length="${node.length}"
next-node="${node.nextJobNames!}">${node.name}</div>
</#list>
</div>
</body>
</html>
oozie.wf.draw.js
jQuery.fn.attrn = function(attr) {
return parseInt($(this).attr(attr));
}
HTMLDivElement.prototype.$$ = function(attr) {
return $(this).attr(attr);
}
HTMLDivElement.prototype.$$n = function(attr) {
return parseInt(this.$$(attr));
}
$(function() {
var $holder = $('.workflow-holder');
var $nodes = $holder.find('.workflow-node');
var nodes = [];
$nodes.each(function(i, el) {
nodes[i] = {
id : $(el).attr('id'),
index : i,
xx : el.$$n('xx'),
yy : el.$$n('yy'),
length: el.$$n('length'),
$instance : $(el)
}
});
var workflowCfg = {
id : $holder.attr('id'),
margin : {
left : $holder.attrn('margin-left'),
top : $holder.attrn('margin-top')
},
grid : {
paddingLeft : $holder.attrn('grid-padding-left'),
paddingTop : $holder.attrn('grid-padding-top'),
width : $holder.attrn('grid-width'),
height : $holder.attrn('grid-height')
},
nodes : nodes,
nodesMap : (function() {
var map = {};
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
map[node.$instance.attr('id')] = node;
}
return map;
})()
};
console.log(workflowCfg)
// 用来存储节点的顺序
var connections = [];
// 拖动节点开始时的事件
var dragger = function() {
this.ox = this.attr('x');
this.oy = this.attr('y');
this.animate({
'fill-opacity' : .2
}, 500);
};
// 拖动事件
var move = function(dx, dy) {
var att = {
x : this.ox + dx,
y : this.oy + dy
};
this.attr(att);
$holder.find("#" + this.id).offset({
top : this.oy + dy + workflowCfg.grid.paddingTop,
left : this.ox + dx + workflowCfg.grid.paddingLeft
});
for (var i = connections.length; i--;) {
r.drawArr(connections[i]);
}
};
// 拖动结束后的事件
var up = function() {
this.animate({
'fill-opacity' : 0
}, 500);
};
// 创建绘图对象
var r = Raphael(workflowCfg.id, $(window).width(), $(window).height());
// 绘制节点
var shapes = [];
var maxRight = 0;
for (var i = 0, len = workflowCfg.nodes.length; i < len; i++) {
var node = workflowCfg.nodes[i];
node.left = workflowCfg.margin.left + node.xx * workflowCfg.grid.width;
node.top = workflowCfg.margin.top + node.yy * workflowCfg.grid.height;
node.width = workflowCfg.grid.width * node.length;
node.height = workflowCfg.grid.height;
shapes[i] = r.rect(node.left, node.top, node.width, node.height, 4);
// 定位节点上的文字
node.$instance.offset({
top : node.top + workflowCfg.grid.paddingTop,
left : node.left + workflowCfg.grid.paddingLeft
});
var right = node.$instance.offset().left + node.width;
maxRight = maxRight > right? maxRight : right;
}
var $svg = $holder.find('svg');
var svnWidth = maxRight + workflowCfg.grid.paddingLeft;
var _svnWidth = $svg.attrn('width');
$svg.attr('width', svnWidth > _svnWidth? svnWidth : _svnWidth);
// 为节点添加样式和事件,并且绘制节点之间的箭头
for (var i = 0, ii = shapes.length; i < ii; i++) {
var color = Raphael.getColor();
shapes[i].attr({
fill : color,
stroke : color,
'fill-opacity' : 0,
'stroke-width' : 2,
cursor : 'move'
});
shapes[i].id = workflowCfg.nodes[i].id;
shapes[i].drag(move, dragger, up);
shapes[i].dblclick(function() {
alert(this.id)
})
}
// 节点连线
for (var i = 0; i < workflowCfg.nodes.length; i++) {
var node = workflowCfg.nodes[i];
var nextNodeIds = node.$instance.attr('next-node');
if (nextNodeIds) {
var nextNodeIdArr = nextNodeIds.split(',');
for (var j = 0; j < nextNodeIdArr.length; j++) {
var nextNodeId = nextNodeIdArr[j];
var nextNode = workflowCfg.nodesMap[nextNodeId];
connections.push(r.drawArr({
obj1 : shapes[node.index],
obj2 : shapes[nextNode.index]
}));
}
}
}
});
运行效果如下: