基於Mesos的作業雲 Elastic-Job-Cloud 原始碼分析 —— 作業排程(一)

芋道原始碼_以德服人_不服就幹發表於2017-09-06

本文基於 Elastic-Job V2.1.5 版本分享
Elastic-Job-Cloud 原始碼分析系列(6篇)傳送門


???關注微信公眾號:【芋道原始碼】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有原始碼分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文註釋原始碼 GitHub 地址
  3. 您對於原始碼的疑問每條留言將得到認真回覆。甚至不知道如何讀原始碼也可以請教噢
  4. 新的原始碼解析文章實時收到通知。每週更新一篇左右
  5. 認真的原始碼交流微信群。

1. 概述

本文主要分享 Elastic-Job-Cloud 排程主流程。對應到 Elastic-Job-Lite 原始碼解析文章如下:

如果你閱讀過以下文章,有助於對本文的理解:

? 另外,筆者假設你已經對 《Elastic-Job-Lite 原始碼分析系列》 有一定的瞭解。

本文涉及到主體類的類圖如下( 開啟大圖 ):

你行好事會因為得到讚賞而愉悅
同理,開源專案貢獻者會因為 Star 而更加有動力
為 Elastic-Job 點贊!傳送門

Elastic-Job-Cloud 基於 Mesos 實現分散式作業排程,或者說 Elastic-Job-Cloud 是 Mesos 上的 框架( Framework )。

一個 Mesos 框架由兩部分組成:

  • 控制器部分,稱為排程器( Scheduler )。
  • 工作單元部分,稱為執行器( Executor )。

Elastic-Job-Cloud 由兩個專案組成:

  • Elastic-Job-Cloud-Scheduler,實現排程器,實現類為 com.dangdang.ddframe.job.cloud.scheduler.mesos.SchedulerEngine
  • Elastic-Job-Cloud-Executor,實現執行器,實現類為 com.dangdang.ddframe.job.cloud.executor.TaskExecutor

本文略微“囉嗦”,請保持耐心。搭配《用Mesos框架構建分散式應用》一起閱讀,理解難度降低 99%。OK,開始我們的 Cloud 之旅。

2. 作業執行型別

在 Elastic-Job-Cloud,作業執行分成兩種型別:

  • 常駐作業

常駐作業是作業一旦啟動,無論執行與否均佔用系統資源;
常駐作業適合初始化時間長、觸發間隔短、實時性要求高的作業,要求資源配備充足。

  • 瞬時作業

瞬時作業是在作業啟動時佔用資源,執行完成後釋放資源。
瞬時作業適合初始化時間短、觸發間隔長、允許延遲的作業,一般用於資源不太充分,或作業要求的資源多,適合資源錯峰使用的場景。

Elastic-Job-Cloud 不同於 Elastic-Job-Lite 去中心化執行排程,轉變為 Mesos Framework 的中心節點排程。這裡不太理解,沒關係,下文看到具體程式碼就能明白了。

常駐作業、瞬時作業在排程中會略有不同,大體粗略流程如下:

下面,我們針對每個過程一節一節解析。

3. Producer 釋出任務

在上文《Elastic-Job-Cloud 原始碼分析 —— 作業配置》的「3.1.1 操作雲作業配置」可以看到新增雲作業配置後,Elastic-Job-Cloud-Scheduler 會執行作業排程,實現程式碼如下:

// ProducerManager.java
/**
* 排程作業.
* 
* @param jobConfig 作業配置
*/
public void schedule(final CloudJobConfiguration jobConfig) {
   // 應用 或 作業 被禁用,不排程
   if (disableAppService.isDisabled(jobConfig.getAppName()) || disableJobService.isDisabled(jobConfig.getJobName())) {
       return;
   }
   if (CloudJobExecutionType.TRANSIENT == jobConfig.getJobExecutionType()) { // 瞬時作業
       transientProducerScheduler.register(jobConfig);
   } else if (CloudJobExecutionType.DAEMON == jobConfig.getJobExecutionType()) { // 常駐作業
       readyService.addDaemon(jobConfig.getJobName());
   }
}複製程式碼
  • 瞬時作業和常駐作業在排程上會有一定的不同。

3.1 常駐作業

常駐作業在排程時,直接新增到待執行作業佇列。What?豈不是馬上就執行了!No No No,答案在「5. TaskExecutor 執行任務」,這裡先打住。

// ReadyService.java
/**
* 將常駐作業放入待執行佇列.
*
* @param jobName 作業名稱
*/
public void addDaemon(final String jobName) {
   if (regCenter.getNumChildren(ReadyNode.ROOT) > env.getFrameworkConfiguration().getJobStateQueueSize()) {
       log.warn("Cannot add daemon job, caused by read state queue size is larger than {}.", env.getFrameworkConfiguration().getJobStateQueueSize());
       return;
   }
   Optional<CloudJobConfiguration> cloudJobConfig = configService.load(jobName);
   if (!cloudJobConfig.isPresent() || CloudJobExecutionType.DAEMON != cloudJobConfig.get().getJobExecutionType() || runningService.isJobRunning(jobName)) {
       return;
   }
   // 新增到待執行佇列
   regCenter.persist(ReadyNode.getReadyJobNodePath(jobName), "1");
}

// ReadyNode.java
final class ReadyNode {

    static final String ROOT = StateNode.ROOT + "/ready";

    private static final String READY_JOB = ROOT + "/%s"; // %s = ${JOB_NAME}
}複製程式碼
  • ReadyService,待執行作業佇列服務,提供對待執行作業佇列的各種操作方法。
  • 待執行作業佇列儲存在註冊中心( Zookeeper )的持久資料節點 /${NAMESPACE}/state/ready/${JOB_NAME},儲存值為待執行次數。例如此處,待執行次數為 1。使用 zkClient 檢視如下:

      [zk: localhost:2181(CONNECTED) 4] ls /elastic-job-cloud/state/ready
      [test_job_simple]
      [zk: localhost:2181(CONNECTED) 5] get /elastic-job-cloud/state/ready/test_job_simple
      1複製程式碼
  • 在運維平臺,我們可以看到待執行作業佇列:

  • 從官方的 RoadMap 來看,待執行作業佇列未來會使用 Redis 儲存以提高效能。

    FROM elasticjob.io/docs/elasti…
    Redis Based Queue Improvement

3.2 瞬時作業

瞬時作業在排程時,使用釋出瞬時作業任務的排程器( TransientProducerScheduler )排程作業。當瞬時作業到達作業執行時間,新增到待執行作業佇列。

3.2.1 TransientProducerScheduler

TransientProducerScheduler,釋出瞬時作業任務的排程器,基於 Quartz 實現對瞬時作業的排程。初始化程式碼如下:

// TransientProducerScheduler.java
void start() {
   scheduler = getScheduler();
   try {
       scheduler.start();
   } catch (final SchedulerException ex) {
       throw new JobSystemException(ex);
   }
}

private Scheduler getScheduler() {
   StdSchedulerFactory factory = new StdSchedulerFactory();
   try {
       factory.initialize(getQuartzProperties());
       return factory.getScheduler();
   } catch (final SchedulerException ex) {
       throw new JobSystemException(ex);
   }
}

private Properties getQuartzProperties() {
   Properties result = new Properties();
   result.put("org.quartz.threadPool.class", SimpleThreadPool.class.getName());
   result.put("org.quartz.threadPool.threadCount", Integer.toString(Runtime.getRuntime().availableProcessors() * 2)); // 執行緒池數量
   result.put("org.quartz.scheduler.instanceName", "ELASTIC_JOB_CLOUD_TRANSIENT_PRODUCER");
   result.put("org.quartz.plugin.shutdownhook.class", ShutdownHookPlugin.class.getName());
   result.put("org.quartz.plugin.shutdownhook.cleanShutdown", Boolean.TRUE.toString());
   return result;
}複製程式碼

3.2.2 註冊瞬時作業

呼叫 TransientProducerScheduler#register(...) 方法,註冊瞬時作業。實現程式碼如下:

// TransientProducerScheduler.java
private final TransientProducerRepository repository;

synchronized void register(final CloudJobConfiguration jobConfig) {
   String cron = jobConfig.getTypeConfig().getCoreConfig().getCron();
   // 新增 cron 作業集合
   JobKey jobKey = buildJobKey(cron);
   repository.put(jobKey, jobConfig.getJobName());
   // 排程 作業
   try {
       if (!scheduler.checkExists(jobKey)) {
           scheduler.scheduleJob(buildJobDetail(jobKey), buildTrigger(jobKey.getName()));
       }
   } catch (final SchedulerException ex) {
       throw new JobSystemException(ex);
   }
}複製程式碼
  • 呼叫 #buildJobKey(...) 方法,建立 Quartz JobKey。你會發現很有意思的使用的是 cron 引數作為主鍵。Why?在看下 !scheduler.checkExists(jobKey) 處,相同 JobKey( cron ) 的作業不重複註冊到 Quartz Scheduler。Why?此處是一個優化,相同 cron 使用同一個 Quartz Job,Elastic-Job-Cloud-Scheduler 可能會註冊大量的瞬時作業,如果一個瞬時作業建立一個 Quartz Job 太過浪費,特別是 cron 每分鐘、每5分鐘、每小時、每天已經覆蓋了大量的瞬時作業的情況。因此,相同 cron 使用同一個 Quartz Job。
  • 呼叫 TransientProducerRepository#put(...) 以 Quartz JobKey 為主鍵聚合作業。

      final class TransientProducerRepository {
    
          /**
           * cron 作業集合
           * key:作業Key
           */
          private final ConcurrentHashMap<JobKey, List<String>> cronTasks = new ConcurrentHashMap<>(256, 1);
    
          synchronized void put(final JobKey jobKey, final String jobName) {
              remove(jobName);
              List<String> taskList = cronTasks.get(jobKey);
              if (null == taskList) {
                  taskList = new CopyOnWriteArrayList<>();
                  taskList.add(jobName);
                  cronTasks.put(jobKey, taskList);
                  return;
              }
              if (!taskList.contains(jobName)) {
                  taskList.add(jobName);
              }
          }
      }複製程式碼
  • 呼叫 #buildJobDetail(...) 建立 Quartz Job 資訊。實現程式碼如下:

      private JobDetail buildJobDetail(final JobKey jobKey) {
          JobDetail result = JobBuilder.newJob(ProducerJob.class) // ProducerJob.java
                  .withIdentity(jobKey).build();
          result.getJobDataMap().put("repository", repository);
          result.getJobDataMap().put("readyService", readyService);
          return result;
      }複製程式碼
    • JobBuilder#newJob(...) 的引數是 ProducerJob,下文會講解到。
  • 呼叫 #buildTrigger(...) 建立 Quartz Trigger。實現程式碼如下:

      private Trigger buildTrigger(final String cron) {
         return TriggerBuilder.newTrigger()
                 .withIdentity(cron)
                 .withSchedule(CronScheduleBuilder.cronSchedule(cron) // cron
                 .withMisfireHandlingInstructionDoNothing())
                 .build();
      }複製程式碼

