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去執行)