azkaban源碼解讀

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章