3.2.3 ProducerJob

ProducerJob,當 Quartz Job 到達 cron 執行時間( 即作業執行時間),將相應的瞬時作業新增到待執行作業佇列。實現程式碼如下:

public static final class ProducerJob implements Job {

   private TransientProducerRepository repository;

   private ReadyService readyService;

   @Override
   public void execute(final JobExecutionContext context) throws JobExecutionException {
       List<String> jobNames = repository.get(context.getJobDetail().getKey());
       for (String each : jobNames) {
           readyService.addTransient(each);
       }
   }
}複製程式碼
  • 呼叫 TransientProducerRepository#get(...) 方法,獲得該 Job 對應的作業集合。實現程式碼如下:

      final class TransientProducerRepository {
    
          /**
           * cron 作業集合
           * key:作業Key
           */
          private final ConcurrentHashMap<JobKey, List<String>> cronTasks = new ConcurrentHashMap<>(256, 1);
    
          List<String> get(final JobKey jobKey) {
              List<String> result = cronTasks.get(jobKey);
              return null == result ? Collections.<String>emptyList() : result;
          }
      }複製程式碼
  • 呼叫 ReadyService#addTransient(...) 方法,新增瞬時作業到待執行作業佇列。實現程式碼如下:

      /**
      * 將瞬時作業放入待執行佇列.
      * 
      * @param jobName 作業名稱
      */
      public void addTransient(final String jobName) {
         //
         if (regCenter.getNumChildren(ReadyNode.ROOT) > env.getFrameworkConfiguration().getJobStateQueueSize()) {
             log.warn("Cannot add transient job, caused by read state queue size is larger than {}.", env.getFrameworkConfiguration().getJobStateQueueSize());
             return;
         }
         //
         Optional<CloudJobConfiguration> cloudJobConfig = configService.load(jobName);
         if (!cloudJobConfig.isPresent() || CloudJobExecutionType.TRANSIENT != cloudJobConfig.get().getJobExecutionType()) {
             return;
         }
         // 
         String readyJobNode = ReadyNode.getReadyJobNodePath(jobName);
         String times = regCenter.getDirectly(readyJobNode);
         if (cloudJobConfig.get().getTypeConfig().getCoreConfig().isMisfire()) {
             regCenter.persist(readyJobNode, Integer.toString(null == times ? 1 : Integer.parseInt(times) + 1));
         } else {
             regCenter.persist(ReadyNode.getReadyJobNodePath(jobName), "1");
         }
      }複製程式碼
    • 新增瞬時作業到待執行作業佇列新增常駐作業到待執行作業佇列基本是一致的。
    • TODO :misfire

3.3 小結

無論是常駐作業還是瞬時作業,都會加入到待執行作業佇列。目前我們看到瞬時作業的每次排程是 TransientProducerScheduler 負責。那麼常駐作業的每次排程呢?「5. TaskExecutor 執行任務」會看到它的排程,這是 Elastic-Job-Cloud 設計巧妙有趣的地方。

4. TaskLaunchScheduledService 提交任務

TaskLaunchScheduledService,任務提交排程服務。它繼承 Guava AbstractScheduledService 實現定時將待執行作業佇列的作業提交到 Mesos 進行排程執行。實現定時程式碼如下:

public final class TaskLaunchScheduledService extends AbstractScheduledService {

    @Override
    protected String serviceName() {
        return "task-launch-processor";
    }

    @Override
    protected Scheduler scheduler() {
        return Scheduler.newFixedDelaySchedule(2, 10, TimeUnit.SECONDS);
    }

    @Override
    protected void runOneIteration() throws Exception {
        // .... 省略程式碼
    }

