DataX 3.0 原始碼解析一

f發表於2020-10-19

原始碼解析

基本呼叫類分析

任務啟動由python指令碼新建程式進行任務執行,後續執行由Java進行,以下將對java部分進行分
其中的呼叫原理機制。

Engine

首先入口類為com.alibaba.datax.core.Enginemain方法,其中通過呼叫其本身的靜態方法entry,該方法主要針對輸入參入進行格式化以及校驗:

BasicParser parser = new BasicParser();
CommandLine cl = parser.parse(options, args);

其中需要注意執行模式是通過RUNTIME_MODE = cl.getOptionValue("mode")程式碼直接賦值給靜態變數的,針對命令列引數採用了org.apache.commonsBasicParser解析,針對任務的配置檔案則通過其本身的ConfigParser進行解析(可以支援本地和網路檔案)。

完成配置初始化後該方法將例項化本身並呼叫其start方法,將初始化好的配置物件傳入其中。該方法首先將型別轉換進行初始化,以保證在後續資料匯入匯出中不相容型別可以進行順利的轉換工作,具體通過ColumnCast.bind(configuration)方法進行繫結,其中主要針對三種型別進行初始化工作:

StringCast.init(configuration);
DateCast.init(configuration);
BytesCast.init(configuration);

接著就是利用LoadUtil.Bind(pluginCoonfigs)儲存外掛,便於後續讀取外掛相關配置資訊內容。剩下就是該函式的核心流程,即判斷當前的任務執行模式,是TaskGroup還是Job模式。但通過實際分析來看基本都是Job模式,所以後續我們主要以JobContainer為切入點,另一個則為TaskGroupContainer。兩者均繼承自AbstractContainer基類,並通過呼叫他們的start方法進行啟動。

對於非Standlone模式還支援記錄任務進度情況,進行彙報的功能。具體由最後例項化的PerfTrace類實現。

JobContainer

首先該類的建構函式中僅初始化了ErrorRecordChecker類用於檢查任務是否到達錯誤記錄限制。而主要的執行則落在了start方法中,細心讀者可以發現其中讀取了job.setting.dryRun配置引數,判斷是否需要執行預檢查(preCheck)。正常工作流程則如下所示:

  1. preHandle
    Job前置操作,即初始化preHandler外掛並執行其preHandler;
AbstractJobPlugin handler = LoadUtil.loadJobPlugin(
        handlerPluginType, handlerPluginName);
// todo...
handler.preHandler(configuration);
  1. init

初始化reader和writer,實際方法中根據讀寫外掛各自執行了對應的初始化方法,具體程式碼如下所示。

this.jobReader = this.initJobReader(jobPluginCollector);
this.jobWriter = this.initJobWriter(jobPluginCollector);

其中各方法均類似,就是讀取對應載入對應的外掛物件並呼叫外掛對應的方法進行相關配置的設定以及對應方法的初始化,選取initJobReader方法中的部分程式碼片段如下:

Reader.Job jobReader = (Reader.Job) LoadUtil.loadJobPlugin(
        PluginType.READER, this.readerPluginName);
// todo...
jobReader.setJobPluginCollector(jobPluginCollector);
jobReader.init();
  1. prepare
    全域性準備工作,比如odpswriter清空目標表。由於讀寫外掛的特殊性質,其方法內部主要也是執行了各型別外掛的方法來實現準備工作。
this.prepareJobReader();
this.prepareJobWriter();

其中各自方法的差異性較小,主要就是例項化外掛然後直接呼叫其對應的prepare即可。

  1. split

拆分Task,引數adviceNumber為建議的拆分數。除此之外我們還可以通過位元組和事務的限速來進行控制,從而決定Channel的數量。具體配置引數如下:

  • job.setting.speed.byte:總BPS限速,如果存在值則單個Channel的BPS不能為空,通過總限速除以單個Channel限速得出Channel的需求數量;
  • core.transport.channel.speed.byte:單個Channel的BPS限速;
  • job.setting.speed.record:總TPS限速,如果存在則單個Channel的TPS不能為空,通過總限速除以單個Channel限速得出Channel的需求數量;
  • core.transport.channel.speed.record:單個Channel的TPS限速;

如果兩個限速均存在則取值最少的那一個,如果兩者都沒有設定則通過job.setting.speed.channel引數獲取,最終決定needChannelNumber引數。根據得出的引數進行Reader與Writer的拆分。

List<Configuration> readerTaskConfigs = this
        .doReaderSplit(this.needChannelNumber);
int taskNumber = readerTaskConfigs.size();
List<Configuration> writerTaskConfigs = this
        .doWriterSplit(taskNumber);

