源码分析 Datax 日志监控数据

1,431 阅读8分钟

2021 的第二篇文章

内容简述

本文内容主要讲述关于 Datax 的日志审计功能。

日志是应用运行情况以及缺陷排错的不可缺少的利器. 而对于 Datax 这样具备大数据量传输、需要极致压缩机器性能、实时传输以及脏数据记录等功能的数据传输工具来说的话, 日志功能显得尤为重要。

设计解释

在 Datax 的日志中, 内容包括了以下但不限于传输速度、 Reader\Writer 性能、进程、CPU、JVM 和 GC 等情况. 从上面列举出来的方向来看, 我们不难看出涉及到的维度已经从大到小, 从 CPU 到进程, 从任务到更加细小颗粒度的任务...

所以实际上为了囊括这些内容, Datax 会分为两个不同的层次去进行日志监控:

  1. CPU、JVM、GC 等
  2. 任务级别

而任务级别又会被拆分成为

  1. Job 任务
  2. TaskGroup 测试任务组
  3. Task 单个任务

Task 单个任务又会有更多细分的颗粒度:

  1. Reader\Writer 的 init 所需时间
  2. Reader\Writer 的 Prepare 所需时间
  3. Reader\Writer 的处理 Data 所需时间
  4. Reader\Writer 其他事件所需时间 ....
  5. 同时还有自定义事件的时间... 这个一般是自己开发定制

Task 单个任务继续拆分的还有传输速率、各项指标的综合以及平均数.

在我看来,Datax 也会在设计上将两个不同的层次在代码层面上也是两个层面。所以在代码层面上,Datax 有这样的设计:

  1. AbstractContainerCommunicator 是负责收集整个任务的运行信息;其子类 TGContainerCommunicator 是收集 taskGroupContainer 的汇报任务组信息,其子类 JobContainerCommunicator 是汇报整个任务级别的信息
  2. PerfTrace 相当于单个 jvm 的链路追踪信息器,而 PerfRecord 则是 PerfTrace 中的数据。

从代码设计来看,不难看出 PerfTrace 适用于 jvm 运行的时候,实时进行日志记录;而 Communicator 则是充当对外信息的汇报器,可以实时也可以定时进行一个日志以及监控数据的输出。

源码解释

AbstractContainerCommunicator

先看下 AbstractContainerCommunicator 的属性

    private Configuration configuration; //①
    private AbstractCollector collector; //②
    private AbstractReporter reporter;   //③

    private Long jobId;     //④           

    private VMInfo vmInfo = VMInfo.getVmInfo();    //⑤

代码①代表着任务配置
代码②代表着信息收集器
代表③代表着信息汇报器
代码④代表着任务标识
代码⑤代表着 jvm、进程级别的监控

初始化

Communicator 的初始化包括了 Job 以及 TaskGroup 的初始化。Job 的代码入口是 JobContainer 开始调度的过程中实例化 scheduler,同时进行设置 Communicator。

    private void schedule() {
        scheduler = initStandaloneScheduler(this.configuration);  
    }
    
    private AbstractScheduler initStandaloneScheduler(Configuration configuration) {
        AbstractContainerCommunicator containerCommunicator = new StandAloneJobContainerCommunicator(configuration);
        super.setContainerCommunicator(containerCommunicator);

        return new StandAloneScheduler(containerCommunicator);
    }

而 StandAloneJobContainerCommunicator 指的是面对单机独立的 JobContainer。实例化 StandAloneJobContainerCommunicator,同时设置信息收集器以及汇报器。

    public StandAloneJobContainerCommunicator(Configuration configuration) {
        super(configuration);
        super.setCollector(new ProcessInnerCollector(configuration.getLong(
                CoreConstant.DATAX_CORE_CONTAINER_JOB_ID)));
        super.setReporter(new ProcessInnerReporter());
    }

而 TaskGroup 的初始化,是在 TaskGroupContainer 初始化的时候,同时会实例化一个 StandaloneTGContainerCommunicator 并赋值其成员属性。

    public TaskGroupContainer(Configuration configuration) {
        super(configuration);

        initCommunicator(configuration);

        //...
    }
    
    private void initCommunicator(Configuration configuration) {
        super.setContainerCommunicator(new StandaloneTGContainerCommunicator(configuration));
    }

所以不难看出,Datax 对于 Job 和 TaskGroup 两个不同层面使用了不同的 Communicator。所以接下来我们将实际检测的过程中,也会分开来讲。

注册