    // ... 省略部分方法
}複製程式碼
  • 每 10 秒執行提交任務( #runOneIteration() )。對 Guava AbstractScheduledService 不瞭解的同學,可以閱讀完本文後 Google 下。

#runOneIteration() 方法相對比較複雜,我們一塊一塊拆解,耐心理解。實現程式碼如下:

@Override
protected void runOneIteration() throws Exception {
   try {
       System.out.println("runOneIteration:" + new Date());
       // 建立 Fenzo 任務請求
       LaunchingTasks launchingTasks = new LaunchingTasks(facadeService.getEligibleJobContext());
       List<TaskRequest> taskRequests = launchingTasks.getPendingTasks();
       // 獲取所有正在執行的雲作業App https://github.com/Netflix/Fenzo/wiki/Constraints
       if (!taskRequests.isEmpty()) {
           AppConstraintEvaluator.getInstance().loadAppRunningState();
       }
       // 將任務請求分配到 Mesos Offer
       Collection<VMAssignmentResult> vmAssignmentResults = taskScheduler.scheduleOnce(taskRequests, LeasesQueue.getInstance().drainTo()).getResultMap().values();
       // 建立 Mesos 任務請求
       List<TaskContext> taskContextsList = new LinkedList<>(); // 任務執行時上下文集合
       Map<List<Protos.OfferID>, List<Protos.TaskInfo>> offerIdTaskInfoMap = new HashMap<>(); // Mesos 任務資訊集合
       for (VMAssignmentResult each: vmAssignmentResults) {
           List<VirtualMachineLease> leasesUsed = each.getLeasesUsed();
           List<Protos.TaskInfo> taskInfoList = new ArrayList<>(each.getTasksAssigned().size() * 10);
           taskInfoList.addAll(getTaskInfoList(
                   launchingTasks.getIntegrityViolationJobs(vmAssignmentResults), // 獲得作業分片不完整的作業集合
                   each, leasesUsed.get(0).hostname(), leasesUsed.get(0).getOffer()));
           for (Protos.TaskInfo taskInfo : taskInfoList) {
               taskContextsList.add(TaskContext.from(taskInfo.getTaskId().getValue()));
           }
           offerIdTaskInfoMap.put(getOfferIDs(leasesUsed), // 獲得 Offer ID 集合
                   taskInfoList);
       }
       // 遍歷任務執行時上下文
       for (TaskContext each : taskContextsList) {
           // 將任務執行時上下文放入執行時佇列
           facadeService.addRunning(each);
           // 釋出作業狀態追蹤事件(State.TASK_STAGING)
           jobEventBus.post(createJobStatusTraceEvent(each));
       }
       // 從佇列中刪除已執行的作業
       facadeService.removeLaunchTasksFromQueue(taskContextsList);
       // 提交任務給 Mesos
       for (Entry<List<OfferID>, List<TaskInfo>> each : offerIdTaskInfoMap.entrySet()) {
           schedulerDriver.launchTasks(each.getKey(), each.getValue());
       }
   } catch (Throwable throwable) {
       log.error("Launch task error", throwable);
   } finally {
       // 清理 AppConstraintEvaluator 所有正在執行的雲作業App
       AppConstraintEvaluator.getInstance().clearAppRunningState();
   }
}複製程式碼

4.1 建立 Fenzo 任務請求

// #runOneIteration()
LaunchingTasks launchingTasks = new LaunchingTasks(facadeService.getEligibleJobContext());
List<TaskRequest> taskRequests = launchingTasks.getPendingTasks();複製程式碼
  • 呼叫 FacadeService#getEligibleJobContext() 方法,獲取有資格執行的作業。

      // FacadeService.java
      /**
      * 獲取有資格執行的作業.
      * 
      * @return 作業上下文集合
      */
      public Collection<JobContext> getEligibleJobContext() {
         // 從失效轉移佇列中獲取所有有資格執行的作業上下文
         Collection<JobContext> failoverJobContexts = failoverService.getAllEligibleJobContexts();
         // 從待執行佇列中獲取所有有資格執行的作業上下文
         Collection<JobContext> readyJobContexts = readyService.getAllEligibleJobContexts(failoverJobContexts);
         // 合併
         Collection<JobContext> result = new ArrayList<>(failoverJobContexts.size() + readyJobContexts.size());
         result.addAll(failoverJobContexts);
         result.addAll(readyJobContexts);
         return result;
      }複製程式碼
    • 呼叫 FailoverService#getAllEligibleJobContexts() 方法,從失效轉移佇列中獲取所有有資格執行的作業上下文。TaskLaunchScheduledService 提交的任務還可能來自失效轉移佇列。本文暫時不解析失效轉移佇列相關實現,避免增加複雜度影響大家的理解,在《Elastic-Job-Cloud 原始碼分析 —— 作業失效轉移》詳細解析。
    • 呼叫 ReadyService#getAllEligibleJobContexts(...) 方法,從待執行佇列中獲取所有有資格執行的作業上下文。

        // ReadyService.java
        /**
        * 從待執行佇列中獲取所有有資格執行的作業上下文.
        *
        * @param ineligibleJobContexts 無資格執行的作業上下文
        * @return 有資格執行的作業上下文集合
        */
        public Collection<JobContext> getAllEligibleJobContexts(final Collection<JobContext> ineligibleJobContexts) {
           // 不存在 待執行佇列
           if (!regCenter.isExisted(ReadyNode.ROOT)) {
               return Collections.emptyList();
           }
           // 無資格執行的作業上下文 轉換成 無資格執行的作業集合
           Collection<String> ineligibleJobNames = Collections2.transform(ineligibleJobContexts, new Function<JobContext, String>() {
      
               @Override
               public String apply(final JobContext input) {
                   return input.getJobConfig().getJobName();
               }
           });
           // 獲取 待執行佇列 有資格執行的作業上下文
           List<String> jobNames = regCenter.getChildrenKeys(ReadyNode.ROOT);
           List<JobContext> result = new ArrayList<>(jobNames.size());
           for (String each : jobNames) {
               if (ineligibleJobNames.contains(each)) {
                   continue;
               }
               // 排除 作業配置 不存在的作業
               Optional<CloudJobConfiguration> jobConfig = configService.load(each);
               if (!jobConfig.isPresent()) {
                   regCenter.remove(ReadyNode.getReadyJobNodePath(each));
                   continue;
               }
               if (!runningService.isJobRunning(each)) { // 排除 執行中 的作業
                   result.add(JobContext.from(jobConfig.get(), ExecutionType.READY));
               }
           }
           return result;
        }複製程式碼
    • JobContext,作業執行上下文。實現程式碼如下:

        // JobContext.java
        public final class JobContext {
      
            private final CloudJobConfiguration jobConfig;
      
            private final List<Integer> assignedShardingItems;
      
            private final ExecutionType type;
      
            /**
             * 通過作業配置建立作業執行上下文.
             * 
             * @param jobConfig 作業配置
             * @param type 執行型別
             * @return 作業執行上下文
             */
            public static JobContext from(final CloudJobConfiguration jobConfig, final ExecutionType type) {
                int shardingTotalCount = jobConfig.getTypeConfig().getCoreConfig().getShardingTotalCount();
                // 分片項
                List<Integer> shardingItems = new ArrayList<>(shardingTotalCount);
                for (int i = 0; i < shardingTotalCount; i++) {
                    shardingItems.add(i);
                }
                return new JobContext(jobConfig, shardingItems, type);
            }
        }複製程式碼
  • LaunchingTasks,分配任務行為包。建立 LaunchingTasks 程式碼如下:

     public final class LaunchingTasks {
    
         /**
          * 作業上下文集合
          * key:作業名
          */
         private final Map<String, JobContext> eligibleJobContextsMap;
    
         public LaunchingTasks(final Collection<JobContext> eligibleJobContexts) {
             eligibleJobContextsMap = new HashMap<>(eligibleJobContexts.size(), 1);
             for (JobContext each : eligibleJobContexts) {
                 eligibleJobContextsMap.put(each.getJobConfig().getJobName(), each);
             }
         }
     }複製程式碼
  • 呼叫 LaunchingTasks#getPendingTasks() 方法,獲得待執行任務集合。這裡要注意,每個作業如果有多個分片,則會生成多個待執行任務,即此處完成了作業分片。實現程式碼如下:

      // LaunchingTasks.java
      /**
      * 獲得待執行任務
      *
      * @return 待執行任務
      */
      List<TaskRequest> getPendingTasks() {
         List<TaskRequest> result = new ArrayList<>(eligibleJobContextsMap.size() * 10);
         for (JobContext each : eligibleJobContextsMap.values()) {
             result.addAll(createTaskRequests(each));
         }
         return result;
      }
    
      /**
      * 建立待執行任務集合
      *
      * @param jobContext 作業執行上下文
      * @return 待執行任務集合
      */
      private Collection<TaskRequest> createTaskRequests(final JobContext jobContext) {
         Collection<TaskRequest> result = new ArrayList<>(jobContext.getAssignedShardingItems().size());
         for (int each : jobContext.getAssignedShardingItems()) {
             result.add(new JobTaskRequest(new TaskContext(jobContext.getJobConfig().getJobName(), Collections.singletonList(each), jobContext.getType()), jobContext.getJobConfig()));
         }
         return result;
      }
    
      // TaskContext.java
      public final class TaskContext {
         /**
          * 任務編號
          */
         private String id;
         /**
          * 任務元資訊
          */
         private final MetaInfo metaInfo;
         /**
          * 執行型別
          */
         private final ExecutionType type;
         /**
          * Mesos Slave 編號
          */
         private String slaveId;
         /**
          * 是否閒置
          */
         @Setter
         private boolean idle;
    
         public static class MetaInfo {
    
             /**
              * 作業名
              */
             private final String jobName;
             /**
              * 作業分片項
              */
             private final List<Integer> shardingItems;
         }
    
         // ... 省略部分方法
      }
    
      // JobTaskRequest.JAVA
      public final class JobTaskRequest implements TaskRequest {
    
         private final TaskContext taskContext;
    
         private final CloudJobConfiguration jobConfig;
    
         @Override
         public String getId() {
           return taskContext.getId();
         }
    
         @Override
         public double getCPUs() {
             return jobConfig.getCpuCount();
         }
    
         @Override
         public double getMemory() {
           return jobConfig.getMemoryMB();
         }
    
         // ... 省略部分方法
      }複製程式碼
    • 呼叫 #createTaskRequests(...) 方法,將單個作業按照其作業分片總數拆分成一個或多個待執行任務集合
    • TaskContext,任務執行時上下文。
    • JobTaskRequest,作業任務請求物件。
  • 因為物件有點多,我們來貼一個 LaunchingTasks#getPendingTasks() 方法的返回結果。

友情提示,程式碼可能比較多,請耐心觀看。

4.2 AppConstraintEvaluator

在說 AppConstraintEvaluator 之前,我們先一起了簡單解下 Netflix Fenzo

FROM dockone.io/article/636
Fenzo是一個在Mesos框架上應用的通用任務排程器。它可以讓你通過實現各種優化策略的外掛,來優化任務排程,同時這也有利於叢集的自動縮放。

Elastic-Job-Cloud-Scheduler 基於 Fenzo 實現對 Mesos 的彈性資源分配。

例如,AppConstraintEvaluator,App 目標 Mesos Slave 適配度限制器,選擇 Slave 時需要考慮其上是否執行有 App 的 Executor,如果沒有執行 Executor 需要將其資源消耗考慮進適配計算演算法中。它是 Fenzo ConstraintEvaluator 介面 在 Elastic-Job-Cloud-Scheduler 的自定義任務約束實現。通過這個任務約束,在下文呼叫 TaskScheduler#scheduleOnce(...) 方法排程任務所需資源時,會將 AppConstraintEvaluator 考慮進去。

那麼作業任務請求( JobTaskRequest ) 是怎麼關聯上 AppConstraintEvaluator 的呢?

// JobTaskRequest.java
public final class JobTaskRequest implements TaskRequest {

    @Override
    public List<? extends ConstraintEvaluator> getHardConstraints() {
        return Collections.singletonList(AppConstraintEvaluator.getInstance());
    }

}複製程式碼
  • Fenzo TaskRequest 介面 是 Fenzo 的任務請求介面,通過實現 #getHardConstraints() 方法,關聯上 TaskRequest 和 ConstraintEvaluator。

關聯上之後,任務匹配 Mesos Slave 資源時,呼叫 ConstraintEvaluator#evaluate(...) 實現方法判斷是否符合約束:

public interface ConstraintEvaluator {

    public static class Result {
        private final boolean isSuccessful;
        private final String failureReason;
    }

    /**
     * Inspects a target to decide whether or not it meets the constraints appropriate to a particular task.
     *
     * @param taskRequest a description of the task to be assigned
     * @param targetVM a description of the host that is a potential match for the task
     * @param taskTrackerState the current status of tasks and task assignments in the system at large
     * @return a successful Result if the target meets the constraints enforced by this constraint evaluator, or
     *         an unsuccessful Result otherwise
     */
    public Result evaluate(TaskRequest taskRequest, VirtualMachineCurrentState targetVM,
                           TaskTrackerState taskTrackerState);
}複製程式碼

OK,簡單瞭解結束,有興趣瞭解更多的同學,請點選《Fenzo Wiki —— Constraints》。下面來看看 Elastic-Job-Cloud-Scheduler 自定義實現的任務約束 AppConstraintEvaluator。


呼叫 AppConstraintEvaluator#loadAppRunningState() 方法,載入當前執行中的雲作業App,為 AppConstraintEvaluator#evaluate(...) 方法提供該資料。程式碼實現如下:

// AppConstraintEvaluator.java
private final Set<String> runningApps = new HashSet<>();

void loadAppRunningState() {
   try {
       for (MesosStateService.ExecutorStateInfo each : facadeService.loadExecutorInfo()) {
           runningApps.add(each.getId());
       }
   } catch (final JSONException | UniformInterfaceException | ClientHandlerException e) {
       clearAppRunningState();
   }
}複製程式碼
  • 呼叫 FacadeService#loadExecutorInfo() 方法,從 Mesos 獲取所有正在執行的 Mesos 執行器( Executor )的資訊。執行器和雲作業App有啥關係?每個雲作業App 即是一個 Elastic-Job-Cloud-Executor 例項。FacadeService#loadExecutorInfo() 方法這裡就不展開了,有興趣的同學自己看下,主要是對 Mesos 的 API操作,我們來看下 runningApps 的結果:


呼叫 TaskScheduler#scheduleOnce(...) 方法排程提交任務所需資源時,會呼叫 ConstraintEvaluator#loadAppRunningState() 檢查分配的資源是否符合任務的約束條件。AppConstraintEvaluator#loadAppRunningState() 實現程式碼如下:

// AppConstraintEvaluator.java
@Override
public Result evaluate(final TaskRequest taskRequest, final VirtualMachineCurrentState targetVM, final TaskTrackerState taskTrackerState) {
   double assigningCpus = 0.0d;
   double assigningMemoryMB = 0.0d;
   final String slaveId = targetVM.getAllCurrentOffers().iterator().next().getSlaveId().getValue();
   try {
       // 判斷當前分配的 Mesos Slave 是否執行著該作業任務請求對應的雲作業App
       if (isAppRunningOnSlave(taskRequest.getId(), slaveId)) {
           return new Result(true, "");
       }
       // 判斷當前分配的 Mesos Slave 啟動雲作業App 是否超過資源限制
       Set<String> calculatedApps = new HashSet<>(); // 已計算作業App集合
       List<TaskRequest> taskRequests = new ArrayList<>(targetVM.getTasksCurrentlyAssigned().size() + 1);
       taskRequests.add(taskRequest);
       for (TaskAssignmentResult each : targetVM.getTasksCurrentlyAssigned()) { // 當前已經分配作業請求
           taskRequests.add(each.getRequest());
       }
       for (TaskRequest each : taskRequests) {
           assigningCpus += each.getCPUs();
           assigningMemoryMB += each.getMemory();
           if (isAppRunningOnSlave(each.getId(), slaveId)) { // 作業App已經啟動
               continue;
           }
           CloudAppConfiguration assigningAppConfig = getAppConfiguration(each.getId());
           if (!calculatedApps.add(assigningAppConfig.getAppName())) { // 是否已經計算該App
               continue;
           }
           assigningCpus += assigningAppConfig.getCpuCount();
           assigningMemoryMB += assigningAppConfig.getMemoryMB();
       }
   } catch (final LackConfigException ex) {
       log.warn("Lack config, disable {}", getName(), ex);
       return new Result(true, "");
   }
   if (assigningCpus > targetVM.getCurrAvailableResources().cpuCores()) { // cpu
       log.debug("Failure {} {} cpus:{}/{}", taskRequest.getId(), slaveId, assigningCpus, targetVM.getCurrAvailableResources().cpuCores());
       return new Result(false, String.format("cpu:%s/%s", assigningCpus, targetVM.getCurrAvailableResources().cpuCores()));
   }
   if (assigningMemoryMB > targetVM.getCurrAvailableResources().memoryMB()) { // memory
       log.debug("Failure {} {} mem:{}/{}", taskRequest.getId(), slaveId, assigningMemoryMB, targetVM.getCurrAvailableResources().memoryMB());
       return new Result(false, String.format("mem:%s/%s", assigningMemoryMB, targetVM.getCurrAvailableResources().memoryMB()));
   }
   log.debug("Success {} {} cpus:{}/{} mem:{}/{}", taskRequest.getId(), slaveId, assigningCpus, targetVM.getCurrAvailableResources()
           .cpuCores(), assigningMemoryMB, targetVM.getCurrAvailableResources().memoryMB());
   return new Result(true, String.format("cpus:%s/%s mem:%s/%s", assigningCpus, targetVM.getCurrAvailableResources()
           .cpuCores(), assigningMemoryMB, targetVM.getCurrAvailableResources().memoryMB()));
}複製程式碼
  • 呼叫 #isAppRunningOnSlave() 方法,判斷當前分配的 Mesos Slave 是否執行著該作業任務請求對應的雲作業App。若雲作業App未執行,則該作業任務請求提交給 Mesos 後,該 Mesos Slave 會啟動該雲作業 App,App 本身會佔用一定的 CloudAppConfiguration#cpuCloudAppConfiguration#memory,計算時需要統計,避免超過當前 Mesos Slave 剩餘 cpumemory
  • 當計算符合約束時,返回 Result(true, ...);否則,返回 Result(false, ...)
  • TODO 異常為啥返回true。

4.3 將任務請求分配到 Mesos Offer

我們先簡單瞭解下 Elastic-Job-Cloud-Scheduler 實現的 Mesos Scheduler 類 com.dangdang.ddframe.job.cloud.scheduler.mesos.SchedulerEngine。排程器的主要職責之一:在接受到的 Offer 上啟動任務。SchedulerEngine 接收到資源 Offer,先儲存到資源預佔佇列( LeasesQueue ),等到作業被排程需要啟動任務時進行使用。儲存到資源預佔佇列實現程式碼如下:

public final class SchedulerEngine implements Scheduler {

    @Override
    public void resourceOffers(final SchedulerDriver schedulerDriver, final List<Protos.Offer> offers) {
        for (Protos.Offer offer: offers) {
            log.trace("Adding offer {} from host {}", offer.getId(), offer.getHostname());
            LeasesQueue.getInstance().offer(offer);
        }
    }

}複製程式碼
  • org.apache.mesos.Scheduler,Mesos 排程器介面,實現該介面成為自定義 Mesos 排程器。
  • 實現 #resourceOffers(...) 方法,有新的資源 Offer 時,會進行呼叫。在 SchedulerEngine 會呼叫 #offer(...) 方法,儲存 Offer 到資源預佔佇列,實現程式碼如下:

      public final class LeasesQueue {
    
          /**
           * 單例
           */
          private static final LeasesQueue INSTANCE = new LeasesQueue();
    
          private final BlockingQueue<VirtualMachineLease> queue = new LinkedBlockingQueue<>();
    
          /**
           * 獲取例項.
           * 
           * @return 單例物件
           */
          public static LeasesQueue getInstance() {
              return INSTANCE;
          }
    
          /**
           * 新增資源至佇列預佔.
           *
           * @param offer 資源
           */
          public void offer(final Protos.Offer offer) {
              queue.offer(new VMLeaseObject(offer));
          }
    
          // ... 省略 #drainTo() 方法,下文解析。
      }複製程式碼
    • VMLeaseObject,Netflix Fenzo 對 Mesos Offer 的抽象包裝,點選連結檢視實現程式碼,馬上會看到它的用途。

另外,可能有同學對 Mesos Offer 理解比較生澀,Offer 定義如下:

FROM segmentfault.com/a/119000000…
Offer是Mesos資源的抽象,比如說有多少CPU、多少memory,disc是多少,都放在Offer裡,打包給一個Framework,然後Framework來決定到底怎麼用這個Offer。


OK,知識鋪墊完成,回到本小節的重心:

// #runOneIteration()
Collection<VMAssignmentResult> vmAssignmentResults = taskScheduler.scheduleOnce(taskRequests, LeasesQueue.getInstance().drainTo()).getResultMap().values();

// LeasesQueue.java
public final class LeasesQueue {

    private final BlockingQueue<VirtualMachineLease> queue = new LinkedBlockingQueue<>();

    public List<VirtualMachineLease> drainTo() {
        List<VirtualMachineLease> result = new ArrayList<>(queue.size());
        queue.drainTo(result);
        return result;
    }
}複製程式碼

呼叫 TaskScheduler#scheduleOnce(...) 方法,將任務請求分配到 Mesos Offer。通過 Fenzo TaskScheduler 實現對多個任務分配到多個 Mesos Offer 的合理優化分配。這是一個相對複雜的問題。為什麼這麼說呢?

FROM 《Mesos 框架構建分散式應用》 P76
將任務匹配到 offer 上,首次適配通常是最好的演算法。你可能會想,如果在更多的工作裡嘗試計算出匹配該 offer 的優化組合,可能比首次適配更能高效地利用 offer。這絕對是正確的,但是要考慮如下這些方面:對於啟動所有等待執行的任務來說,叢集裡要麼有充足的資源要麼沒有。如果資源很多,那麼首次適配肯定一直都能保證每個任務的啟動。如果資源不夠,怎麼都無法啟動所有任務。因此,編寫程式碼選擇接下來會執行哪個任務是很自然的,這樣才能保證服務的質量。只有當資源剛夠用時,才需要更為精細的打包演算法。不幸的是,這裡的問題 —— 通常稱為揹包問題( Knapsack problem ) —— 是一個眾所周知的 NP 完全問題。NP 完全問題指的是需要相當長時間才能找到最優解決方案的問題,並且沒有任何已知道技巧能夠快速解決這類問題。

舉個簡單的例子,只考慮 memory 資源情況下,有一臺 Slave 記憶體為 8GB ,現在要執行三個 1GB 的作業和 5GB 的作業。其中 5GB 的作業在 1GB 執行多次之後才執行。

實際情況會比圖更加複雜的多的多。通過使用 Fenzo ,可以很方便的,並且令人滿意的分配。為了讓你對 Fenzo 有更加透徹的理解,這裡再引用一段對其的介紹:

FROM 《Mesos 框架構建分散式應用》 P80
呼叫庫函式 Fenzo
Fenzo 是 Nettflix 在 2015 年夏天釋出的庫函式。Fenzo 為基於 java 的排程器提供了完整的解決方案,完成 offer 緩衝,多工啟動,以及軟和硬約束條件的匹配。就算不是所有的,也是很多排程器都能夠受益於使用 Fenzo 來完成計算任務分配,而不用自己編寫 offer 緩衝、打包和放置路由等。

下面,來看兩次 TaskScheduler#scheduleOnce(...) 的返回:

  • 第一次排程:
  • 第二次排程:
  • com.netflix.fenzo.VMAssignmentResult,每臺主機分配任務結果。實現程式碼如下:

      public class VMAssignmentResult {
          /**
           * 主機
           */
          private final String hostname;
          /**
           * 使用的 Mesos Offer
           */
          private final List<VirtualMachineLease> leasesUsed;
          /**
           * 分配的任務
           */
          private final Set<TaskAssignmentResult> tasksAssigned;
      }複製程式碼

受限於筆者的能力,建議你可以在閱讀如下文章,更透徹的理解 TaskScheduler :

4.4 建立 Mesos 任務資訊

// #runOneIteration()
List<TaskContext> taskContextsList = new LinkedList<>(); // 任務執行時上下文集合
Map<List<Protos.OfferID>, List<Protos.TaskInfo>> offerIdTaskInfoMap = new HashMap<>(); // Mesos 任務資訊集合
for (VMAssignmentResult each: vmAssignmentResults) {
    List<VirtualMachineLease> leasesUsed = each.getLeasesUsed();
    List<Protos.TaskInfo> taskInfoList = new ArrayList<>(each.getTasksAssigned().size() * 10);
    taskInfoList.addAll(getTaskInfoList(
            launchingTasks.getIntegrityViolationJobs(vmAssignmentResults), // 獲得作業分片不完整的作業集合
            each, leasesUsed.get(0).hostname(), leasesUsed.get(0).getOffer()));
    for (Protos.TaskInfo taskInfo : taskInfoList) {
        taskContextsList.add(TaskContext.from(taskInfo.getTaskId().getValue()));
    }
    offerIdTaskInfoMap.put(getOfferIDs(leasesUsed), // 獲得 Offer ID 集合
            taskInfoList);
}複製程式碼
  • offerIdTaskInfoMap,Mesos 任務資訊集合。key 和 value 都為相同 Mesos Slave Offer 和 任務。為什麼?呼叫 SchedulerDriver#launchTasks(...) 方法提交一次任務時,必須保證所有任務和 Offer 在相同 Mesos Slave 上。

    FROM FROM 《Mesos 框架構建分散式應用》 P61
    組合 offer
    latchTasks 接受 offer 列表為輸入,這就允許使用者將一些相同 slave 的 offer 組合起來,從而將這些 offer 的資源放到池裡。它還能接受任務列表為輸入,這樣就能夠啟動適合給定 offer 的足夠多的任務。注意所有任務和 offer 都必須是同一臺 slave —— 如果不在同一臺 slave 上,launchTasks 就會失敗。如果想在多臺 slave 上啟動任務,多次呼叫 latchTasks 即可。

  • 呼叫 LaunchingTasks#getIntegrityViolationJobs(...) 方法,獲得作業分片不完整的作業集合。一個作業有多個分片,因為 Mesos Offer 不足,導致有部分分片不能執行,則整個作業都不進行執行。程式碼實現如下:

      // LaunchingTasks.java
      /**
      * 獲得作業分片不完整的作業集合
      *
      * @param vmAssignmentResults 主機分配任務結果集合
      * @return 作業分片不完整的作業集合
      */
      Collection<String> getIntegrityViolationJobs(final Collection<VMAssignmentResult> vmAssignmentResults) {
         Map<String, Integer> assignedJobShardingTotalCountMap = getAssignedJobShardingTotalCountMap(vmAssignmentResults);
         Collection<String> result = new HashSet<>(assignedJobShardingTotalCountMap.size(), 1);
         for (Map.Entry<String, Integer> entry : assignedJobShardingTotalCountMap.entrySet()) {
             JobContext jobContext = eligibleJobContextsMap.get(entry.getKey());
             if (ExecutionType.FAILOVER != jobContext.getType() // 不包括 FAILOVER 執行型別的作業
                     && !entry.getValue().equals(jobContext.getJobConfig().getTypeConfig().getCoreConfig().getShardingTotalCount())) {
                 log.warn("Job {} is not assigned at this time, because resources not enough to run all sharding instances.", entry.getKey());
                 result.add(entry.getKey());
             }
         }
         return result;
      }
    
      /**
      * 獲得每個作業分片數集合
      * key:作業名
      * value:分片總數
      *
      * @param vmAssignmentResults 主機分配任務結果集合
      * @return 每個作業分片數集合
      */
      private Map<String, Integer> getAssignedJobShardingTotalCountMap(final Collection<VMAssignmentResult> vmAssignmentResults) {
         Map<String, Integer> result = new HashMap<>(eligibleJobContextsMap.size(), 1);
         for (VMAssignmentResult vmAssignmentResult: vmAssignmentResults) {
             for (TaskAssignmentResult tasksAssigned: vmAssignmentResult.getTasksAssigned()) {
                 String jobName = TaskContext.from(tasksAssigned.getTaskId()).getMetaInfo().getJobName();
                 if (result.containsKey(jobName)) {
                     result.put(jobName, result.get(jobName) + 1);
                 } else {
                     result.put(jobName, 1);
                 }
             }
         }
         return result;
      }複製程式碼
  • 呼叫 #getTaskInfoList(...) 方法,建立單個主機的 Mesos 任務資訊集合。實現程式碼如下:

      private List<Protos.TaskInfo> getTaskInfoList(final Collection<String> integrityViolationJobs, final VMAssignmentResult vmAssignmentResult, final String hostname, final Protos.Offer offer) {
         List<Protos.TaskInfo> result = new ArrayList<>(vmAssignmentResult.getTasksAssigned().size());
         for (TaskAssignmentResult each: vmAssignmentResult.getTasksAssigned()) {
             TaskContext taskContext = TaskContext.from(each.getTaskId());
             String jobName = taskContext.getMetaInfo().getJobName();
             if (!integrityViolationJobs.contains(jobName) // 排除作業分片不完整的任務
                     && !facadeService.isRunning(taskContext) // 排除正在執行中的任務
                     && !facadeService.isJobDisabled(jobName)) { // 排除被禁用的任務
                 // 建立 Mesos 任務
                 Protos.TaskInfo taskInfo = getTaskInfo(offer, each);
                 if (null != taskInfo) {
                     result.add(taskInfo);
                     // 新增任務主鍵和主機名稱的對映
                     facadeService.addMapping(taskInfo.getTaskId().getValue(), hostname);
                     // 通知 TaskScheduler 主機分配了這個任務
                     taskScheduler.getTaskAssigner().call(each.getRequest(), hostname);
                 }
             }
         }
         return result;
      }複製程式碼
    • 呼叫 #getTaskInfo(...) 方法,建立單個 Mesos 任務,在「4.4.1 建立單個 Mesos 任務資訊」詳細解析。
    • 呼叫 FacadeService#addMapping(...) 方法,新增任務主鍵和主機名稱的對映。通過該對映,可以根據任務主鍵查詢到對應的主機名。實現程式碼如下:

       // FacadeService.java
       /**
       * 新增任務主鍵和主機名稱的對映.
       *
       * @param taskId 任務主鍵
       * @param hostname 主機名稱
       */
       public void addMapping(final String taskId, final String hostname) {
          runningService.addMapping(taskId, hostname);
       }
      
       // RunningService.java
       /**
       * 任務主鍵和主機名稱的對映
       * key: 任務主鍵
       * value: 主機名稱
       */
       private static final ConcurrentHashMap<String, String> TASK_HOSTNAME_MAPPER = new ConcurrentHashMap<>(TASK_INITIAL_SIZE);
      
       public void addMapping(final String taskId, final String hostname) {
          TASK_HOSTNAME_MAPPER.putIfAbsent(taskId, hostname);
       }複製程式碼
    • 呼叫 TaskScheduler#getTaskAssigner()#call(...) 方法,通知 TaskScheduler 任務被確認分配到這個主機。TaskScheduler 做任務和 Offer 的匹配,對哪些任務執行在哪些主機是有依賴的,不然怎麼做匹配優化呢。在《Fenzo Wiki —— Notify the Scheduler of Assigns and UnAssigns of Tasks》可以進一步瞭解。

  • 呼叫 #getOfferIDs(...) 方法,獲得 Offer ID 集合。實現程式碼如下:

      private List<Protos.OfferID> getOfferIDs(final List<VirtualMachineLease> leasesUsed) {
         List<Protos.OfferID> result = new ArrayList<>();
         for (VirtualMachineLease virtualMachineLease: leasesUsed) {
             result.add(virtualMachineLease.getOffer().getId());
         }
         return result;
      }複製程式碼

4.4.1 建立單個 Mesos 任務資訊

呼叫 #getTaskInfo() 方法,建立單個 Mesos 任務資訊。實現程式碼如下:

如下會涉及大量的 Mesos API

private Protos.TaskInfo getTaskInfo(final Protos.Offer offer, final TaskAssignmentResult taskAssignmentResult) {
   // 校驗 作業配置 是否存在
   TaskContext taskContext = TaskContext.from(taskAssignmentResult.getTaskId());
   Optional<CloudJobConfiguration> jobConfigOptional = facadeService.load(taskContext.getMetaInfo().getJobName());
   if (!jobConfigOptional.isPresent()) {
       return null;
   }
   CloudJobConfiguration jobConfig = jobConfigOptional.get();
   // 校驗 作業配置 是否存在
   Optional<CloudAppConfiguration> appConfigOptional = facadeService.loadAppConfig(jobConfig.getAppName());
   if (!appConfigOptional.isPresent()) {
       return null;
   }
   CloudAppConfiguration appConfig = appConfigOptional.get();
   // 設定 Mesos Slave ID
   taskContext.setSlaveId(offer.getSlaveId().getValue());
   // 獲得 分片上下文集合
   ShardingContexts shardingContexts = getShardingContexts(taskContext, appConfig, jobConfig);
   // 瞬時的指令碼作業,使用 Mesos 命令列執行,無需使用執行器
   boolean isCommandExecutor = CloudJobExecutionType.TRANSIENT == jobConfig.getJobExecutionType() && JobType.SCRIPT == jobConfig.getTypeConfig().getJobType();
   String script = appConfig.getBootstrapScript();
   if (isCommandExecutor) {
       script = ((ScriptJobConfiguration) jobConfig.getTypeConfig()).getScriptCommandLine();
   }
   // 建立 啟動命令
   Protos.CommandInfo.URI uri = buildURI(appConfig, isCommandExecutor);
   Protos.CommandInfo command = buildCommand(uri, script, shardingContexts, isCommandExecutor);
   // 建立 Mesos 任務資訊
   if (isCommandExecutor) {
       return buildCommandExecutorTaskInfo(taskContext, jobConfig, shardingContexts, offer, command);
   } else {
       return buildCustomizedExecutorTaskInfo(taskContext, appConfig, jobConfig, shardingContexts, offer, command);
   }
}複製程式碼
  • 呼叫 #getShardingContexts(...) 方法, 獲得分片上下文集合。實現程式碼如下:

      private ShardingContexts getShardingContexts(final TaskContext taskContext, final CloudAppConfiguration appConfig, final CloudJobConfiguration jobConfig) {
         Map<Integer, String> shardingItemParameters = new ShardingItemParameters(jobConfig.getTypeConfig().getCoreConfig().getShardingItemParameters()).getMap();
         Map<Integer, String> assignedShardingItemParameters = new HashMap<>(1, 1);
         int shardingItem = taskContext.getMetaInfo().getShardingItems().get(0); // 單個作業分片
         assignedShardingItemParameters.put(shardingItem, shardingItemParameters.containsKey(shardingItem) ? shardingItemParameters.get(shardingItem) : "");
         return new ShardingContexts(taskContext.getId(), jobConfig.getJobName(), jobConfig.getTypeConfig().getCoreConfig().getShardingTotalCount(),
                 jobConfig.getTypeConfig().getCoreConfig().getJobParameter(), assignedShardingItemParameters, appConfig.getEventTraceSamplingCount());
      }複製程式碼
  • 當任務為瞬時指令碼作業時,使用 Mesos Slave 命令列呼叫即可,無需使用 Elastic-Job-Cloud-Executor。
  • 呼叫 #buildURI(...) 方法,建立執行器的二進位制檔案下載地址。試下程式碼如下:

      private Protos.CommandInfo.URI buildURI(final CloudAppConfiguration appConfig, final boolean isCommandExecutor) {
         Protos.CommandInfo.URI.Builder result = Protos.CommandInfo.URI.newBuilder()
                 .setValue(appConfig.getAppURL())
                 .setCache(appConfig.isAppCacheEnable()); // cache
         if (isCommandExecutor && !SupportedExtractionType.isExtraction(appConfig.getAppURL())) {
             result.setExecutable(true); // 是否可執行
         } else {
             result.setExtract(true); // 是否需要解壓
         }
         return result.build();
      }複製程式碼
    • 雲作業應用配置 CloudAppConfiguration.appURL ,通過 Mesos 實現檔案的下載。
    • 雲作業應用配置 CloudAppConfiguration.appCacheEnable,應用檔案下載是否快取。

      FROM 《Mesos 框架構建分散式應用》 P99
      Fetcher 快取
      Mesos 0.23 裡釋出稱為 fetcher 快取的新功能。fetcher 快取確保每個 artifact 在每個 slave 只會下載一次,即使多個執行器請求同一個 artifact,也只需要等待單詞下載完成即可。

  • 呼叫 #buildCommand(...) 方法,建立執行器啟動命令。實現程式碼如下:

      private Protos.CommandInfo buildCommand(final Protos.CommandInfo.URI uri, final String script, final ShardingContexts shardingContexts, final boolean isCommandExecutor) {
         Protos.CommandInfo.Builder result = Protos.CommandInfo.newBuilder().addUris(uri).setShell(true);
         if (isCommandExecutor) {
             CommandLine commandLine = CommandLine.parse(script);
             commandLine.addArgument(GsonFactory.getGson().toJson(shardingContexts), false);
             result.setValue(Joiner.on(" ").join(commandLine.getExecutable(), Joiner.on(" ").join(commandLine.getArguments())));
         } else {
             result.setValue(script);
         }
         return result.build();
      }複製程式碼
  • 呼叫 #buildCommandExecutorTaskInfo(...) 方法,為瞬時指令碼作業建立 Mesos 任務資訊。實現程式碼如下:

      private Protos.TaskInfo buildCommandExecutorTaskInfo(final TaskContext taskContext, final CloudJobConfiguration jobConfig, final ShardingContexts shardingContexts,
                                                          final Protos.Offer offer, final Protos.CommandInfo command) {
         Protos.TaskInfo.Builder result = Protos.TaskInfo.newBuilder().setTaskId(Protos.TaskID.newBuilder().setValue(taskContext.getId()).build())
                 .setName(taskContext.getTaskName()).setSlaveId(offer.getSlaveId())
                 .addResources(buildResource("cpus", jobConfig.getCpuCount(), offer.getResourcesList()))
                 .addResources(buildResource("mem", jobConfig.getMemoryMB(), offer.getResourcesList()))
                 .setData(ByteString.copyFrom(new TaskInfoData(shardingContexts, jobConfig).serialize())); //
         return result.setCommand(command).build();
      }複製程式碼
  • 呼叫 #buildCustomizedExecutorTaskInfo(...) 方法,建立 Mesos 任務資訊。實現程式碼如下:

      private Protos.TaskInfo buildCustomizedExecutorTaskInfo(final TaskContext taskContext, final CloudAppConfiguration appConfig, final CloudJobConfiguration jobConfig, 
                                                             final ShardingContexts shardingContexts, final Protos.Offer offer, final Protos.CommandInfo command) {
         Protos.TaskInfo.Builder result = Protos.TaskInfo.newBuilder().setTaskId(Protos.TaskID.newBuilder().setValue(taskContext.getId()).build())
                 .setName(taskContext.getTaskName()).setSlaveId(offer.getSlaveId())
                 .addResources(buildResource("cpus", jobConfig.getCpuCount(), offer.getResourcesList()))
                 .addResources(buildResource("mem", jobConfig.getMemoryMB(), offer.getResourcesList()))
                 .setData(ByteString.copyFrom(new TaskInfoData(shardingContexts, jobConfig).serialize()));
         // ExecutorInfo
         Protos.ExecutorInfo.Builder executorBuilder = Protos.ExecutorInfo.newBuilder().setExecutorId(Protos.ExecutorID.newBuilder()
                 .setValue(taskContext.getExecutorId(jobConfig.getAppName()))) // 執行器 ID
                 .setCommand(command)
                 .addResources(buildResource("cpus", appConfig.getCpuCount(), offer.getResourcesList()))
                 .addResources(buildResource("mem", appConfig.getMemoryMB(), offer.getResourcesList()));
         if (env.getJobEventRdbConfiguration().isPresent()) {
             executorBuilder.setData(ByteString.copyFrom(SerializationUtils.serialize(env.getJobEventRdbConfigurationMap()))).build();
         }
         return result.setExecutor(executorBuilder.build()).build();
      }複製程式碼
    • 呼叫 Protos.ExecutorInfo.Builder#setValue(...) 方法,設定執行器編號。大多數在 Mesos 實現的執行器,一個任務對應一個執行器。而 Elastic-Job-Cloud-Executor 不同於大多數在 Mesos 上的執行器,一個執行器可以對應多個作業。什麼意思?在一個 Mesos Slave,相同作業應用,只會啟動一個 Elastic-Job-Cloud-Scheduler。當該執行器不存在時,啟動一個。當該執行器已經存在,複用該執行器。那麼是如何實現該功能的呢?相同作業應用,在同一個 Mesos Slave,使用相同執行器編號。實現程式碼如下:

       /**
        * 獲取任務執行器主鍵.
        * 
        * @param appName 應用名稱
        * @return 任務執行器主鍵
        */
       public String getExecutorId(final String appName) {
           return Joiner.on(DELIMITER).join(appName, slaveId);
       }複製程式碼

4.5 將任務執行時上下文放入執行時佇列

呼叫 FacadeService#addRunning(...) 方法,將任務執行時上下文放入執行時佇列。實現程式碼如下:

// FacadeService.java
/**
* 將任務執行時上下文放入執行時佇列.
*
* @param taskContext 任務執行時上下文
*/
public void addRunning(final TaskContext taskContext) {
   runningService.add(taskContext);
}

// RunningService.java
/**
* 將任務執行時上下文放入執行時佇列.
* 
* @param taskContext 任務執行時上下文
*/
public void add(final TaskContext taskContext) {
   if (!configurationService.load(taskContext.getMetaInfo().getJobName()).isPresent()) {
       return;
   }
   // 新增到執行中的任務集合
   getRunningTasks(taskContext.getMetaInfo().getJobName()).add(taskContext);
   // 判斷是否為常駐任務
   if (!isDaemon(taskContext.getMetaInfo().getJobName())) {
       return;
   }
   // 新增到執行中佇列
   String runningTaskNodePath = RunningNode.getRunningTaskNodePath(taskContext.getMetaInfo().toString());
   if (!regCenter.isExisted(runningTaskNodePath)) {
       regCenter.persist(runningTaskNodePath, taskContext.getId());
   }
}

// RunningNode.java
final class RunningNode {

    static final String ROOT = StateNode.ROOT + "/running";

    private static final String RUNNING_JOB = ROOT + "/%s"; // %s = ${JOB_NAME}

    private static final String RUNNING_TASK = RUNNING_JOB + "/%s"; // %s = ${TASK_META_INFO}。${TASK_META_INFO}=${JOB_NAME}@-@${ITEM_ID}。
}複製程式碼
  • RunningService,任務執行時服務,提供對執行中的任務集合、執行中作業佇列的各種操作方法。
  • 呼叫 #getRunningTasks() 方法,獲得執行中的任務集合,並將當前任務新增到其中。實現程式碼如下:

      public Collection<TaskContext> getRunningTasks(final String jobName) {
         Set<TaskContext> taskContexts = new CopyOnWriteArraySet<>();
         Collection<TaskContext> result = RUNNING_TASKS.putIfAbsent(jobName, taskContexts);
         return null == result ? taskContexts : result;
      }複製程式碼

    在運維平臺,我們可以看到當前任務正在執行中:

  • 常駐作業會儲存在執行中作業佇列。執行中作業佇列儲存在註冊中心( Zookeeper )的持久資料節點 /${NAMESPACE}/state/running/${JOB_NAME}/${TASK_META_INFO},儲存值為任務編號。使用 zkClient 檢視如下:

      [zk: localhost:2181(CONNECTED) 14] ls /elastic-job-cloud/state/running/test_job_simple
      [test_job_simple@-@0, test_job_simple@-@1, test_job_simple@-@2]
      [zk: localhost:2181(CONNECTED) 15] get /elastic-job-cloud/state/running/test_job_simple/test_job_simple@-@0
      test_job_simple@-@0@-@READY@-@400197d9-76ca-464b-b2f0-e0fba5c2a598-S0@-@9780ed12-9612-45e3-ac14-feb2911896ff複製程式碼

4.6 從佇列中刪除已執行的作業

// #runOneIteration()
facadeService.removeLaunchTasksFromQueue(taskContextsList);

// FacadeService.java
/**
* 從佇列中刪除已執行的作業.
* 
* @param taskContexts 任務上下文集合
*/
public void removeLaunchTasksFromQueue(final List<TaskContext> taskContexts) {
   List<TaskContext> failoverTaskContexts = new ArrayList<>(taskContexts.size());
   Collection<String> readyJobNames = new HashSet<>(taskContexts.size(), 1);
   for (TaskContext each : taskContexts) {
       switch (each.getType()) {
           case FAILOVER:
               failoverTaskContexts.add(each);
               break;
           case READY:
               readyJobNames.add(each.getMetaInfo().getJobName());
               break;
           default:
               break;
       }
   }
   // 從失效轉移佇列中刪除相關任務
   failoverService.remove(Lists.transform(failoverTaskContexts, new Function<TaskContext, TaskContext.MetaInfo>() {

       @Override
       public TaskContext.MetaInfo apply(final TaskContext input) {
           return input.getMetaInfo();
       }
   }));
   // 從待執行佇列中刪除相關作業
   readyService.remove(readyJobNames);
}複製程式碼

4.7 提交任務給 Mesos

// #runOneIteration()
for (Entry<List<OfferID>, List<TaskInfo>> each : offerIdTaskInfoMap.entrySet()) {
   schedulerDriver.launchTasks(each.getKey(), each.getValue());
}複製程式碼
  • 呼叫 SchedulerDriver#launchTasks(...) 方法,提交任務給 Mesos Master。由 Mesos Master 排程任務給 Mesos Slave。Mesos Slave 提交執行器執行任務。

5. TaskExecutor 執行任務

TaskExecutor,實現了 Mesos Executor 介面 org.apache.mesos.Executor。執行器的主要職責之一:執行排程器所請求的任務。TaskExecutor 接收到 Mesos Slave 提交的任務,呼叫 #launchTask(...) 方法,處理任務。實現程式碼如下:

// DaemonTaskScheduler.java
@Override
public void launchTask(final ExecutorDriver executorDriver, final Protos.TaskInfo taskInfo) {
   executorService.submit(new TaskThread(executorDriver, taskInfo));
}複製程式碼
  • 呼叫 ExecutorService#submit(...) 方法,提交 TaskThread 到執行緒池,執行任務。

5.1 TaskThread

@RequiredArgsConstructor
class TaskThread implements Runnable {

   private final ExecutorDriver executorDriver;

   private final TaskInfo taskInfo;

   @Override
   public void run() {
       // 更新 Mesos 任務狀態,執行中。
       executorDriver.sendStatusUpdate(Protos.TaskStatus.newBuilder().setTaskId(taskInfo.getTaskId()).setState(Protos.TaskState.TASK_RUNNING).build());
       //
       Map<String, Object> data = SerializationUtils.deserialize(taskInfo.getData().toByteArray());
       ShardingContexts shardingContexts = (ShardingContexts) data.get("shardingContext");
       @SuppressWarnings("unchecked")
       JobConfigurationContext jobConfig = new JobConfigurationContext((Map<String, String>) data.get("jobConfigContext"));
       try {
           // 獲得 分散式作業
           ElasticJob elasticJob = getElasticJobInstance(jobConfig);
           // 排程器提供內部服務的門面物件
           final CloudJobFacade jobFacade = new CloudJobFacade(shardingContexts, jobConfig, jobEventBus);
           // 執行作業
           if (jobConfig.isTransient()) {
               // 執行作業
               JobExecutorFactory.getJobExecutor(elasticJob, jobFacade).execute();
               // 更新 Mesos 任務狀態,已完成。
               executorDriver.sendStatusUpdate(Protos.TaskStatus.newBuilder().setTaskId(taskInfo.getTaskId()).setState(Protos.TaskState.TASK_FINISHED).build());
           } else {
               // 初始化 常駐作業排程器
               new DaemonTaskScheduler(elasticJob, jobConfig, jobFacade, executorDriver, taskInfo.getTaskId()).init();
           }
           // CHECKSTYLE:OFF
       } catch (final Throwable ex) {
           // CHECKSTYLE:ON
           log.error("Elastic-Job-Cloud-Executor error", ex);
           executorDriver.sendStatusUpdate(Protos.TaskStatus.newBuilder().setTaskId(taskInfo.getTaskId()).setState(Protos.TaskState.TASK_ERROR).setMessage(ExceptionUtil.transform(ex)).build());
           executorDriver.stop();
           throw ex;
       }
   }
}複製程式碼
  • TaskInfo.data 屬性中,可以獲得提交任務附帶的資料,例如分片上下文集合( ShardingContexts ),內部的作業配置上下文( JobConfigurationContext )。
  • 呼叫 #getElasticJobInstance() 方法,獲得任務需要執行的分散式作業( Elastic-Job )。實現程式碼如下:

      private ElasticJob getElasticJobInstance(final JobConfigurationContext jobConfig) {
        if (!Strings.isNullOrEmpty(jobConfig.getBeanName()) && !Strings.isNullOrEmpty(jobConfig.getApplicationContext())) { // spring 環境
            return getElasticJobBean(jobConfig);
        } else {
            return getElasticJobClass(jobConfig);
        }
      }
    
      /**
      * 從 Spring 容器中獲得作業物件
      *
      * @param jobConfig 作業配置
      * @return 作業物件
      */
      private ElasticJob getElasticJobBean(final JobConfigurationContext jobConfig) {
        String applicationContextFile = jobConfig.getApplicationContext();
        if (null == applicationContexts.get(applicationContextFile)) {
            synchronized (applicationContexts) {
                if (null == applicationContexts.get(applicationContextFile)) {
                    applicationContexts.put(applicationContextFile, new ClassPathXmlApplicationContext(applicationContextFile));
                }
            }
        }
        return (ElasticJob) applicationContexts.get(applicationContextFile).getBean(jobConfig.getBeanName());
      }
    
      /**
      * 建立作業物件
      *
      * @param jobConfig 作業配置
      * @return 作業物件
      */
      private ElasticJob getElasticJobClass(final JobConfigurationContext jobConfig) {
        String jobClass = jobConfig.getTypeConfig().getJobClass();
        try {
            Class<?> elasticJobClass = Class.forName(jobClass);
            if (!ElasticJob.class.isAssignableFrom(elasticJobClass)) {
                throw new JobSystemException("Elastic-Job: Class '%s' must implements ElasticJob interface.", jobClass);
            }
            if (elasticJobClass != ScriptJob.class) {
                return (ElasticJob) elasticJobClass.newInstance();
            }
            return null;
        } catch (final ReflectiveOperationException ex) {
            throw new JobSystemException("Elastic-Job: Class '%s' initialize failure, the error message is '%s'.", jobClass, ex.getMessage());
        }
      }複製程式碼
    • 當作業是瞬時作業時,呼叫 AbstractElasticJobExecutor#execute(...) 執行作業邏輯,並呼叫 ExecutorDriver#sendStatusUpdate(...) 傳送狀態,更新 Mesos 任務已完成( Protos.TaskState.TASK_FINISHED )。AbstractElasticJobExecutor#execute(...) 實現程式碼,在 Elastic-Job-Lite 和 Elastic-Job-Cloud 基本一致,在《Elastic-Job-Lite 原始碼分析 —— 作業執行》有詳細解析。
    • 當作業是常駐作業時,呼叫 DaemonTaskScheduler#init() 方法,初始化作業排程,在「5.2 DaemonTaskScheduler」詳細解析。

5.2 DaemonTaskScheduler

瞬時作業,通過 Elastic-Job-Cloud-Scheduler 排程任務,提交 Elastic-Job-Cloud-Executor 執行後,等待 Elastic-Job-Scheduler 進行下次排程。

常駐作業,通過 Elastic-Job-Scheduler 提交 Elastic-Job-Cloud-Executor 進行排程。Elastic-Job-Cloud-Executor 使用 DaemonTaskScheduler 不斷對常駐作業進行排程而無需 Elastic-Job-Cloud-Scheduler 參與其中。

這就是瞬時作業和常駐作業不同之處。

DaemonTaskScheduler,常駐作業排程器。呼叫 DaemonTaskScheduler#init() 方法,對一個作業初始化排程,實現程式碼如下:

/**
* 初始化作業.
*/
public void init() {
   // Quartz JobDetail
   JobDetail jobDetail = JobBuilder.newJob(DaemonJob.class)
           .withIdentity(jobRootConfig.getTypeConfig().getCoreConfig().getJobName()).build();
   jobDetail.getJobDataMap().put(ELASTIC_JOB_DATA_MAP_KEY, elasticJob);
   jobDetail.getJobDataMap().put(JOB_FACADE_DATA_MAP_KEY, jobFacade);
   jobDetail.getJobDataMap().put(EXECUTOR_DRIVER_DATA_MAP_KEY, executorDriver);
   jobDetail.getJobDataMap().put(TASK_ID_DATA_MAP_KEY, taskId);
   try {
       scheduleJob(initializeScheduler(), jobDetail, taskId.getValue(), jobRootConfig.getTypeConfig().getCoreConfig().getCron());
   } catch (final SchedulerException ex) {
       throw new JobSystemException(ex);
   }
}

private Scheduler initializeScheduler() throws SchedulerException {
   StdSchedulerFactory factory = new StdSchedulerFactory();
   factory.initialize(getBaseQuartzProperties());
   return factory.getScheduler();
}

private Properties getBaseQuartzProperties() {
   Properties result = new Properties();
   result.put("org.quartz.threadPool.class", org.quartz.simpl.SimpleThreadPool.class.getName());
   result.put("org.quartz.threadPool.threadCount", "1"); // 執行緒數:1
   result.put("org.quartz.scheduler.instanceName", taskId.getValue());
   if (!jobRootConfig.getTypeConfig().getCoreConfig().isMisfire()) {
       result.put("org.quartz.jobStore.misfireThreshold", "1");
   }
   result.put("org.quartz.plugin.shutdownhook.class", ShutdownHookPlugin.class.getName());
   result.put("org.quartz.plugin.shutdownhook.cleanShutdown", Boolean.TRUE.toString());
   return result;
}

private void scheduleJob(final Scheduler scheduler, final JobDetail jobDetail, final String triggerIdentity, final String cron) {
   try {
       if (!scheduler.checkExists(jobDetail.getKey())) {
           scheduler.scheduleJob(jobDetail, createTrigger(triggerIdentity, cron));
       }
       scheduler.start();
       RUNNING_SCHEDULERS.putIfAbsent(scheduler.getSchedulerName(), scheduler);
   } catch (final SchedulerException ex) {
       throw new JobSystemException(ex);
   }
}

private CronTrigger createTrigger(final String triggerIdentity, final String cron) {
   return TriggerBuilder.newTrigger()
           .withIdentity(triggerIdentity)
           .withSchedule(CronScheduleBuilder.cronSchedule(cron)
           .withMisfireHandlingInstructionDoNothing())
           .build();
}複製程式碼
  • DaemonTaskScheduler 基於 Quartz 實現作業排程。這裡大家看下原始碼,就不囉嗦解釋啦。
  • JobBuilder#newJob(...) 的引數是 DaemonJob,下文會講解到。

DaemonJob 實現程式碼如下:

public static final class DaemonJob implements Job {

   @Setter
   private ElasticJob elasticJob;

   @Setter
   private JobFacade jobFacade;

   @Setter
   private ExecutorDriver executorDriver;

   @Setter
   private Protos.TaskID taskId;

   @Override
   public void execute(final JobExecutionContext context) throws JobExecutionException {
       ShardingContexts shardingContexts = jobFacade.getShardingContexts();
       int jobEventSamplingCount = shardingContexts.getJobEventSamplingCount();
       int currentJobEventSamplingCount = shardingContexts.getCurrentJobEventSamplingCount();
       if (jobEventSamplingCount > 0 && ++currentJobEventSamplingCount < jobEventSamplingCount) {
           shardingContexts.setCurrentJobEventSamplingCount(currentJobEventSamplingCount);
           //
           jobFacade.getShardingContexts().setAllowSendJobEvent(false);
           // 執行作業
           JobExecutorFactory.getJobExecutor(elasticJob, jobFacade).execute();
       } else {
           //
           jobFacade.getShardingContexts().setAllowSendJobEvent(true);
           //
           executorDriver.sendStatusUpdate(Protos.TaskStatus.newBuilder().setTaskId(taskId).setState(Protos.TaskState.TASK_RUNNING).setMessage("BEGIN").build());
           // 執行作業
           JobExecutorFactory.getJobExecutor(elasticJob, jobFacade).execute();
           //
           executorDriver.sendStatusUpdate(Protos.TaskStatus.newBuilder().setTaskId(taskId).setState(Protos.TaskState.TASK_RUNNING).setMessage("COMPLETE").build());
           // 
           shardingContexts.setCurrentJobEventSamplingCount(0);
       }
   }
}複製程式碼
  • 呼叫 AbstractElasticJobExecutor#execute(...) 執行作業邏輯。AbstractElasticJobExecutor#execute(...) 實現程式碼,在 Elastic-Job-Lite 和 Elastic-Job-Cloud 基本一致,在《Elastic-Job-Lite 原始碼分析 —— 作業執行》有詳細解析。
  • jobEventSamplingCount 來自應用配置 (CloudAppConfiguration.eventTraceSamplingCount) 屬性,常駐作業事件取樣率統計條數,預設取樣全部記錄。為避免資料量過大,可對頻繁排程的常駐作業配置取樣率,即作業每執行N次,才會記錄作業執行及追蹤相關資料。

    當滿足取樣條件時,呼叫 ShardingContexts#setAllowSendJobEvent(true),標記記錄作業事件。否則,呼叫 ShardingContexts#setAllowSendJobEvent(false),標記記錄作業時間。作業事件追蹤在《Elastic-Job-Lite 原始碼分析 —— 作業事件追蹤》有詳細解析。

    另外,當滿足取樣除錯時,也會呼叫 ExecutorDriver#sendStatusUpdate(...) 方法,更新 Mesos 任務狀態為執行中,並附帶 "BEGIN""COMPLETE" 訊息。

6. SchedulerEngine 處理任務的狀態變更

Mesos 排程器的職責之一,處理任務的狀態,特別是響應任務和故障。因此在 Elastic-Job-Cloud-Executor 呼叫 ExecutorDriver#sendStatusUpdate(...) 方法,更新 Mesos 任務狀態時,觸發呼叫 Elastic-Job-Cloud-Scheduler 的 SchedulerEngine 的 #statusUpdate(...) 方法,實現程式碼如下:

@Override
public void statusUpdate(final SchedulerDriver schedulerDriver, final Protos.TaskStatus taskStatus) {
   String taskId = taskStatus.getTaskId().getValue();
   TaskContext taskContext = TaskContext.from(taskId);
   String jobName = taskContext.getMetaInfo().getJobName();
   log.trace("call statusUpdate task state is: {}, task id is: {}", taskStatus.getState(), taskId);
   jobEventBus.post(new JobStatusTraceEvent(jobName, taskContext.getId(), taskContext.getSlaveId(), Source.CLOUD_SCHEDULER, 
           taskContext.getType(), String.valueOf(taskContext.getMetaInfo().getShardingItems()), State.valueOf(taskStatus.getState().name()), taskStatus.getMessage()));
   switch (taskStatus.getState()) {
       case TASK_RUNNING:
           if (!facadeService.load(jobName).isPresent()) {
               schedulerDriver.killTask(Protos.TaskID.newBuilder().setValue(taskId).build());
           }
           if ("BEGIN".equals(taskStatus.getMessage())) {
               facadeService.updateDaemonStatus(taskContext, false);
           } else if ("COMPLETE".equals(taskStatus.getMessage())) {
               facadeService.updateDaemonStatus(taskContext, true);
               statisticManager.taskRunSuccessfully();
           }
           break;
       case TASK_FINISHED:
           facadeService.removeRunning(taskContext);
           unAssignTask(taskId);
           statisticManager.taskRunSuccessfully();
           break;
       case TASK_KILLED:
           log.warn("task id is: {}, status is: {}, message is: {}, source is: {}", taskId, taskStatus.getState(), taskStatus.getMessage(), taskStatus.getSource());
           facadeService.removeRunning(taskContext);
           facadeService.addDaemonJobToReadyQueue(jobName);
           unAssignTask(taskId);
           break;
       case TASK_LOST:
       case TASK_DROPPED:
       case TASK_GONE:
       case TASK_GONE_BY_OPERATOR:
       case TASK_FAILED:
       case TASK_ERROR:
           log.warn("task id is: {}, status is: {}, message is: {}, source is: {}", taskId, taskStatus.getState(), taskStatus.getMessage(), taskStatus.getSource());
           facadeService.removeRunning(taskContext);
           facadeService.recordFailoverTask(taskContext);
           unAssignTask(taskId);
           statisticManager.taskRunFailed();
           break;
       case TASK_UNKNOWN:
       case TASK_UNREACHABLE:
           log.error("task id is: {}, status is: {}, message is: {}, source is: {}", taskId, taskStatus.getState(), taskStatus.getMessage(), taskStatus.getSource());
           statisticManager.taskRunFailed();
           break;
       default:
           break;
   }
}複製程式碼
  • 當更新 Mesos 任務狀態為 TASK_RUNNING 時,根據附帶訊息為 "BEGIN""COMPLETE",分別呼叫 FacadeService#updateDaemonStatus(false / true) 方法,更新作業閒置狀態。實現程式碼如下:

      // FacadeService.java
      /**
      * 更新常駐作業執行狀態.
      * 
      * @param taskContext 任務執行時上下文
      * @param isIdle 是否空閒
      */
      public void updateDaemonStatus(final TaskContext taskContext, final boolean isIdle) {
         runningService.updateIdle(taskContext, isIdle);
      }
    
      // RunningService.java
      /**
      * 更新作業閒置狀態.
      * @param taskContext 任務執行時上下文
      * @param isIdle 是否閒置
      */
      public void updateIdle(final TaskContext taskContext, final boolean isIdle) {
         synchronized (RUNNING_TASKS) {
             Optional<TaskContext> taskContextOptional = findTask(taskContext);
             if (taskContextOptional.isPresent()) {
                 taskContextOptional.get().setIdle(isIdle);
             } else {
                 add(taskContext);
             }
         }
      }複製程式碼

    若作業配置不存在時,呼叫 SchedulerDriver#killTask(...) 方法,殺死該 Mesos 任務。在《Elastic-Job-Cloud 原始碼分析 —— 作業排程(二)》進一步解析。

  • 當更新 Mesos 任務狀態為 TASK_FINISHED 時,呼叫 FacadeService#removeRunning(...) 方法,將任務從執行時佇列刪除。實現程式碼如下:

      // FacadeService.java
      /**
      * 將任務從執行時佇列刪除.
      *
      * @param taskContext 任務執行時上下文
      */
      public void removeRunning(final TaskContext taskContext) {
         runningService.remove(taskContext);
      }
    
      // RunningService.java
      /**
      * 將任務從執行時佇列刪除.
      * 
      * @param taskContext 任務執行時上下文
      */
      public void remove(final TaskContext taskContext) {
         // 移除執行中的任務集合
         getRunningTasks(taskContext.getMetaInfo().getJobName()).remove(taskContext);
         // 判斷是否為常駐任務
         if (!isDaemonOrAbsent(taskContext.getMetaInfo().getJobName())) {
             return;
         }
         // 將任務從執行時佇列刪除
         regCenter.remove(RunningNode.getRunningTaskNodePath(taskContext.getMetaInfo().toString()));
         String jobRootNode = RunningNode.getRunningJobNodePath(taskContext.getMetaInfo().getJobName());
         if (regCenter.isExisted(jobRootNode) && regCenter.getChildrenKeys(jobRootNode).isEmpty()) {
             regCenter.remove(jobRootNode);
         }
      }複製程式碼
    • 當該作業對應的所有 Mesos 任務狀態都更新為 TASK_FINISHED 後,作業可以再次被 Elastic-Job-Cloud-Scheduler 排程。

      呼叫 #unAssignTask(...) 方法,通知 TaskScheduler 任務被確認未分配到這個主機。TaskScheduler 做任務和 Offer 的匹配,對哪些任務執行在哪些主機是有依賴的,不然怎麼做匹配優化呢。在《Fenzo Wiki —— Notify the Scheduler of Assigns and UnAssigns of Tasks》可以進一步瞭解。實現程式碼如下:

      private void unAssignTask(final String taskId) {
        String hostname = facadeService.popMapping(taskId);
        if (null != hostname) {
            taskScheduler.getTaskUnAssigner().call(TaskContext.getIdForUnassignedSlave(taskId), hostname);
        }
      }複製程式碼
  • 當更新 Mesos 任務狀態為 TASK_KILLED 時,呼叫 FacadeService#addDaemonJobToReadyQueue(...) 方法,將常駐作業放入待執行佇列。在《Elastic-Job-Cloud 原始碼分析 —— 作業排程(二)》進一步解析。TODO

    另外會呼叫 FacadeService#removeRunning(...)#unAssignTask(...) 方法。

  • 當更新 Mesos 任務狀態為 TASK_ERROR 等等時,呼叫 FacadeService#recordFailoverTask(...) 方法,在 《Elastic-Job-Cloud 原始碼分析 —— 作業失效轉移》詳細解析。

    另外會呼叫 FacadeService#removeRunning(...)#unAssignTask(...) 方法。

666. 彩蛋

旁白君:真的真的真的,好長好長好長啊。但是真的真的真的,乾貨!
芋道君:那必須的!

道友,趕緊上車,分享一波朋友圈!

相關文章