以上這兩種方法大同小異,只是內部讀取的外掛不同,這裡我們就以Reader為例進行說明。內部例項化好對應外掛後,通過外掛Job的split方法進行實際切分。

List<Configuration> readerSlicesConfigs =
        this.jobReader.split(adviceNumber);

而實際的切分則需要由外掛開發人員通過實現Job的split方法來滿足,該方法將返回Configuration列表,最終將會把reader和writer以及配置項job.content[0].transformer重新整合成contentconfig並作為變數configuration中Key為job.content的值,從而便於將其傳遞至各Task中。

List<Configuration> contentConfig = mergeReaderAndWriterTaskConfigs(
        readerTaskConfigs, writerTaskConfigs, transformerList);
  1. schedule

完成任務的切換後將開始執行任務。由於實際任務是由TaskGroupContainer執行,為此我們還需要劃分對應TaskGroup需要執行的Task,該引數通過core.container.taskGroup.channel進行配置,預設為5。決定每個Group執行那些Task的則由以下方法進行決定,將直接返回對應任務組的配置引數。

List<Configuration> taskGroupConfigs = JobAssignUtil.assignFairly(this.configuration,
        this.needChannelNumber, channelsPerTaskGroup);

關於該方法的具體剖析可以跳轉到本篇幅

完成任務分配後我們就需要根據執行模式決定排程器,通過這裡的原始碼可以明顯看出其DataX 3.0是經過了閹割,僅保留了單機執行模式。

executeMode = ExecuteMode.STANDALONE;
scheduler = initStandaloneScheduler(this.configuration);

故後續我們僅能描述單機模式下關於任務排程的工作原理。首先是排程器初始化的核心方法initStandaloneScheduler,其方法主要是初始化了StandAloneJobContainerCommunicator類用於通訊(其中collect由ProcessInnerCollector提供,reporter由ProcessInnerReporter提供),StandAloneScheduler則為實際排程器。具體的說明請跳轉到本篇幅。最終將對應的配置資訊傳入排程器中進行執行就完成了。

scheduler.schedule(taskGroupConfigs);

ProcessInnerCollector

AbstractSchedulerschedule中通過StandAloneJobContainerCommunicator類呼叫了其collect方法,而其方法的背後則是其他類對應的方法。

public Communication collect() {
    return super.getCollector().collectFromTaskGroup();
}

該類為ProcessInnerCollector類,其對應的方法依然是LocalTGCommunicationManager靜態類其中一個靜態方法。

public Communication collectFromTaskGroup() {
    return LocalTGCommunicationManager.getJobCommunication();
}

其內部也是將之前每個TaskGroup所建立的Communication維護了一個靜態字典並在需要的時候進行合併。

public static Communication getJobCommunication() {
    Communication communication = new Communication();
    communication.setState(State.SUCCEEDED);
    for (Communication taskGroupCommunication :
            taskGroupCommunicationMap.values()) {
            communication.mergeFrom(taskGroupCommunication);
    }
    return communication;
}

ProcessInnerReporter

AbstractSchedulerschedule中通過StandAloneJobContainerCommunicator類呼叫了其report方法,而其方法的背後則是其他類對應的方法。

public void report(Communication communication) {
    super.getReporter().reportJobCommunication(super.getJobId(), communication);

    LOG.info(CommunicationTool.Stringify.getSnapshot(communication));
    reportVmInfo();
}

而Reporter物件則為ProcessInnerReporter類,對應的方法則是該類的reportJobCommunication方法,其本身也是呼叫了其他靜態類的靜態方法進行實現。

public void reportJobCommunication(Long jobId, Communication communication) {
        // do nothing
}

可以看到當前原始碼並沒有是實現輸出Job的統計資訊。

JobAssignUtil.assignFairly

該方法首先通過Channel數量除以每個TaskGroup可以處理的Channel數量從而得出TaskGroup數量。在實際切分中考慮到Shuffle的成本,外掛開發者可以通過reader.parameter.loadBalanceResourceMarkwriter.parameter.loadBalanceResourceMark來劃定每個Task的標識,從而便於在分配任務時將對應標識的Reader與Writer分配到同一個TaskGroup中,如果不存在則會自動設定一個預設的標識。

String readerResourceMark = aTaskConfig.getString(CoreConstant.JOB_READER_PARAMETER + "." +
        CommonConstant.LOAD_BALANCE_RESOURCE_MARK);
String writerResourceMark = aTaskConfig.getString(CoreConstant.JOB_WRITER_PARAMETER + "." +
        CommonConstant.LOAD_BALANCE_RESOURCE_MARK);