当 Communicator 与 scheduler 实例完,那么可以开始使用了。首先将拆分后的 Configurations 在 Communicator 进行注册。

     this.containerCommunicator.registerCommunication(configurations);

注册调用的 AbstractCollector#registerTGCommunication 方法。逻辑为:循环 Configurations,获取其 taskGroupId,每个 taskGroup 都会有一个 Communication

    public void registerTGCommunication(List<Configuration> taskGroupConfigurationList) {
        for (Configuration config : taskGroupConfigurationList) {
            int taskGroupId = config.getInt(
                    CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_ID);
            LocalTGCommunicationManager.registerTaskGroupCommunication(taskGroupId, new Communication());
        }
    }

逻辑为:循环 Configurations,获取其 taskGroupId,每个 taskGroup 都会有一个 Communication。而 LocalTGCommunicationManager 负责保存这些信息

使用流程

使用流程指的是 Communicator 在实际框架代码中的运用。

JobContainerCommunicator

Job 的监控层面位于 AbstractScheduler#schedule 方法。在调度的过程中,监控涉及的步骤有:

  1. 收集 Job 的运行情况
  2. 获取各个 TaskGroup 的运行信息,然后汇报

首先是收集 Job 的运行情况,调用 collect 方法

    Communication nowJobContainerCommunication = this.containerCommunicator.collect();

然后实际上调用链是

StandAloneJobContainerCommunicator#collect
ProcessInnerCollector#collectFromTaskGroup
LocalTGCommunicationManager#getJobCommunication

然后获取各个 TaskGroup 的运行信息,然后汇报。

    this.containerCommunicator.report(reportCommunication);

然后实际上调用链是

StandAloneJobContainerCommunicator#report
AbstractReporter#reportJobCommunication

TaskGroupContainerCommunicator

TaskGroup 的监控层面位于 TaskGroupContainer#start 方法。在过程中,监控涉及的步骤有:

  1. 是将同个 TaskGroup 的 Task 都注册进如 TGCommunicator
  2. 从 TGCommunicator 获取 Task 与 Communication 的映射,循环判断进行计数

首先是注册 TaskGroup 的 Task。

  this.containerCommunicator.registerCommunication(taskConfigs);

然后上面调用的是 Communicator#registerCommunication

再调用 AbstractCollector#registerTaskCommunication

    public void registerTaskCommunication(List<Configuration> taskConfigurationList) {
        for (Configuration taskConfig : taskConfigurationList) {
            int taskId = taskConfig.getInt(CoreConstant.TASK_ID);
            this.taskCommunicationMap.put(taskId, new Communication());
        }
    }

然后是从 TGCommunicator 获取 Task 与 Communication 的映射,循环判断。由于代码篇幅过长,我进行代码简化。

while (true) {
    //①
    Map<Integer, Communication> communicationMap = containerCommunicator.getCommunicationMap();
    //②
    for(Map.Entry<Integer, Communication> entry : communicationMap.entrySet()){
        if(taskCommunication.getState() == State.FAILED){   //③
            containerCommunicator.resetCommunication(taskId); 
            
        }else if(taskCommunication.getState() == State.KILLED){  //④
            failedOrKilled = true;
            break;
        }else if(taskCommunication.getState() == State.SUCCEEDED){  //⑤
            //...
            PerfRecord.addPerfRecord(taskGroupId, taskId, PerfRecord.PHASE.TASK_TOTAL,taskStartTime, usedTime * 1000L * 1000L);
            //...
        }
    }
   
    //⑥
    if (failedOrKilled) {
        lastTaskGroupContainerCommunication = reportTaskGroupCommunication(
                            lastTaskGroupContainerCommunication, taskCountInThisTaskGroup);
    }
    
    //⑦
    while(iterator.hasNext() && runTasks.size() < channelNumber){
        //...
        TaskExecutor lastExecutor = taskFailedExecutorMap.get(taskId);
        if(lastExecutor!=null){
            //...
            if(!lastExecutor.isShutdown()){ 
                if(now - failedTime > taskMaxWaitInMsec){
                    markCommunicationFailed(taskId);
                    reportTaskGroupCommunication(lastTaskGroupContainerCommunication, taskCountInThisTaskGroup);
                    //throw Exception
                }else {
                    
                }
            }
        }
        
        //...
        TaskExecutor taskExecutor = new TaskExecutor(taskConfigForRun, attemptCount);
        taskExecutor.doStart();
        //...
        taskMonitor.registerTask(taskId, this.containerCommunicator.getCommunication(taskId));
    }
    //⑧
    if (taskQueue.isEmpty() && isAllTaskDone(runTasks) && containerCommunicator.collectState() == State.SUCCEEDED) {
        lastTaskGroupContainerCommunication = reportTaskGroupCommunication(
                            lastTaskGroupContainerCommunication, taskCountInThisTaskGroup);
    }
    
    //⑨
    long now = System.currentTimeMillis();
    if (now - lastReportTimeStamp > reportIntervalInMillSec) {
        lastTaskGroupContainerCommunication = reportTaskGroupCommunication(
                            lastTaskGroupContainerCommunication, taskCountInThisTaskGroup);
                            
        for(TaskExecutor taskExecutor:runTasks){
             taskMonitor.report(taskExecutor.getTaskId(),this.containerCommunicator.getCommunication(taskExecutor.getTaskId()));
        }
    }
}

