datax值转换使用以及源码分析

一、DataX的数据传输基础

在这里插入图片描述
跟一般的生产者-消费者模式一样,Reader插件和Writer插件之间也是通过channel来实现数据的传输的。channel可以是内存的,也可能是持久化的,插件不必关心。插件通过RecordSender往channel写入数据,通过RecordReceiver从channel读取数据。
 channel中的一条数据为一个Record的对象,Record中可以放多个Column对象,这可以简单理解为数据库中的记录和列。

传输模块transport代码目录:
在这里插入图片描述

1. 通道Channel

抽象类Channel,统计和限速都在这里。

一个DataX Job会切分成多个Task,每个Task会按TaskGroup进行分组,一个Task内部会有一组Reader->Channel->Writer。Channel是连接Reader和Writer的数据交换通道,所有的数据都会经由Channel进行传输。

在DataX内部对每个Channel会有严格的速度控制,分两种,一种是控制每秒同步的记录数,另外一种是每秒同步的字节数,默认的速度限制是1MB/s,可以根据具体硬件情况设置这个byte速度或者record速度,一般设置byte速度,比如:我们可以把单个Channel的速度上限配置为5MB。

当一个Job内Channel数变多后,内存的占用会显著增加,因为DataX作为数据交换通道,在内存中会缓存较多的数据。

注意:
MysqlReader进行数据抽取时,如果指定splitPk,表示用户希望使用splitPk代表的字段进行数据分片,DataX因此会启动并发任务进行数据同步,这样可以大大提供数据同步的效能,splitPk不填写,包括不提供splitPk或者splitPk值为空,DataX视作使用单通道同步该表数据,配置多channel单不配置splitPk测试不出来效果。

官方只提供了一个内存Channel的具体实现,底层其实是一个ArrayBlockingQueue

public class MemoryChannel extends Channel {
...
}

2. TaskGroupContainer中的成员变量Channel分析

对于TaskGroupContainer,每个TaskGroupContainer并发执行的个数由CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_CHANNEL决定。

            // 获取channel数目
            int channelNumber = this.configuration.getInt(
                    CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_CHANNEL);

CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_CHANNEL的值如下显示为5

    private void schedule() {
        /**
         * 这里的全局speed和每个channel的速度设置为B/s
         */
        int channelsPerTaskGroup = this.configuration.getInt(
                CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_CHANNEL, 5);
        int taskNumber = this.configuration.getList(
                CoreConstant.DATAX_JOB_CONTENT).size();

        this.needChannelNumber = Math.min(this.needChannelNumber, taskNumber);
        PerfTrace.getInstance().setChannelNumber(needChannelNumber);

总结:由此可以看出TaskGroupContainer默认并发的task个数是5。

3. Channel配置

DataX 内部对每个 Channel 会做限速,可以限制每秒 byte 数,也可以限制每秒 record 数。除了对每个 Channel 限速,在全局还会有一个速度限制的配置,默认是不限。
1, 配置全局 Byte 限速以及单 Channel Byte 限速,Channel 个数 = 全局 Byte 限速 / 单 Channel Byte 限速。(下面示例中最终 Channel 个数为 10)

"core": {
"transport": {
"channel": {
"speed": {
"byte": 1048576
}
}
}
},
"job": {
"setting": {
"speed": {
"byte" : 10485760
}
},
...
}
}

2,配置全局 Record 限速以及单 Channel Record 限速,Channel 个数 = 全局 Record 限速 / 单 Channel Record 限速。(下面示例中最终 Channel 个数为 3)

"core": {
"transport": {
"channel": {
"speed": {
"record": 100
}
}
}
},
"job": {
"setting": {
"speed": {
"record" : 300
}
},
...
}
}

3, 全局不限速,直接配置 Channel 个数。(下面示例中最终 Channel 个数为 5)

"core": {
"transport": {
"channel": {
"speed": {
"byte": 1048576
}
}
}
},
"job": {
"setting": {
"speed": {
"channel" : 5
}
},
...
}
}