boolean hasLoadBalanceResourceMark = StringUtils.isNotBlank(readerResourceMark) ||
        StringUtils.isNotBlank(writerResourceMark);

if (!hasLoadBalanceResourceMark) {
    for (Configuration conf : contentConfig) {
        conf.set(CoreConstant.JOB_READER_PARAMETER + "." +
                CommonConstant.LOAD_BALANCE_RESOURCE_MARK, "aFakeResourceMarkForLoadBalance");
    }
    Collections.shuffle(contentConfig, new Random(System.currentTimeMillis()));
}

根據資源標識將開始將根據資源標識將對應的Task進行切換,其主要由parseAndGetResourceMarkAndTaskIdMap方法進行分配,其內部就是根據資源標識維護一個字典,如果是預設標識則字典僅有一個物件,所有的Task都歸屬其中。

LinkedHashMap<String, List<Integer>> resourceMarkAndTaskIdMap = parseAndGetResourceMarkAndTaskIdMap(contentConfig);

根據標識完成分組後就需要將Task配置按照TaskGroup進行分配,以滿足呼叫的需要,這裡通過呼叫doAssign方法來滿足。其方法主要先獲取到按照標識分組後其中最大組的成員數量mapValueMaxLength,並與標識數採用進行2層迴圈將各個task配置儲存到對應分組編號的資料中。

for (int i = 0; i < mapValueMaxLength; i++) {
    for (String resourceMark : resourceMarks) {
        if (resourceMarkAndTaskIdMap.get(resourceMark).size() > 0) {
            int taskId = resourceMarkAndTaskIdMap.get(resourceMark).get(0);
            taskGroupConfigList.get(taskGroupIndex % taskGroupNumber).add(contentConfig.get(taskId));
            taskGroupIndex++;

            resourceMarkAndTaskIdMap.get(resourceMark).remove(0);
        }
    }
}

以上進行以陣列的形式進行分配,而實際需要使用Configuration物件,為此我們還需要將以上資訊重新組織儲存到對應的配置物件中,具體結構可以參考如下原始碼:

for (int i = 0; i < taskGroupNumber; i++) {
    tempTaskGroupConfig = taskGroupTemplate.clone();
    tempTaskGroupConfig.set(CoreConstant.DATAX_JOB_CONTENT, taskGroupConfigList.get(i));
    tempTaskGroupConfig.set(CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_ID, i);

    result.add(tempTaskGroupConfig);
}

上述方法雖然完成了最終的任務,但是實際每個TaskGroup所分配到的Task並不是平均的,這就導致對應的TaskGroup的Channel也是不均衡的,為了便於後期的優化,我們還需要將對應TaskGroup所需的Channel數量存入到core.container.taskGroup.channel配置項中。

StandAloneScheduler

該類本身並沒有太多實質性的內容,具體的功能內容更多的在其父類ProcessInnerSchedulerAbstractScheduler中,關於該兩個類的說明將直接在本篇幅中進行概述,不新起篇章。

我們以schedule的呼叫順蘇為例進行說明,首先獲取用於彙報的時間間隔,分別為core.container.job.reportIntervalcore.container.job.sleepInterval引數,前者為每次彙報的時間間隔,預設為30秒,後者為每次睡眠時間,即每次彙總採集的間隔時間。

由於任務的執行無法避免錯誤的出現,為了保障任務的成功執行,在每次彙報的同時還增加了額外的錯誤檢查機制,通過髒資料出現的次數與比率進行判斷,從而中止任務的繼續。

errorLimit = new ErrorRecordChecker(configurations.get(0));

// to do...

errorLimit.checkRecordLimit(nowJobContainerCommunication);

其通過ErrorRecordChecker類提供,該類通過recordLimit檢查條數與percentageLimit百分比檢查任務是否到達錯誤記錄的限制,對應的限制通過讀取配置中的job.setting.errorLimit.recordjob.setting.errorLimit.percentage引數。對於任務的執行最核心的當然是startAllTaskGroup方法了,該方法位於ProcessInnerScheduler類中。

該方法直接利用Java本身的Executors.newFixedThreadPool方法建立了分組數的執行緒池資源,然後通過將TaskGroupContainer物件包裝到TaskGroupContainerRunner物件中來進行執行。

TaskGroupContainer taskGroupContainer = new TaskGroupContainer(configuration);

return new TaskGroupContainerRunner(taskGroupContainer);

TaskGroupContainerRunner內部的run實際依然是呼叫了對應TaskGroupContainer物件的start方法。而關於該類的說明將會另啟篇幅進行具體說明。

相關文章