代码①,首先获取所有已注册的 Task 的 Communication
代码②,循环 Communication,对各种状态进行一个对应的处理
代码③,如果是 FAILED 校验是否符合重试条件
代码④,发现有 TaskExeccutor 处于 KILLED 则直接跳出循环
代码⑤,如果是成功就记录成功的数据
代码⑥,如果 failedOrKilled=true,则汇报数据
代码⑦,有任务未执行,且正在运行的任务数小于最大通道限制。这里主要会检测是否未执行的失败任务中,是否还是无法运行。如果无法运行就需要汇报数据
代码⑧,如果所有任务执行成功的话,那么也进行汇报数据
代码⑨,定时去汇报每一个任务的执行情况,相当于将 TaskMonitor 的数据刷到 Communicator

这段代码是 TaskGroup 在对其 Task 的运行情况进行监控。而其中最频繁的是 reportTaskGroupCommunication 方法,总的来说他其实就是一个更新统计数据的方法

    private Communication reportTaskGroupCommunication(Communication lastTaskGroupContainerCommunication, int taskCount){
        Communication 
        //①
        nowTaskGroupContainerCommunication = this.containerCommunicator.collect();
        nowTaskGroupContainerCommunication.setTimestamp(System.currentTimeMillis());
        //②
        Communication reportCommunication = CommunicationTool.getReportCommunication(nowTaskGroupContainerCommunication,
                lastTaskGroupContainerCommunication, taskCount);
        //③
        this.containerCommunicator.report(reportCommunication);
        return reportCommunication;
    }

代码①,获取目前最新的运行情况 代码②,将最新的数据与旧的数据进行计数 代码③,重新进行汇报数据最后返回

PS: CommunicationTool 是 Datax 的统计工具,有兴趣的可以去了解一下

PerfTrace 与 PerfRecord

上面讲了容器级别的一个日志监控,事实上我们还有一个传输事件级别的监控。什么是传输事件级别的监控?实际上就是对 Reader 和 Writer 中划分的各个阶段的监控,监控的方向大致上是耗时、速率或者统计数据等等。

所以在 Datax 中,PerfTrace 和 PerfRecord 是承担了这个实现的责任。PerfTrace 可以理解为微服务的链路追踪的数据统计。它记录 job(local模式),taskGroup(distribute模式),因为这 2 种都是 jvm,即一个 jvm 里只需要有 1 个 PerfTrace。

那实际上 PerfRecord 允许记录什么数据呢?下面我放一张比较详细、目前框架支持的统计数据的纬度。

public enum PHASE {

    //task total运行的时间,前10为框架统计,后面为部分插件的个性统计
    TASK_TOTAL(0),

    //Reader init、prepare、data、post 以及 destroy
    READ_TASK_INIT(1),
    READ_TASK_PREPARE(2),
    READ_TASK_DATA(3),
    READ_TASK_POST(4),
    READ_TASK_DESTROY(5),

    //Writer init、prepare、data、post 以及 destroy
    WRITE_TASK_INIT(6),
    WRITE_TASK_PREPARE(7),
    WRITE_TASK_DATA(8),
    WRITE_TASK_POST(9),
    WRITE_TASK_DESTROY(10),

    //SQL_QUERY: sql query阶段, 部分reader的个性统计
    SQL_QUERY(100),

    //数据从sql全部读出来
    RESULT_NEXT_ALL(101),
}

那么 PerfTrace 与 PerfRecord 是怎么在实际代码中实践的呢? 我以 ReaderRunner 为例子。ReaderRunner#start 里面需要统计 Reader.Task 的 init、start、end 或者 destroy 方法,所以利用 PerfRecord 来进行统计。