第三种方式最简单直接,但是这样就缺少了全局的限速。在选择 Channel 个数时,同样需要注意,Channel 个数并不是越多越好。Channel 个数的增加,带来的是更多的 CPU 消耗以及内存消耗。如果 Channel 并发配置过高导致 JVM 内存不够用,会出现的情况是发生频繁的 Full GC,导出速度会骤降,适得其反。

可以在 DataX 的输出日志中,找到本次任务的 Channel 的数:关键字:splits to
在这里插入图片描述

注意: 这里对一定要对tasks要有个正确的认识,根据如下代码显示,日志打印中 [1]tasks的tasks其实就是来自CoreConstant.DATAX_JOB_CONTENT(“job.content”) 关键字content 数组的大小。就是说一个task对应一个content。

           List<Configuration> taskConfigs = this.configuration
                    .getListConfiguration(CoreConstant.DATAX_JOB_CONTENT);

            if(LOG.isDebugEnabled()) {
                LOG.debug("taskGroup[{}]'s task configs[{}]", this.taskGroupId,
                        JSON.toJSONString(taskConfigs));
            }
            
            int taskCountInThisTaskGroup = taskConfigs.size();
            LOG.info(String.format(
                    "taskGroupId=[%d] start [%d] channels for [%d] tasks.",
                    this.taskGroupId, channelNumber, taskCountInThisTaskGroup));

二、datax脏数据处理

1. 什么是脏数据?

目前主要有三类脏数据:

  • Reader读到不支持的类型、不合法的值。
  • 不支持的类型转换,比如:Bytes转换为Date。
  • 写入目标端失败,比如:写mysql整型长度超长

2. 如何处理脏数据

AbstractTaskPlugin.getPluginCollector()可以拿到一个TaskPluginCollector,它提供了一系列collectDirtyRecord的方法。当脏数据出现时,只需要调用合适的collectDirtyRecord方法,把被认为是脏数据的Record传入即可。

1)脏数据阈值控制

job.setting.errorLimit(脏数据控制)
Job支持用户对于脏数据的自定义监控和告警,包括对脏数据最大记录数阈值(record值)或者脏数据占比阈值(percentage值),当Job传输过程出现的脏数据大于用户指定的数量/百分比,DataX Job报错退出。

3. 类型转换(DataX的内部类型)

参考: datax源码中 搜 关键字 “”类型转换“”
如下:来自官方文档

为了规范源端和目的端类型转换操作,保证数据不失真,DataX支持六种内部数据类型:

  • Long:定点数(Int、Short、Long、BigInteger等)。
  • Double:浮点数(Float、Double、BigDecimal(无限精度)等)。
  • String:字符串类型,底层不限长,使用通用字符集(Unicode)。
  • Date:日期类型。
  • Bool:布尔值。
  • Bytes:二进制,可以存放诸如MP3等非结构化数据。

对应地,有DateColumnLongColumnDoubleColumnBytesColumnStringColumnBoolColumn六种Column的实现。

Column除了提供数据相关的方法外,还提供一系列以as开头的数据类型转换转换方法。

DataX的内部类型在实现上会选用不同的java类型:

内部类型实现类型备注
Datejava.util.Date
Longjava.math.BigInteger使用无限精度的大整数,保证不失真
Doublejava.lang.String用String表示,保证不失真
Bytesbyte[]
Stringjava.lang.String
Booljava.lang.Boolean

类型之间相互转换的关系如下:

from\toDateLongDoubleBytesStringBool
Date-使用毫秒时间戳不支持不支持使用系统配置的date/time/datetime格式转换不支持
Long作为毫秒时间戳构造Date-BigInteger转为BigDecimal,然后BigDecimal.doubleValue()不支持BigInteger.toString()0为false,否则true
Double不支持内部String构造BigDecimal,然后BigDecimal.longValue()-不支持直接返回内部String
Bytes不支持不支持不支持-按照common.column.encoding配置的编码转换为String,默认utf-8不支持
String按照配置的date/time/datetime/extra格式解析用String构造BigDecimal,然后取longValue()用String构造BigDecimal,然后取doubleValue(),会正确处理NaN/Infinity/-Infinity按照common.column.encoding配置的编码转换为byte[],默认utf-8-"true"为true, "false"为false,大小写不敏感。其他字符串不支持
Bool不支持true1L,否则0Ltrue1.0,否则0.0不支持-

