azkaban源码解读
一. web server源代码解析
1.配置文件读取过程:
主要读取的两个配置文件为:
1)读取下面的2个文件
File azkabanPrivatePropsFile =
new File(dir, AZKABAN_PRIVATE_PROPERTIES_FILE);//"azkaban.private.properties"
File azkabanPropsFile = new File(dir, AZKABAN_PROPERTIES_FILE);//"azkaban.properties"
2)组织两个props形成父子关系,azkabanPrivatePropsFile 为父配置,另外一个为子配置,
父子关系如何指定呢?通过属性
private Props _parent;
接下来就立刻读取了2个属性,代码如下:
int maxThreads =
azkabanSettings.getInt("jetty.maxThreads", Constants.DEFAULT_JETTY_MAX_THREAD_COUNT);
boolean isStatsOn =
azkabanSettings.getBoolean("jetty.connector.stats", true);
logger.info("Setting up connector with stats on: " + isStatsOn);
主要是jetty启动的线程数和统计数,这都比较容易,主要的是读取的顺序。
public String get(Object key) {
if (_current.containsKey(key)) {
return _current.get(key);
} else if (_parent != null) {
return _parent.get(key);
} else {
return null;
}
}
先统计子类的信息,如果有了就不用统计父类的配置啦。
PS.azkaban.private.properties是azkaban.properties的父类
2.Servlet引擎初始化
ServletHolder staticServlet = new ServletHolder(new DefaultServlet());
root.addServlet(staticServlet, "/css/*");
root.addServlet(staticServlet, "/js/*");
root.addServlet(staticServlet, "/images/*");
root.addServlet(staticServlet, "/fonts/*");
root.addServlet(staticServlet, "/favicon.ico"); // 静态资源配置路径
root.addServlet(new ServletHolder(new ProjectManagerServlet()), "/manager");
root.addServlet(new ServletHolder(new ExecutorServlet()), "/executor");
root.addServlet(new ServletHolder(new HistoryServlet()), "/history");
root.addServlet(new ServletHolder(new ScheduleServlet()), "/schedule");
root.addServlet(new ServletHolder(new JMXHttpServlet()), "/jmx");
root.addServlet(new ServletHolder(new TriggerManagerServlet()), "/triggers");
root.addServlet(new ServletHolder(new StatsServlet()), "/stats"); // 动态请求配置路径
ServletHolder restliHolder = new ServletHolder(new RestliServlet());
restliHolder.setInitParameter("resourcePackages", "azkaban.restli");
root.addServlet(restliHolder, "/restli/*");
Azkaban 是基于jetty进行发布的。Servlet 是 server applet 的缩写,即服务器运行小程序,而Servlet框架是对HTTP服务器和用户小程序中间层的标准化和抽象形式。
在Jetty中,每个Servlet和其相关信息都由ServletHolder封装。Context代码引擎以及url映射到哪些servlet下,ServletHolder会代理不同servlet的操作。
3.配置session
1)azkaban前端开发Velocity框架配置就不一一介绍了
2)session管理
public SessionCache(Props props) {//直接调用google的jar包
cache = CacheBuilder.newBuilder()
.maximumSize(props.getInt("max.num.sessions", MAX_NUM_SESSIONS))
.expireAfterAccess(
props.getLong("session.time.to.live", SESSION_TIME_TO_LIVE),
TimeUnit.MILLISECONDS)
.build();//
}
}
主要是使用了google的Guava Cache,azkaban web的session是缓存在本地中,如果要将azkaban web做成分布式的,需要将本地缓存改为memcache或redis。
PS.Guava Cache 和 ConcurrentHashMap缓存区别。hashmap需要显示的删除,但是cache不需要,它会自动回收,但是ConcurrentHashMap会有更好的内存效率,具体的源码还没仔细看。
之后,他会将azkaban-user.xml这个xml中的文件加入到缓存中。。
4.azkaban-user.xml中的权限初始化
azkaban是采用用户——角色——组权限三个维度控制权限。其中用户可以创建用户组,给用户组制定权限,这样在该用户组下的所有用户自动拥有该权限。
主要是分析用户xml获取用户的UserTag,RoleTag和GroupRoleTag信息,核心代码如下:
for (int i = 0; i < azkabanUsersList.getLength(); ++i) {//遍历每个节点
Node node = azkabanUsersList.item(i);//获取当前节点
if (node.getNodeType() == Node.ELEMENT_NODE) {//节点类型是否是我们需要的?
if (node.getNodeName().equals(USER_TAG)) {//用户节点
parseUserTag(node, users, userPassword, proxyUserMap);
} else if (node.getNodeName().equals(ROLE_TAG)) {
parseRoleTag(node, roles);
} else if (node.getNodeName().equals(GROUP_TAG)) {
parseGroupRoleTag(node, groupRoles);
}
}
}
eg:
<azkaban-users>
<user username="admin" password="admin" roles="admin" groups="admin" />
<user username="zhangsan" password="zhangsan" groups="group_user" />
<user username="lisi" password="lisi" groups="group_user" />
<user username="metrics" password="metrics" roles="metrics"/>
<user username="azkaban" password="azkaban" groups="group_inspector"/>
<group name="group_user" roles="user" />
<group name="group_inspector" roles="inspector" />
<role name="admin" permissions="ADMIN" />
<role name="metrics" permissions="METRICS"/>
<role name="user" permissions="READ,WRITE,EXECUTE,SCHEDULE,CREATEPROJECTS"/>
<role name="inspector" permissions="READ"/>
<role name="write" permissions="WRITE"/>
<role name="execute" permissions="EXECUTE"/>
<role name="schedule" permissions="SCHEDULE"/>
<role name="createprojects" permissions="CREATEPROJECTS"/>
</azkaban-users>
admin用户拥有超级管理员权限,可以给其他用户赋权限。
在group_user用户组下的用户,拥有使用azkaban的权限,可以创建project,读写执行,调度。
在group_inspector用户组下的用户,拥有审查员权限,只能读。也就是只能看project项目,flow,看执行日志,但是不能更改。
当然如果希望可以使用已经存在的用户系统,也可以实现azkaban UserManager的接口。去配置相应用户。。
public interface UserManager {
public User getUser(String username, String password) throws UserManagerException;
public boolean validateUser(String username);
public boolean validateGroup(String group);
public Role getRole(String roleName);
public boolean validateProxyUser(String proxyUser, User realUser);
}
5.Jdbc初始化过程:
主要的源码位于JdbcExecutorLoader 下,其底层内部实现主要为dbcp的连接池
6.创建项目,工作流
首先,上传zip文件;项目和工作流的所有信息都是存储在mysql数据库中的。
其中在上传工作流时,azkaban会对上传的zip文件进行解压缩,然后分析成各个节点组成的DAG图。
工作流的步骤:
// Load all the props files and create the Node objects
loadProjectFromDir(baseDirectory.getPath(), baseDirectory, null);
jobPropertiesCheck(project);
// Create edges and find missing dependencies
resolveDependencies();
// Create the flows.
buildFlowsFromDependencies();
// Resolve embedded flows
resolveEmbeddedFlows();
1)loadProjectFromDir --- 从目录中加载Flow的定义
# foo.job 文件内容
type=command
command=echo foo
# bar.job 文件内容
type=command
dependencies=foo
command=echo bar
private void loadProjectFromDir(String base, File dir, Props parent) {
File[] propertyFiles = dir.listFiles(new SuffixFilter(PROPERTY_SUFFIX));
Arrays.sort(propertyFiles);
// zip文件中的配置文件信息
for (File file : propertyFiles) {
String relative = getRelativeFilePath(base, file.getPath());
try {
parent = new Props(parent, file);
parent.setSource(relative);
FlowProps flowProps = new FlowProps(parent);
flowPropsList.add(flowProps);
} catch (IOException e) {
errors.add("Error loading properties " + file.getName() + ":"
+ e.getMessage());
}
logger.info("Adding " + relative);
propsList.add(parent);
}
// 加载所有的.job文件信息,如果有重复的就不加载了。
File[] jobFiles = dir.listFiles(new SuffixFilter(JOB_SUFFIX));
for (File file : jobFiles) {
String jobName = getNameWithoutExtension(file);
try {
if (!duplicateJobs.contains(jobName)) {
if (jobPropsMap.containsKey(jobName)) {
errors.add("Duplicate job names found '" + jobName + "'.");
duplicateJobs.add(jobName);
jobPropsMap.remove(jobName);
nodeMap.remove(jobName);
} else {
// 将job的配置信息和开始加载,parent开始为null
Props prop = new Props(parent, file);
// 截取字符串
String relative = getRelativeFilePath(base, file.getPath());
prop.setSource(relative); // 截取后的job名类似hello.job
// 构造了一个节点
Node node = new Node(jobName);
String type = prop.getString("type", null);
if (type == null) {
errors.add("Job doesn't have type set '" + jobName + "'.");
}
node.setType(type);
node.setJobSource(relative);
if (parent != null) {
node.setPropsSource(parent.getSource());
}
// 如果是root节点
if (prop.getBoolean(CommonJobProperties.ROOT_NODE, false)) {
rootNodes.add(jobName);
}
jobPropsMap.put(jobName, prop);
nodeMap.put(jobName, node);
}
}
} catch (IOException e) {
errors.add("Error loading job file " + file.getName() + ":"
+ e.getMessage());
}
}
//如果有子文件夹,同样的加载,说明支持多层次的文件夹
File[] subDirs = dir.listFiles(DIR_FILTER);
for (File file : subDirs) {
loadProjectFromDir(base, file, parent);
}
}
2).jobPropertiesCheck
检查校验一些参数的合法性
3).resolveDependencies
创建节点直接的关联关系。代码就不一条条的截取了,主要是依照1中生成的map生成一套HashMap> nodeDependencies;依赖关联的map。
比如,我们现在有两个任务 a.job 和 b.job。b依赖于a。则在最终的节点中,存储的是:
HashMap<
// 当前节点信息
String, ---b
Map<
// 父节点信息
String, ---a
// 边信息
Edge ---
>
>
4).buildFlowsFromDependencies
这个方法的主要功能就是依据依赖关系,创建工作流,修改数据库记录这些信息。
a.先是查询出当前最大的版本号,然后+1赋值。
b.上传工作流等相关的文件到数据库中,10M为1个单位,到project_files表中。
c.然后修改project_flows,这张表主要记录了依据.job文件生成的工作流拓扑图。
5).resolveEmbeddedFlows
递归判断每个依赖,看是否有环出现或者工作流中的依赖是否存在。
private void resolveEmbeddedFlow(String flowId, Set<String> visited) {
Set<String> embeddedFlow = flowDependencies.get(flowId);
if (embeddedFlow == null) {
return;
}
visited.add(flowId);
for (String embeddedFlowId : embeddedFlow) {
if (visited.contains(embeddedFlowId)) {
errors.add("Embedded flow cycle found in " + flowId + "->"
+ embeddedFlowId);
return;
} else if (!flowMap.containsKey(embeddedFlowId)) {
errors.add("Flow " + flowId + " depends on " + embeddedFlowId
+ " but can't be found.");
return;
} else {
resolveEmbeddedFlow(embeddedFlowId, visited);
}
}
visited.remove(flowId);
}
7.提交工作流
在web server中提交一个任务时,如果没有指定要提交的executor则会由选择器进行选择,依照executor本身的hashcode来计算提交到哪。。。
之后再azkaban.executor.ExecutorManager 中
// 构造http client
ExecutorApiClient apiclient = ExecutorApiClient.getInstance();
@SuppressWarnings("unchecked")
// 构造URI
URI uri = ExecutorApiClient.buildUri(host, port, path, true, paramList.toArray(new Pair[0]));
return apiclient.httpGet(uri, null);
这其中构造了一个类似: uri = "http://x.x.x.x:port/executor?action=execute&execid=12&user"
这种的url跳转到executor中执行任务。
二. executor代码:
每个节点的结构如下:
InNode InNode InNode InNode InNode
Node
OutNode OutNode OutNode OutNode
Node是当前节点,InNodes是父节点,OutNodes是子节点
1.接收任务
azkaban的web是作为分发者存在的,web分发GET请求任务到executor Server中
private void handleAjaxExecute(HttpServletRequest req,
Map<String, Object> respMap, int execId) throws ServletException {
try {
// 提交到本地的manager来处理
flowRunnerManager.submitFlow(execId);
} catch (ExecutorManagerException e) {
e.printStackTrace();
logger.error(e.getMessage(), e);
respMap.put(RESPONSE_ERROR, e.getMessage());
}
}
这里主要的处理逻辑是,从数据库中获取project的详情,然后从project_files中拿到上传的压缩文件,解压到本地的projects文件夹中。
最终提交任务到线程池中:
Future future = executorService.submit(runner);
线城池是按照工作流配置参数"flow.num.job.threads",来设置线程数的。
2.Execute Server的任务真正执行过程
1)计算当前拓扑图的开始节点
public List<String> getStartNodes() {
if (startNodes == null) {
startNodes = new ArrayList<String>();
for (ExecutableNode node : executableNodes.values()) {
if (node.getInNodes().isEmpty()) {
startNodes.add(node.getId());
}
}
}
return startNodes;
}
2) 运行的基本配置
runReadyJob :
private boolean runReadyJob(ExecutableNode node) throws IOException {
if (Status.isStatusFinished(node.getStatus())
|| Status.isStatusRunning(node.getStatus())) {
return false;
}
// 判断这个当前节点的下个节点是否需要执行,
Status nextNodeStatus = getImpliedStatus(node);
if (nextNodeStatus == null) {
return false;
}
....
}
getImpliedStatus:判断当前节点是否可执行,当该节点的父类节点的状态都是FINISHED并且不是Failed和KILLED,就返回可执行。
ExecutableFlowBase flow = node.getParentFlow();
boolean shouldKill = false;
for (String dependency : node.getInNodes()) {
ExecutableNode dependencyNode = flow.getExecutableNode(dependency);
Status depStatus = dependencyNode.getStatus();
if (!Status.isStatusFinished(depStatus)) {
return null;
} else if (depStatus == Status.FAILED || depStatus == Status.CANCELLED
|| depStatus == Status.KILLED) {
// We propagate failures as KILLED states.
shouldKill = true;
}
}
3)执行每一个节点
for (String startNodeId : ((ExecutableFlowBase) node).getStartNodes()) {
ExecutableNode startNode = flow.getExecutableNode(startNodeId);
runReadyJob(startNode);
}
......
private void runExecutableNode(ExecutableNode node) throws IOException {
// Collect output props from the job's dependencies.
prepareJobProperties(node);
node.setStatus(Status.QUEUED);
JobRunner runner = createJobRunner(node);
logger.info("Submitting job '" + node.getNestedId() + "' to run.");
try {
executorService.submit(runner);
activeJobRunners.add(runner);
} catch (RejectedExecutionException e) {
logger.error(e);
}
}
从没有父类InNodes的开始节点开始,每次按照2中的标准执行其OutNodes子节点。执行的本质是将每个job放置在线程池中执行,封装的对象是jobRunner。
4)jobRunner listener详解
在执行前。会有一堆校验和预处理操作。。。
其中,最重要的是加入了一个监听器。
4.1 JobRunnerEventListener: azkaban.execapp.FlowRunner
这块的代码主要是用作修改工作流的节点的状态信息,当修改后会产生一个Event,这个事件记录了节点修改后的状态。主要源码:
if (quickFinish) {
node.setStartTime(time);
fireEvent(Event.create(this, Type.JOB_STARTED, new EventData(nodeStatus)));
node.setEndTime(time);
fireEvent(Event.create(this, Type.JOB_FINISHED, new EventData(nodeStatus)));
return true;
}
4.2 JobCallbackManager : 这个listener主要监听结束状态的节点信息,修改任务最重结束的状态,成功or失败or killed..
4.3JmxJobMBeanManager: 这个主要是记录了当前环境下的所有任务执行状态的统计,比如:当当前任务执行成功后,总执行任务+1 ,成功执行任务+1。以此类推。
5) JobRunner中构造Job
5.1首先通过job type选择不同的任务类型:
private void loadDefaultTypes(JobTypePluginSet plugins)
throws JobTypeManagerException {
logger.info("Loading plugin default job types");
plugins.addPluginClass("command", ProcessJob.class);
plugins.addPluginClass("javaprocess", JavaProcessJob.class);
plugins.addPluginClass("noop", NoopJob.class);
plugins.addPluginClass("python", PythonJob.class);
plugins.addPluginClass("ruby", RubyJob.class);
plugins.addPluginClass("script", ScriptJob.class);
}
5.2 依照不同类型去创建任务:(具体的代码比较多,就不粘贴了,在azkaban.jobtype.JobTypeManager),主要的思想是通过反射构造。
job = (Job) Utils.callConstructor(executorClass, jobId, pluginLoadProps, jobProps, logger);
6). 在jobRunner中构造好了job接着就需要执行了。job.
6.1 执行一个节点
在这我们用command任务做说明:
ProcessJob.run():
// 可以传入多条command
for (String command : commands) {
// 申请一条进行去处理
AzkabanProcessBuilder builder = null;
if (isExecuteAsUser) {
command =
String.format("%s %s %s", executeAsUserBinaryPath, effectiveUser,
command);
info("Command: " + command);
builder =
new AzkabanProcessBuilder(partitionCommandLine(command))
.setEnv(envVars).setWorkingDir(getCwd()).setLogger(getLog())
.enableExecuteAsUser().setExecuteAsUserBinaryPath(executeAsUserBinaryPath)
.setEffectiveUser(effectiveUser);
} else {
info("Command: " + command);
builder =
new AzkabanProcessBuilder(partitionCommandLine(command))
.setEnv(envVars).setWorkingDir(getCwd()).setLogger(getLog());
}
// 设置env
if (builder.getEnv().size() > 0) {
info("Environment variables: " + builder.getEnv());
}
info("Working directory: " + builder.getWorkingDir());
// print out the Job properties to the job log.
this.logJobProperties();
boolean success = false;
this.process = builder.build();
try {
// 执行这个进程
this.process.run();
success = true;
} catch (Throwable e) {
for (File file : propFiles)
if (file != null && file.exists())
file.delete();
throw new RuntimeException(e);
} finally {
this.process = null;
info("Process completed "
+ (success ? "successfully" : "unsuccessfully") + " in "
+ ((System.currentTimeMillis() - startMs) / 1000) + " seconds.");
}
}
从上述代码中可以看出,azkaban在执行command类型任务时,都是在系统环境下生成一个process去处理每一行的command。
eg:
type=command
command=sleep 1
command.1=echo "start execute"
.job文件内容为command; command.1 这两条不同的任务执行会生成两条不同的进程,两个进程间无法通信获取内容及结果。
6.2触发子节点执行:
在flowRunner中,会判断当期工作流是否结束,不结束判断下一个执行节点
while (!flowFinished) {
synchronized (mainSyncObj) {
if (flowPaused) {
try {
mainSyncObj.wait(CHECK_WAIT_MS);
} catch (InterruptedException e) {
}
continue;
} else {
if (retryFailedJobs) {
retryAllFailures();
// 判断下一个执行节点
} else if (!progressGraph()) {
try {
mainSyncObj.wait(CHECK_WAIT_MS);
} catch (InterruptedException e) {
}
}
}
}
}
progressGraph 这个关键的方法代码如下:
private boolean progressGraph() throws IOException {
finishedNodes.swap();
// 当这个节点结束后,去获取outNodes,outNodes是图中,当前节点的下一个
HashSet<ExecutableNode> nodesToCheck = new HashSet<ExecutableNode>();
for (ExecutableNode node : finishedNodes) {
Set<String> outNodeIds = node.getOutNodes();
ExecutableFlowBase parentFlow = node.getParentFlow();
// 如果任务失败了,那设置工作流失败
if (node.getStatus() == Status.FAILED) {
// The job cannot be retried or has run out of retry attempts. We will
// fail the job and its flow now.
if (!retryJobIfPossible(node)) {
propagateStatus(node.getParentFlow(), Status.FAILED_FINISHING);
if (failureAction == FailureAction.CANCEL_ALL) {
this.kill();
}
this.flowFailed = true;
} else {
nodesToCheck.add(node);
continue;
}
}
// 如果没有后续节点则工作流over了
if (outNodeIds.isEmpty()) {
finalizeFlow(parentFlow);
finishExecutableNode(parentFlow);
// If the parent has a parent, then we process
if (!(parentFlow instanceof ExecutableFlow)) {
outNodeIds = parentFlow.getOutNodes();
parentFlow = parentFlow.getParentFlow();
}
}
// 如果有后续节点,加入list
for (String nodeId : outNodeIds) {
ExecutableNode outNode = parentFlow.getExecutableNode(nodeId);
nodesToCheck.add(outNode);
}
}
// 先看是否有skip或者kill的任务(用户可以设置哪些节点不执行),若没有则继续执行后续节点
boolean jobsRun = false;
for (ExecutableNode node : nodesToCheck) {
if (Status.isStatusFinished(node.getStatus())
|| Status.isStatusRunning(node.getStatus())) {
// Really shouldn't get in here.
continue;
}
jobsRun |= runReadyJob(node);
}
if (jobsRun || finishedNodes.getSize() > 0) {
updateFlow();
return true;
}
return false;
}
当这些都执行结束后:
logger.info("Finishing up flow. Awaiting Termination");
executorService.shutdown();
updateFlow();
logger.info("Finished Flow");
关闭线程池,更新数据库
总而言之,一个executorService对应着一个工作流的执行信息。每条现成会去执行节点(开辟系统的process去执行)