首先是调用 PerfRecord#start 的时候,会将自己注册进 PerfTrace,这样子就可以进一步追踪。

    public void start() {
        if(PerfTrace.getInstance().isEnable()) {
            this.startTime = new Date();
            this.startTimeInNs = System.nanoTime();
            this.action = ACTION.start;
            //在PerfTrace里注册
            PerfTrace.getInstance().tracePerfRecord(this);
            perf.info(toString());
        }
    }

然后开始做实际业务逻辑,例如统计 init 方法

    PerfRecord initPerfRecord = new PerfRecord(getTaskGroupId(), getTaskId(), PerfRecord.PHASE.READ_TASK_INIT);
    initPerfRecord.start();
    taskReader.init();
    initPerfRecord.end();

从代码层面,我们仅需要传入 TaskGroupId、TaskId 以及事件就可以进行一个执行时间段的统计;又例如

    PerfRecord dataPerfRecord = new PerfRecord(getTaskGroupId(), getTaskId(), PerfRecord.PHASE.READ_TASK_DATA);
    dataPerfRecord.start();
    taskReader.startRead(recordSender);
    dataPerfRecord.addCount(CommunicationTool.getTotalReadRecords(super.getRunnerCommunication()));
    dataPerfRecord.addSize(CommunicationTool.getTotalReadBytes(super.getRunnerCommunication()));
    dataPerfRecord.end();

只需要通过 addCount、addSize 就可以完成数量的统计。但是同时你也发现了,CommunicationTool 不就是那个前面说的数据统计的吗? 是的,实际上 ReaderRunner 自身有一个 Communication 属性,用于统计对应相关的数据。

任务结束时,对当前的 PerfRecord 汇总统计。Job 和 TaskGroup 统计的节点都是在 finally 的时候调用 PerfTrace#summarizeNoException 这时候会进行计算并打印。

首先如果 PerfRecord 的数量大于 0 则汇总打印

    if (totalEndReport.size() > 0) {
        sumPerf4EndPrint(totalEndReport);
    }

接着循环计算 taskGroup 中各个阶段的事件的平均耗时、任务数、最大用时以及耗时最长 task

    for (PHASE phase : keys) {
        SumPerfRecord4Print sumPerfRecord = perfRecordMaps4print.get(phase);
        if (sumPerfRecord == null) {
            continue;
        }
        long averageTime = sumPerfRecord.getAverageTime();
        long maxTime = sumPerfRecord.getMaxTime();
        int maxTaskId = sumPerfRecord.maxTaskId;
        int maxTaskGroupId = sumPerfRecord.getMaxTaskGroupId();
        info.append(String.format("%-20s | %18s | %18s | %18s | %18s | %-100s\n",
                phase, unitTime(averageTime), sumPerfRecord.totalCount, unitTime(maxTime), jobId + "-" + maxTaskGroupId + "-" + maxTaskId, taskDetails.get(maxTaskId)));
    }

最后计算任务执行的平均记录数、平均字节、速率、最大字节数等等数据

    long averageRecords = countSumPerf.getAverageRecords();
    long averageBytes = countSumPerf.getAverageBytes();
    long maxRecord = countSumPerf.getMaxRecord();
    long maxByte = countSumPerf.getMaxByte();
    int maxTaskId4Records = countSumPerf.getMaxTaskId4Records();
    int maxTGID4Records = countSumPerf.getMaxTGID4Records();

    info.append("\n\n 2. record average count and max count task info :\n\n");
    info.append(String.format("%-20s | %18s | %18s | %18s | %18s | %18s | %-100s\n", "PHASE", "AVERAGE RECORDS", "AVERAGE BYTES", "MAX RECORDS", "MAX RECORD`S BYTES", "MAX TASK ID", "MAX TASK INFO"));
    if (maxTaskId4Records > -1) {
        info.append(String.format("%-20s | %18s | %18s | %18s | %18s | %18s | %-100s\n"
                , PHASE.READ_TASK_DATA, averageRecords, unitSize(averageBytes), maxRecord, unitSize(maxByte), jobId + "-" + maxTGID4Records + "-" + maxTaskId4Records, taskDetails.get(maxTaskId4Records)));

    }

文末

这篇文章只是较浅的说了一下关于 Datax 的日志监控功能,后续其实可以在日志数据监控方面有更多的定制以及设想。