4. 插件是如何在实际的存储类型和DataX的内部类型之间进行转换的

关于具体每个插件 datax源码文档都有描述,具体可以查看每个插件源码目录下的md文件
类型转换
- 插件是如何在实际的存储类型和DataX的内部类型之间进行转换的。
- 以及是否存在特殊处理。

三、DataX自定义transformer

DataX 运行时加载自定义 transformer 插件
参考URL: https://blog.csdn.net/landstream/article/details/88878172

  1. 下载源码,在根目录下找到 transformer 文件夹。
    在这里插入图片描述
    找到抽象类transformer类

  2. 参考已有的transformer类实现接口,按你的需求接收参数,用于从 job 配置文件接收命令。在这里插入图片描述

  3. 在 core\src\main\java\com\alibaba\datax\core\transport\transformer 目录的 TransformerRegistry 类中注册你编写的 transformer 类。

在这里插入图片描述
注意:这种方式,自定义的转换类中的setTransformerName(“dx_substr”);必须以dx_开头,因此如下代码所示registTransformer在调registTransformer时的第三个参数写死时true(代表是不是datax自带方法),这个函数里面是判断如果是datax自带实现方法,那么必须以 dx_substr开头。

    public static synchronized void registTransformer(Transformer transformer) {
        registTransformer(transformer, null, true);
    }

1. 如何定义自定义的转换方法,不在datax源码中

datax其实把转换分成2类,

  • 一类叫 本地(isNative)(转换类自定义在datax中)
  • 一类叫 非本地
    非本地转换容许你定义一个transformer.json,从这个json中读取有用信息。

它首先从datax json 配置 transformer下读取name,然后调
TransformerRegistry.loadTransformerFromLocalStorage(functionNames);

如下代码所示,它是判断name的值(其实是一个目录名)在路径(DATAX_HOME/local_storage/transformer/)下是否存在,如果存在调loadTransformer 加载该转换。

    public static void loadTransformerFromLocalStorage(List<String> transformers) {

        String[] paths = new File(CoreConstant.DATAX_STORAGE_TRANSFORMER_HOME).list();
        if (null == paths) {
            return;
        }

        for (final String each : paths) {
            try {
                if (transformers == null || transformers.contains(each)) {
                    loadTransformer(each);
                }
            } catch (Exception e) {
                LOG.error(String.format("skip transformer(%s) loadTransformer has Exception(%s)", each, e.getMessage()), e);
            }

        }
    }

loadTransformer加载是一个 目录名/transformer.json结尾的 json配置文件。
从该json配置文件中解析要加载转换类jar包,加载该jar包。

因此,自定义非本地函数,写好这个 json配置文件很重要。

四、TransformerUtil执行流程

它封装了一个处理转换的工具类:TransformerUtil,调了TransformerRegistry的两个静态方法,一个是:加载转换从本地,一个是:根据方法名获取转换实例。
TransformerRegistry.loadTransformerFromLocalStorage(functionNames);
和TransformerRegistry.getTransformer(functionName);
在这里插入图片描述

  1. TransformerUtil流程
    1)TransformerUtil该类,只有一个静态方法,它读取datax json配置文件的transformer key下的内容,读到一个List中。即读取所有转换配置到一个list中。
    2) for遍历这个List读取name key,获取函数名List
  1. 遍历函数名list,调TransformerRegistry.loadTransformerFromLocalStorage(functionNames);加载这些类。
    4)再一次遍历配置List,
    获取列索引,
    获取参数,
    组装TransformerExecutionParas实例,该实例用于传递参数给具体转换类。
    又根据transformerExecutionParas捆绑到具体使用该配置的转换实例 TransformerExecution transformerExecution = new TransformerExecution(transformerInfo, transformerExecutionParas);
public static List<TransformerExecution> buildTransformerInfo(Configuration taskConfig){
        List<Configuration> tfConfigs = taskConfig.getListConfiguration(CoreConstant.JOB_TRANSFORMER);

...
}

