2021 的第二篇文章
内容简述
本文内容主要讲述关于 Datax 的日志审计功能。
日志是应用运行情况以及缺陷排错的不可缺少的利器. 而对于 Datax 这样具备大数据量传输、需要极致压缩机器性能、实时传输以及脏数据记录等功能的数据传输工具来说的话, 日志功能显得尤为重要。
设计解释
在 Datax 的日志中, 内容包括了以下但不限于传输速度、 Reader\Writer 性能、进程、CPU、JVM 和 GC 等情况. 从上面列举出来的方向来看, 我们不难看出涉及到的维度已经从大到小, 从 CPU 到进程, 从任务到更加细小颗粒度的任务...
所以实际上为了囊括这些内容, Datax 会分为两个不同的层次去进行日志监控:
- CPU、JVM、GC 等
- 任务级别
而任务级别又会被拆分成为
- Job 任务
- TaskGroup 测试任务组
- Task 单个任务
Task 单个任务又会有更多细分的颗粒度:
- Reader\Writer 的 init 所需时间
- Reader\Writer 的 Prepare 所需时间
- Reader\Writer 的处理 Data 所需时间
- Reader\Writer 其他事件所需时间 ....
- 同时还有自定义事件的时间... 这个一般是自己开发定制
Task 单个任务继续拆分的还有传输速率、各项指标的综合以及平均数.
在我看来,Datax 也会在设计上将两个不同的层次在代码层面上也是两个层面。所以在代码层面上,Datax 有这样的设计:
- AbstractContainerCommunicator 是负责收集整个任务的运行信息;其子类 TGContainerCommunicator 是收集 taskGroupContainer 的汇报任务组信息,其子类 JobContainerCommunicator 是汇报整个任务级别的信息
- 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 方法。在调度的过程中,监控涉及的步骤有:
- 收集 Job 的运行情况
- 获取各个 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 方法。在过程中,监控涉及的步骤有:
- 是将同个 TaskGroup 的 Task 都注册进如 TGCommunicator
- 从 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 的日志监控功能,后续其实可以在日志数据监控方面有更多的定制以及设想。