TransformerExecutionParas

public class TransformerExecutionParas {

    /**
     * 以下是function参数
     */

    private Integer columnIndex;
    private String[] paras;
    private Map<String, Object> tContext;
    private String code;
    private List<String> extraPackage;

大致总结为如下图:
在这里插入图片描述

1. 每个函数或值转换可以重复执行吗?

经过测试:如下dx_substr函数可以调2次,并且针对同一个字段 “columnIndex”:5。

 "transformer": [
                    {
                        "name": "dx_substr",
                        "parameter": 
                            {
                            "columnIndex":5,
                            "paras":["1","3"]
                            }  
                    },
                    {
                        "name": "dx_substr",
                        "parameter": 
                            {
                            "columnIndex":5,
                            "paras":["1","2"]
                            }  
                    }
                ]

通过上面源码分析,和测试结果一致。for遍历用户json配置的 转换(因此可以从上往下顺序执行),然后根据name字段获取函数名List,然后根据这个list加载对应类。

五、GroovyTransformer转换 代码分析

Datax 自定义函数 dx_groovy
参考URL: https://www.jianshu.com/p/2b267ffda45b

datax json 配置如下,我们看到之前TransformerExecutionParas类中存储的 方法参数 的code、extraPackage这两个成员变量用在这里。

                "transformer": [
                    {
                        "name": "dx_groovy",
                          "parameter": 
                            {
                               "code": "//groovy code//",  
                               "extraPackage":[
                               "import somePackage1;", 
                               "import somePackage2;"
                               ]                      
                            }  
                    }
                ]

Groovy转换类:

public class GroovyTransformer extends Transformer {
    public GroovyTransformer() {
        setTransformerName("dx_groovy");
    }
   ...
 }
  • dx_groovy只能调用一次。不能多次调用。
  • groovy code中支持java.lang, java.util的包,可直接引用的对象有record,以及element下的各种 column(BoolColumn.class,BytesColumn.class,DateColumn.class,DoubleColumn.class,LongColumn.class,StringColumn.class)。.
  • 不支持其他包,如果用户有需要用到其他包,可设置extraPackage,注意extraPackage不支持第三方jar包。
  • groovy code中,返回更新过的Record(比如record.setColumn(columnIndex, new StringColumn(newValue));),或者null。返回null表示过滤此行。
  • 用户可以直接调用静态的Util方式(GroovyTransformerStaticUtil),目前GroovyTransformerStaticUtil的方法列表 (按需补充):

如下所示通过getGroovyRule,返回类的字符串,解析这个字符串到groovyClass,获取对应的实例,执行实例的evaluatef方法(用户输入的代码封装在该方法里面)

    private void initGroovyTransformer(String code, List<String> extraPackage) {
        GroovyClassLoader loader = new GroovyClassLoader(GroovyTransformer.class.getClassLoader());
        String groovyRule = getGroovyRule(code, extraPackage);

        Class groovyClass;
        try {
            groovyClass = loader.parseClass(groovyRule);
        } catch (CompilationFailedException cfe) {
            throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_GROOVY_INIT_EXCEPTION, cfe);
        }

        try {
            Object t = groovyClass.newInstance();
            if (!(t instanceof Transformer)) {
                throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_GROOVY_INIT_EXCEPTION, "datax bug! contact askdatax");
            }
            this.groovyTransformer = (Transformer) t;
        } catch (Throwable ex) {
            throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_GROOVY_INIT_EXCEPTION, ex);
        }
    }

getGroovyRule 方法:其实就是把用户定义的代码封装到RULE类下的 evaluate方法下:
在这里插入图片描述

六、自定义javascript函数处理转换

jdk1.6开始就提供了动态脚本语言诸如JavaScript动态的支持。
java 语言层面支持(java8 新JavaScript引擎nashorn)javascript
因此采用:使用Java自带的ScriptEngine可以说是最完美的Java动态执行代码方案。

  1. 检测用户输入的 javascript code

定义一个 绑定 实现 ScriptContext 接口
用来指定要传递给js的数据,以及其范围,比如 引擎范围 ENGINE_SCOPE或 全局 GLOBAL_SCOPE

比如:传递你要 操作的列

  1. 把java 中列信息,传递给js

  2. js处理列信息,处理列信息

  3. js 返回每个处理后的列信息,给java,datax修改record实例
    record.setColumn(columnIndex, new StringColumn(newValue));

七、问题思考总结

1. datax多个字段混合转换是否支持? (一次多个输入字段或多个输出字段)

我们分析datax 子串转换 SubstrTransformer ,如下我们看到它是,直接从paras取得第一个字符串,强转(Integer) ,因此它默认是不支持,这个SubstrTransformer 是不支持一次操作多个字段。

    @Override
    public Record evaluate(Record record, Object... paras) {

        int columnIndex;
        int startIndex;
        int length;

        try {
            if (paras.length != 3) {
                throw new RuntimeException("dx_substr paras must be 3");
            }

            columnIndex = (Integer) paras[0];
            startIndex = Integer.valueOf((String) paras[1]);
            length = Integer.valueOf((String) paras[2]);

        } catch (Exception e) {
            throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_ILLEGAL_PARAMETER, "paras:" + Arrays.asList(paras).toString() + " => " + e.getMessage());
        }

        Column column = record.getColumn(columnIndex);

思考:那么,如何一次处理多个字段,比如 我需要把 某两个字段 连接起来,作为目标库中的某一个字段的值?

2. datax json配置文件中的columnIndex 从0 开始还是从1 开始?

经过测试:从 0 开始。其中 columnIndex 对应的就是reader 配置下的column配置的顺序。
查看代码如下,列用ArrayList存储,所以从0开始。

public class DefaultRecord implements Record {

	private static final int RECORD_AVERGAE_COLUMN_NUMBER = 16;

	private List<Column> columns;

	private int byteSize;

	// 首先是Record本身需要的内存
	private int memorySize = ClassSize.DefaultRecordHead;

	public DefaultRecord() {
		this.columns = new ArrayList<Column>(RECORD_AVERGAE_COLUMN_NUMBER);
	}

	@Override
	public void addColumn(Column column) {
		columns.add(column);
		incrByteSize(column);
	}

3. datax 代码获取列名信息?

Record 代表一行记录,由列组成,列用成员变量ArrayList columns表示。

跟踪代码,查看Record 和Column 发现没有存储列名地方,也就是说record实例中没有了列名信息,定义到某个列完全使用 ArrayList数组索引。

Record 行

public class DefaultRecord implements Record {

	private static final int RECORD_AVERGAE_COLUMN_NUMBER = 16;

	private List<Column> columns;

	private int byteSize;

	// 首先是Record本身需要的内存
	private int memorySize = ClassSize.DefaultRecordHead;

Column 列

public abstract class Column {

	private Type type;

	private Object rawData;

	private int byteSize;

八、datax调试以及打印

1. datax debug远程调试

【可完全参考】参考URL:https://blog.csdn.net/gucapg/article/details/91045510

步骤主要有一下2步:

  1. 启动脚本加 -d
[root@xxx~]# python /usr/local/datax/bin/datax.py /usr/local/datax/test.json -d

DataX (DATAX-OPENSOURCE-3.0), From Alibaba !
Copyright (C) 2010-2017, Alibaba Group. All Rights Reserved.


local ip:  34.0.0.2
Listening for transport dt_socket at address: 9999

  1. IDEA 配置debug配置
    datax入口类:com.alibaba.datax.core.Engine
    然后Edit Configure–>选择 Remote->配置debug的ip、端口以及源码工程。

先执行第一步,再执行第二步即可。

九、参考

[推荐-写的比较全面]DataX的执行流程分析
参考URL: https://www.jianshu.com/p/b10fbdee7e56
一次详细的 datax 优化
参考URL: https://xiaozhuanlan.com/topic/7860594132
Datax开发使用须知
参考URL: https://blog.csdn.net/MrZhangBaby/article/details/89638119
【Java】使用ScriptEngine动态执行代码(附Java几种动态执行代码比较)
参考URL: https://blog.csdn.net/hangvane123/article/details/84945180

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西京刀客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值