DataX HdfsReader 源码分析,及空文件 Bug修复和路径正则功能增强


目录



1 概述

  • 当我们使用 DataX 的 hdfsreader 时直接配置读取的HDFS 目录后,如果此目录下存在空文件时会报异常,针对此问题,本文档将详细介绍此问题的处理,并同时给也给出基于源码的bug 修复方案。

  • 当我们配置的 path 使用 范围正则形式时报错,针对源码中未支持的此部分功能进行源码层面的完善


2 问题描述

例如当 HDFS 的需要读取数据的文件夹下有存在一个大小为0空文件时,并且此时在hdfsreader的path配置的为此目录(而非正则化路径)时会报如下的错误,具体报错信息后面会通过问题复现来观察,大概是在验证指定目录的文件类型时报了异常,文件 000000_01 验证为 ORC 类型符合预期要求添加到了 source file 列表中,当继续获取指定目录下的 000001_01 时类型验证失败,抛出了异常,但是通过查看 HDFS 上次文件,这个文件的大小为 0。
报错信息

从上图中我们可以很明显看出读取当前文件夹下大小为非 0 的文件是正常的,文件验证通过后会将这个文件添加到 source files 列表,这个列表就是后面job 需要处理的文件。如果此目录下有既非文件又非文件夹的则会在日志中输出一条INFO 级别的日志,如果文件的格式非用户指定的类型,则会在日志中输出一条WARN 级别的日志。因此我们可以断定错误就发生在对文件类型验证(更确切的说是验证此文件是否为 ORC 格式)时发生的异常。


3 问题复现

为了更准确的确定触发此错误的原因,下面我们在测试环境复现这个错误。

3.1 测试数据

假如有如下测试数据

1292052,1,希望让人自由。
1291546,2,风华绝代。
1295644,3,怪蜀黍和小萝莉不得不说的故事。
1292720,4,一部美国近现代史。
1292063,5,最美的谎言。
1291561,6,最好的宫崎骏,最好的久石让。
1292722,7,失去的才是永恒的。
1295124,8,拯救一个人,就是拯救整个世界。
3541415,9,诺兰给了我们一场无法盗取的梦。
3011091,10,永远都不能忘记你所爱的人。
2131459,11,小瓦力,大人生。
3793023,12,英俊版憨豆,高情商版谢耳朵。
1291549,13,天籁一般的童声,是最接近上帝的存在。
1292001,14,每个人都要走一条自己坚定了的路,就算是粉身碎骨。
1292064,15,如果再也不能见到你,祝你早安,午安,晚安。

在本地创建如下测试数据文件及文件夹,其中 d 开头的表示为文件夹,f 开头的表示文件(文件类型为TEXT 格式,使用 ORC 文件测试时结果相同,因为后面对源码分析时可以看到类型判断事,对于 CSV和 TEXT 格式的如果非ORC、RCFile、SEQUENCE则认为是次类型,这里为了展示和查看方便直接使用 TEXT 格式)。f1文件中保存的为 1292052 开头行的数据(1行)、f2 为空文件(0行)、f11 为空文件(0行)、f12 文件中保存的为 1291546-1295644 开头行的数据(2行)、f111 文件中保存的为 1292720-1291561 开头行的数据(3行)、f112 为空文件(0行)、f21 文件中保存的为 1292722-3011091 开头行的数据(4行)、f22 文件中保存的为 2131459-1292064 开头行的数据(5行)

[root@cdh1 datax_test]# tree --du
.
├── [        288]  d1
│   ├── [        150]  d11
│   │   ├── [        120]  f111
│   │   └── [          0]  f112
│   ├── [          6]  d12
│   ├── [          0]  f11
│   └── [         82]  f12
├── [        544]  d2
│   ├── [        201]  f21
│   └── [        315]  f22
├── [         32]  f1
└── [          0]  f2

         910 bytes used in 4 directories, 8 files

将测试数据上传到 HDFS 上,如下图所示
hdfs 数据目录

3.2 正则方式指定path

我们直接读取 HDFS 上的 /yore/d1/11 下的文件,配置如下 json,writer 这里使用 “streamwriter” 输出到日志,注意 reader.parameter 中 path 配置的为 /yore/d1/d11/* 方式。

{
  "job": {
    "content": [
      {
        "reader": {
          "name": "hdfsreader",
          "parameter": {
            "path": "/yore/d1/d11/*",
            "defaultFS": "hdfs://cdh1:8020",
            "column": [
              "*"
            ],
            "fileType": "TEXT",
            "encoding": "UTF-8",
            "fieldDelimiter": ","
          }
        },
        "writer": {
          "name": "streamwriter",
          "parameter": {
            "encoding": "UTF-8",
            "print": true
          }
        }
      }
    ],
    "setting": {
      "speed": {
        "channel": 1
      }
    }
  }
}

成功执行后的结果如下:
正常读取文件时

3.3 普通方式指定path

reader.parameter 中 path 直接指定要读取的数据目录

{
  "job": {
    "content": [
      {
        "reader": {
          "name": "hdfsreader",
          "parameter": {
            "path": "/yore/d1/d11",
            "defaultFS": "hdfs://cdh1:8020",
            "column": [
              "*"
            ],
            "fileType": "TEXT",
            "encoding": "UTF-8",
            "fieldDelimiter": ","
          }
        },
        "writer": {
          "name": "streamwriter",
          "parameter": {
            "encoding": "UTF-8",
            "print": true
          }
        }
      }
    ],
    "setting": {
      "speed": {
        "channel": 1
      }
    }
  }
}

这次发现出现了和前面开始时提到的基本一样的错误(使用 ORC 类型也是同样的错误,因此确定引起上面的问题就是 文件夹下有空文件的情况下没有以正则方式指定 path)

2020-05-22 15:42:20.887 [job-0] ERROR HdfsReader$Job - 检查文件[hdfs://cdh1:8020/yore/d1/d11/f112]类型失败,目前支持ORC,SEQUENCE,RCFile,TEXT,CSV五种格式的文件,请检查您文件类型和文件是否正确。
2020-05-22 15:42:20.893 [job-0] ERROR JobContainer - Exception when job run
com.alibaba.datax.common.exception.DataXException: Code:[HdfsReader-10], Description:[读取文件出错].  - 检查文件[hdfs://cdh1:8020/yore/d1/d11/f112]类型失败,目前支持ORC,SEQUENCE,RCFile,TEXT,CSV五种格式的文件,请检查您文件类型和文件是否正确。 - java.lang.IndexOutOfBoundsException
        at java.nio.Buffer.checkIndex(Buffer.java:540)
        at java.nio.HeapByteBuffer.get(HeapByteBuffer.java:139)
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.isORCFile(DFSUtil.java:585)
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.checkHdfsFileType(DFSUtil.java:535)
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.addSourceFileByType(DFSUtil.java:184)
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.getHDFSAllFilesNORegex(DFSUtil.java:171)
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.getHDFSAllFiles(DFSUtil.java:141)
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.getAllFiles(DFSUtil.java:112)
        at com.alibaba.datax.plugin.reader.hdfsreader.HdfsReader$Job.prepare(HdfsReader.java:169)
        at com.alibaba.datax.core.job.JobContainer.prepareJobReader(JobContainer.java:715)
        at com.alibaba.datax.core.job.JobContainer.prepare(JobContainer.java:308)
        at com.alibaba.datax.core.job.JobContainer.start(JobContainer.java:115)
        at com.alibaba.datax.core.Engine.start(Engine.java:92)
        at com.alibaba.datax.core.Engine.entry(Engine.java:171)
        at com.alibaba.datax.core.Engine.main(Engine.java:204)

        at com.alibaba.datax.common.exception.DataXException.asDataXException(DataXException.java:33) ~[datax-common-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.checkHdfsFileType(DFSUtil.java:565) ~[hdfsreader-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.addSourceFileByType(DFSUtil.java:184) ~[hdfsreader-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.getHDFSAllFilesNORegex(DFSUtil.java:171) ~[hdfsreader-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.getHDFSAllFiles(DFSUtil.java:141) ~[hdfsreader-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.getAllFiles(DFSUtil.java:112) ~[hdfsreader-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.plugin.reader.hdfsreader.HdfsReader$Job.prepare(HdfsReader.java:169) ~[hdfsreader-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.job.JobContainer.prepareJobReader(JobContainer.java:715) ~[datax-core-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.job.JobContainer.prepare(JobContainer.java:308) ~[datax-core-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.job.JobContainer.start(JobContainer.java:115) ~[datax-core-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.Engine.start(Engine.java:92) [datax-core-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.Engine.entry(Engine.java:171) [datax-core-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.Engine.main(Engine.java:204) [datax-core-0.0.1-SNAPSHOT.jar:na]
Caused by: java.lang.IndexOutOfBoundsException: null
        at java.nio.Buffer.checkIndex(Buffer.java:540) ~[na:1.8.0_222]
        at java.nio.HeapByteBuffer.get(HeapByteBuffer.java:139) ~[na:1.8.0_222]
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.isORCFile(DFSUtil.java:585) ~[hdfsreader-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.checkHdfsFileType(DFSUtil.java:535) ~[hdfsreader-0.0.1-SNAPSHOT.jar:na]
        ... 11 common frames omitted
2020-05-22 15:42:20.919 [job-0] INFO  StandAloneJobContainerCommunicator - Total 0 records, 0 bytes | Speed 0B/s, 0 records/s | Error 0 records, 0 bytes |  All Task WaitWriterTime 0.000s |  All Task WaitReaderTime 0.000s | Percentage 0.00%
2020-05-22 15:42:20.921 [job-0] ERROR Engine -

经DataX智能分析,该任务最可能的错误原因是:
com.alibaba.datax.common.exception.DataXException: Code:[HdfsReader-10], Description:[读取文件出错].  - 检查文件[hdfs://cdh1:8020/yore/d1/d11/f112]类型失败,目前支持ORC,SEQUENCE,RCFile,TEXT,CSV五种格式的文件,请检查您文件类型和文件是否正确。 - java.lang.IndexOutOfBoundsException

4 路径的正则问题

通过前面问题复现,我们大体上可以窥探到错误发生的原因,如果生产环境时间赶的紧,可以直接将离线数据同步的 json 文件中配置 hdfs 路径的 path 指定为正则方式(也就是有原先形如 /path/data_dir 改为 /path/data_dir/*)可以立即解决。

因为报错部分也是与这个问题有密切的部分,有必要在分析错误原因之再重点查看下 path 的正则。配置的path 会在实例化org.apache.hadoop.fs.Path 时传入,然后根据 path 进一步获取配置路径的信息。

4.1 正则符号

正则符号 解释
? 匹配任意单个字符;
* 匹配零个或多个字符(多个 等价于一个);
[abc] 匹配字符集 {a, b, c} 中的单个字符;
[a-b] 匹配字符范围 {a…b} 中的单个字符,字符a在字典上必须小于或等于字符b;
[^a] 匹配不是字符集或范围{a}中的单个字符,注意,^字符必须紧跟在右括号的右边;
\c 转义掉字符c的任何特殊含义;
{ab,cd} 匹配字符集 {ab,cd} 的字符串;
{ab,c{de,fh}} 匹配来自字符串集{ab,cde,cfh}的字符串;

4.2 示例

# 1 本地创建如下文件,并写入一些内容标识数据
[root@cdh1 datax_test]# tree regex/
regex/
├── data_t_01
├── data_t_02
├── data_t_03
├── data_t_0a
├── data_t_aa
├── data_tb_00
├── data_tb_01
├── data_tb2_00
├── data_tbl_00
└── f1

0 directories, 10 files


# 2 上传到 HDFS 
hadoop fs -put regex/ /yore

# 3 查看 HDFS 上的文件
[root@cdh1 datax_test]# hadoop fs -ls /yore/regex
Found 10 items
-rw-r--r--   1 root supergroup         28 2020-05-12 14:15 /yore/regex/data_t_01
-rw-r--r--   1 root supergroup         28 2020-05-12 14:15 /yore/regex/data_t_02
-rw-r--r--   1 root supergroup         29 2020-05-12 14:15 /yore/regex/data_t_03
-rw-r--r--   1 root supergroup         29 2020-05-12 14:15 /yore/regex/data_t_0a
-rw-r--r--   1 root supergroup         29 2020-05-12 14:15 /yore/regex/data_t_aa
-rw-r--r--   1 root supergroup         31 2020-05-12 14:15 /yore/regex/data_tb2_00
-rw-r--r--   1 root supergroup         30 2020-05-12 14:15 /yore/regex/data_tb_00
-rw-r--r--   1 root supergroup         30 2020-05-12 14:15 /yore/regex/data_tb_01
-rw-r--r--   1 root supergroup         31 2020-05-12 14:15 /yore/regex/data_tbl_00
-rw-r--r--   1 root supergroup         21 2020-05-12 14:15 /yore/regex/f1

  • 示例1:匹配任意单个字符。如果写 /yore/datax? 则无法匹配到任何目录或文件

    • path: /yore/regex/data_t_0?
    • File Status(匹配到的文件状态,已简写):
      • /yore/regex/data_t_01
      • /yore/regex/data_t_02
      • /yore/regex/data_t_03
      • /yore/regex/data_t_0a
  • 示例2:匹配零个或多个字符。如果写多个等价于一个

    • path: /yore/regex/data_t_*
    • File Status:
      • /yore/regex/data_t_01
      • /yore/regex/data_t_02
      • /yore/regex/data_t_03
      • /yore/regex/data_t_0a
      • /yore/regex/data_t_aa
  • 示例3:匹配字符集中的单个字符

    • path: /yore/regex/data_t_0[12]
    • File Status:
      • /yore/regex/data_t_01
      • /yore/regex/data_t_02
  • 示例4:匹配字符范围中的单个字符

    • path: /yore/regex/data_t_0[0-9]
    • File Status:
      • /yore/regex/data_t_01
      • /yore/regex/data_t_02
      • /yore/regex/data_t_03
  • 示例5:匹配不是字符集或范围中的单个字符

    • path: /yore/regex/data_t_[^a-z]?
    • File Status: /yore/datax_test_result
      • /yore/regex/data_t_01
      • /yore/regex/data_t_02
      • /yore/regex/data_t_03
      • /yore/regex/data_t_0a
  • 示例6:匹配字符集的字符串

    • path: /yore/regex/data_{t,tb}_0[0-9]
    • File Status:
      • /yore/regex/data_t_01
      • /yore/regex/data_t_02
      • /yore/regex/data_t_03
      • /yore/regex/data_tb_00
      • /yore/regex/data_tb_01
  • 示例7:匹配来自字符串集的字符串

    • path: /yore/regex/data_{t,t{b,bl}}_*
    • File Status:
      • /yore/regex/data_t_01
      • /yore/regex/data_t_02
      • /yore/regex/data_t_03
      • /yore/regex/data_t_0a
      • /yore/regex/data_t_aa
      • /yore/regex/data_tb_00
      • /yore/regex/data_tb_01
      • /yore/regex/data_tbl_00

4.3 DataX 路径的进一步正则测试

一次将上面测试的路径正则 path,配置到 json 中,进行测试

{
  "job": {
    "content": [
      {
        "reader": {
          "name": "hdfsreader",
          "parameter": {
            "path": "/yore/regex/data_{t,t{b,bl}}_*",
            "defaultFS": "hdfs://cdh1:8020",
            "column": [
              "*"
            ],
            "fileType": "TEXT",
            "encoding": "UTF-8",
            "fieldDelimiter": "║"
          }
        },
        "writer": {
          "name": "streamwriter",
          "parameter": {
            "encoding": "UTF-8",
            "print": true
          }
        }
      }
    ],
    "setting": {
      "speed": {
        "channel": 1
      }
    }
  }
}
4.3.1 /yore/regex/data_t_0? 测试

测试结果正常

4.3.2 /yore/regex/data_t_* 测试

测试结果正常

4.3.3 /yore/regex/data_t_0[12] 测试

Datax 执行报如下错误

2020-05-22 17:54:06.048 [job-0] ERROR JobContainer - Exception when job run
java.lang.ClassCastException: java.lang.String cannot be cast to java.util.List
        at com.alibaba.datax.common.util.Configuration.getList(Configuration.java:426) ~[datax-common-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.plugin.reader.hdfsreader.HdfsReader$Job.validate(HdfsReader.java:66) ~[hdfsreader-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.plugin.reader.hdfsreader.HdfsReader$Job.init(HdfsReader.java:50) ~[hdfsreader-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.job.JobContainer.initJobReader(JobContainer.java:673) ~[datax-core-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.job.JobContainer.init(JobContainer.java:303) ~[datax-core-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.job.JobContainer.start(JobContainer.java:113) ~[datax-core-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.Engine.start(Engine.java:92) [datax-core-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.Engine.entry(Engine.java:171) [datax-core-0.0.1-SNAPSHOT.jar:na]
        at com.alibaba.datax.core.Engine.main(Engine.java:204) [datax-core-0.0.1-SNAPSHOT.jar:na]
2020-05-22 17:54:06.055 [job-0] INFO  StandAloneJobContainerCommunicator - Total 0 records, 0 bytes | Speed 0B/s, 0 records/s | Error 0 records, 0 bytes |  All Task WaitWriterTime 0.000s |  All Task WaitReaderTime 0.000s | Percentage 0.00%
2020-05-22 17:54:06.057 [job-0] ERROR Engine -

经DataX智能分析,该任务最可能的错误原因是:
com.alibaba.datax.common.exception.DataXException: Code:[Framework-02], Description:[DataX引擎运行过程出错,具体原因请参看DataX运行结束时的错误诊断信息  .].  - java.lang.ClassCastException: java.lang.String cannot be cast to java.util.List

本以为是 hdfsreader 模块中校验 path 时的错误,但是从从日志中我们可以看到其实是 datax-commonConfiguration.java 工具类的第 426 行报了错误,经过对代码分析,当设置的 json 中的值字符串内容也包含[]时,调用 Object object = this.get(path, List.class); 返回的内容为String,而不是 List 对象,String 内容强转 List 时发生了类型转换的异常,因此我们对代码进行如下修复。修改完毕之后重新打包 datax-common 模块,然后 datax/lib 下的 datax-common-0.0.1-SNAPSHOT.jar 替换为新打好的 jar 。
修复 Object 转 List 时的异常

/**
	 * 根据用户提供的json path,寻址List对象,如果对象不存在,返回null
	 */
	@SuppressWarnings("unchecked")
	public <T> List<T> getList(final String path, Class<T> t) {
		Object object = this.get(path, List.class);
		if (null == object) {
			return null;
		}

		List<T> result = new ArrayList<T>();

		List<Object> origin = new ArrayList<>();
		try {
			origin = (List<Object>) object;
		}catch(ClassCastException e){
			log.warn("{} 转为 List 时发生了异常,默认将此值添加到 List 中", String.valueOf(object));
			origin.add(String.valueOf(object));
		}
		for (final Object each : origin) {
			result.add((T) each);
		}

		return result;
	}
4.3.4 /yore/regex/data_t_0[0-9] 测试

执行结果报错,报错信息如下,下面在 6 修复源码的Bug节 会给出修复。

2020-05-22 20:10:54.078 [job-0] ERROR HdfsReader$Job - 无法读取路径[/yore/regex/data_t_0[0-9]]下的所有文件,请确认您的配置项fs.defaultFS, path的值是否正确,是否有读写权限,网络是否已断开!
2020-05-22 20:10:54.096 [job-0] ERROR JobContainer - Exception when job run
com.alibaba.datax.common.exception.DataXException: Code:[HdfsReader-09], Description:[您配置的path格式有误].  - java.io.FileNotFoundException: File /yore/regex/data_t_0[0-9] does not exist.
        at org.apache.hadoop.hdfs.DistributedFileSystem.listStatusInternal(DistributedFileSystem.java:795)
        at org.apache.hadoop.hdfs.DistributedFileSystem.access$700(DistributedFileSystem.java:106)
        at org.apache.hadoop.hdfs.DistributedFileSystem$18.doCall(DistributedFileSystem.java:853)
        at org.apache.hadoop.hdfs.DistributedFileSystem$18.doCall(DistributedFileSystem.java:849)
        at org.apache.hadoop.fs.FileSystemLinkResolver.resolve(FileSystemLinkResolver.java:81)
        at org.apache.hadoop.hdfs.DistributedFileSystem.listStatus(DistributedFileSystem.java:860)
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.getHDFSAllFilesNORegex(DFSUtil.java:162)
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.getHDFSAllFiles(DFSUtil.java:141)
        at com.alibaba.datax.plugin.reader.hdfsreader.DFSUtil.getAllFiles(DFSUtil.java:112)
        at com.alibaba.datax.plugin.reader.hdfsreader.HdfsReader$Job.prepare(HdfsReader.java:169)
        at com.alibaba.datax.core.job.JobContainer.prepareJobReader(JobContainer.java:715)
        at com.alibaba.datax.core.job.JobContainer.prepare(JobContainer.java:308)
        at com.alibaba.datax.core.job.JobContainer.start(JobContainer.java:115)
        at com.alibaba.datax.core.Engine.start(Engine.java:92)
        at com.alibaba.datax.core.Engine.entry(Engine.java:171)
        at com.alibaba.datax.core.Engine.main(Engine.java:204)
4.3.5 /yore/regex/data_t_[^a-z]? 测试

因为正则路径中包含 ?,执行结果成功。

4.3.6 /yore/regex/data_{t,tb}_0[0-9] 测试

执行结果报错,报错信息同 4.3.4 ,下面在 6 修复源码的Bug节 会给出修复。

4.3.7 /yore/regex/data_{t,t{b,bl}}_* 测试

因为正则路径中包含 ?,执行结果成功。


5 DataX 源码

针对上面出现的两大问题,我们避免不了修复源码,一次首先需要将源码获取到本地,然后使用开发工具对源码进行修复。本部主要对源码中的 hdfsreader 模块做修改,为了修复第二个问题当然也会对 datax-common 模块做修改。

5.1 下载源码及Git设置

建议提前安装 git,开发工具使用 IntelliJ IDEA。简单的方式是直接在DataX 的源码处下载 zip包解压后用 IDEA 工具打开进行修改,这里我进一步讲解一下标准的代码修复方式,原因就是我们一般对官方源码库没有写权限,我们修改完毕之后如果想保留源码修改记录,或者想回馈到社区,则必须按照这种方式进行。

# clone 源码到本地(不建议,建议先 Fork 源码到自己 git 仓库)
# git clone https://github.com/alibaba/DataX.git

# 1 Fork github 上的项目到自己的 Repositories

# 2 生成本地系统的 SSH key
# 如果是 Windows 安装完 Git 后,可以在资源文件夹下右键 Git Bash Here ,然后再执行如下命令
cd ~/.ssh
ll -a 
ssh-keygen -t rsa
# 可以看到生成了一个密钥和一个公钥: id_rsa、 id_rsa.pub,然后将公钥的内容复制
ls -a

在这里插入图片描述
登陆 GitHub 依次点击: 账户头像 -> Settings -> SSH and GPG keys -> New SSH key,将复制的公钥添加到 Key 输入框中,Title 可以随意填写(例如填写上账户名标识),添加成功后如下图所示。如果后期不再使用,可以在 SSH and GPG keys 页面中 Delete 掉对应的 SSH key 即可。
在这里插入图片描述

# 1  cd 到项目文件夹下,clone 代码到本地
git clone git@github.com:yoreyuan/DataX.git

# 2 查看提交的历史信息(查看最近10条记录)
git log -n 10

# 3 添加 datax 的远程地址
git remote add upstream https://github.com/alibaba/DataX.git

# 4 查看添加的 remote 信息
git remote -v

# 5 获取最新源码到本地
git pull upstream master

# 6 更新自己仓库的代码到最新
git push origin master

# 7 查看当前的分支
git branch 

# 8 创建一个新的分支(分支名可随意起),并切换到此分支下
git checkout -b yore_v0.0.1-SNAPSHOT

在这里插入图片描述

5.2 IDEA

下面我就可以直接使用 IDEA 打开我们上一步下载的项目。但是打开后代码还会存在一些问题,需要我们进一步把错误排除,准备好编译环境。
在这里插入图片描述

5.3 父模块 pom 报错

在这里插入图片描述
我们只需要将此插件的详细信息不全,其它模块有次问题可依次按此种方式修复错误。这里统一修改为 2.6 版本

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>2.6</version>
    <configuration>
        <finalName>datax</finalName>
        <descriptors>
            <descriptor>package.xml</descriptor>
        </descriptors>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id>
            <phase>package</phase>
        </execution>
    </executions>
</plugin>

引入 2.6 版本 版本后,其他模块在打包时会报如下的错误,后面会在打包部分给出解决方法。

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-assembly-plugin:2.6:single (dwzip) on project hdfsreader: Assembly is incorrectly configured: Assembly is incorrectly configured:
[ERROR] Assembly:  is not configured correctly: Assembly ID must be present and non-empty.
[ERROR] -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

5.4 clickhousewriter 报错

我们定位到这个模块的 pom.xml 文件,可以发现其引入的依赖范围为 test 的一个依赖我们本地无法下载,且项目中也没有这个模块,又因为此模块的 test 部分的代码也是不存在的,因为我们可以直接将其注释掉:
在这里插入图片描述
同时因为 clickhousewriter 是最新提交的,代码可能存在一些小问题,遇到下面的错误时直接注释掉导入的那个包即可,因为这个包在此类中未被引用。
在这里插入图片描述

5.5 关于DataX 逻辑执行模型

这里重点理解一下几个DataX 中的概念,简单理解就是一次提交执行就是一个 Job,Task则是Job的拆分,并分别在框架提供的容器中执行。

  • Job: Job是DataX用以描述从一个源头到一个目的端的同步作业,是DataX数据同步的最小业务单元。比如:从一张mysql的表同步到odps的一个表的特定分区。
  • Task: Task是为最大化而把Job拆分得到的最小执行单元。比如:读一张有1024个分表的mysql分库分表的Job,拆分成1024个读Task,用若干个并发执行。
  • TaskGroup: 描述的是一组Task集合。在同一个TaskGroupContainer执行下的Task集合称之为TaskGroup
  • JobContainer: Job执行器,负责Job全局拆分、调度、前置语句和后置语句等工作的工作单元。类似Yarn中的JobTracker
  • TaskGroupContainer: TaskGroup执行器,负责执行一组Task的工作单元,类似Yarn中的TaskTracker。

一个插件化的 Reader 实现必须继承Reader 抽象类,并实现其中的 Job内部抽象类和Task内部抽象类。
在这里插入图片描述

5.6 HdfsReader 之 Job

HdfsReader 代码实现的方法结构如下图所示。

在这里插入图片描述
我们先来查看 Job 的实现,首先框架会调用 init 方法,这个方法中主要实现了初始化的一些内容,HdfsReader 中这个方法第一步校验了配置的 reader 中的json 参数,比如格式结构是否正确,必填项是否已填写,json 文件的编码格式是否可以解析,检查如果开启了Kerberos 后相关的Kerberos 配置项是否正确,验证json 中配置列信息是否符合规范,compress校验等,最后实例化了DFSUtil 对象,以便下下一步处理的时候可以直接调用具体的 HDFS 相关的方法。

接下来Job会继续执行 prepare方法,在这个方法中主要是通过配置信息进一步获取符合框架校验和且符合json 配置的待读取的HDFS 数据文件的 Set 集合。这一步主要调用的是 dfsUtil.getAllFiles() 方法。

在这里插入图片描述
在这里插入图片描述

这里先跳过 dfsUtil.getAllFiles() 的具体逻辑,后面我们会进一步详细查看此方法。 prepare方法完毕之后就是 split ,该方法主要作用是切分任务,其方法会出入框架的建议切分数,但是插件开发人员可以根据实际情况来指定,例如 HdfsReader 中就并未使用框架建议的任务切分数,而是使用的满足条件的原文件的数量为切分数,但是这个值最好不要小于框架给出的 adviceNumber 值。

split 的具体实现是,先获取校验后的原文件的数量,先以这个为split 的 Num,并判断这个splitNum的值,如果为0这抛出一个异常,意味着读取的原文件为没有找到,可能原因是配置文件path的问题,提示检查。如果不为0则进一步判断,将原先的原文件的Set集合转为 List集合,并传入 adviceNumber 为源文件的数量值作为建议切分值。然后根据List长度的大小和传入的建议切分支的数量确定一个 average 的长度值,在HdfsReader 中此值为1,也就是切分后还是一个文件会放到一个sourceFiles 中,通过切分处理 readerSplitConfigs 会有设置好的文件分组的 Configuration 列表集合,最后返回框架。

@Override
        public List<Configuration> split(int adviceNumber) {

            LOG.info("split() begin...");
            List<Configuration> readerSplitConfigs = new ArrayList<Configuration>();
            // warn:每个slice拖且仅拖一个文件,
            // int splitNumber = adviceNumber;
            int splitNumber = this.sourceFiles.size();
            if (0 == splitNumber) {
                throw DataXException.asDataXException(HdfsReaderErrorCode.EMPTY_DIR_EXCEPTION,
                        String.format("未能找到待读取的文件,请确认您的配置项path: %s", this.readerOriginConfig.getString(Key.PATH)));
            }

            List<List<String>> splitedSourceFiles = this.splitSourceFiles(new ArrayList<String>(this.sourceFiles), splitNumber);
            for (List<String> files : splitedSourceFiles) {
                Configuration splitedConfig = this.readerOriginConfig.clone();
                splitedConfig.set(Constant.SOURCE_FILES, files);
                readerSplitConfigs.add(splitedConfig);
            }

            return readerSplitConfigs;
        }


        private <T> List<List<T>> splitSourceFiles(final List<T> sourceList, int adviceNumber) {
            List<List<T>> splitedList = new ArrayList<List<T>>();
            int averageLength = sourceList.size() / adviceNumber;
            averageLength = averageLength == 0 ? 1 : averageLength;

            for (int begin = 0, end = 0; begin < sourceList.size(); begin = end) {
                end = begin + averageLength;
                if (end > sourceList.size()) {
                    end = sourceList.size();
                }
                splitedList.add(sourceList.subList(begin, end));
            }
            return splitedList;
        }

post 方法和 destory 方法在 HdfsReader 中未实现具体逻辑。我们继续往下看 Task 的执行

        @Override
        public void post() {

        }

        @Override
        public void destroy() {

        }

5.7 HdfsReader 之 Task

Task负责对拆分后的任务的具体执行。Task 同样首先会调用 init 方法执行,如下图,主要是获取Job 配置信息对象,初始化切分后的原文件列表,Reader 中配置的编码格式,实例化 HDFS 工具类对象,等。

    public static class Task extends Reader.Task {

        private static Logger LOG = LoggerFactory.getLogger(Reader.Task.class);
        private Configuration taskConfig;
        private List<String> sourceFiles;
        private String specifiedFileType;
        private String encoding;
        private DFSUtil dfsUtil = null;
        private int bufferSize;

        @Override
        public void init() {

            this.taskConfig = super.getPluginJobConf();
            this.sourceFiles = this.taskConfig.getList(Constant.SOURCE_FILES, String.class);
            this.specifiedFileType = this.taskConfig.getNecessaryValue(Key.FILETYPE, HdfsReaderErrorCode.REQUIRED_VALUE);
            this.encoding = this.taskConfig.getString(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.ENCODING, "UTF-8");
            this.dfsUtil = new DFSUtil(this.taskConfig);
            this.bufferSize = this.taskConfig.getInt(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.BUFFER_SIZE,
                    com.alibaba.datax.plugin.unstructuredstorage.reader.Constant.DEFAULT_BUFFER_SIZE);
        }

// ...
}

在Task 实现类中 prepare方法 、 post方法、destroy方法默认为空,不需要实现具体逻辑,这里也直接忽略,下面重点查看startRead 方法,这也是Task 比较核心的逻辑部分。再这个方法中,循环获取自己需要处理的文件,然后根据文件不同的类型(CSV、TEXT、ORC、SEQ、RC)开启不同的文件读取流,对文件进行处理。

        @Override
        public void startRead(RecordSender recordSender) {

            LOG.info("read start");
            for (String sourceFile : this.sourceFiles) {
                LOG.info(String.format("reading file : [%s]", sourceFile));

                if(specifiedFileType.equalsIgnoreCase(Constant.TEXT)
                        || specifiedFileType.equalsIgnoreCase(Constant.CSV)) {

                    InputStream inputStream = dfsUtil.getInputStream(sourceFile);
                    UnstructuredStorageReaderUtil.readFromStream(inputStream, sourceFile, this.taskConfig,
                            recordSender, this.getTaskPluginCollector());
                }else if(specifiedFileType.equalsIgnoreCase(Constant.ORC)){

                    dfsUtil.orcFileStartRead(sourceFile, this.taskConfig, recordSender, this.getTaskPluginCollector());
                }else if(specifiedFileType.equalsIgnoreCase(Constant.SEQ)){

                    dfsUtil.sequenceFileStartRead(sourceFile, this.taskConfig, recordSender, this.getTaskPluginCollector());
                }else if(specifiedFileType.equalsIgnoreCase(Constant.RC)){

                    dfsUtil.rcFileStartRead(sourceFile, this.taskConfig, recordSender, this.getTaskPluginCollector());
                }else {

                    String message = "HdfsReader插件目前支持ORC, TEXT, CSV, SEQUENCE, RC五种格式的文件," +
                            "请将fileType选项的值配置为ORC, TEXT, CSV, SEQUENCE 或者 RC";
                    throw DataXException.asDataXException(HdfsReaderErrorCode.FILE_TYPE_UNSUPPORT, message);
                }

                if(recordSender != null){
                    recordSender.flush();
                }
            }

            LOG.info("end read source files...");
        }

5.8 HdfsReader 获取所有满足条件的原文件类表

5.7 节 部分我们在Job 的 prepare 方法提到过 dfsUtil.getAllFiles(path, specifiedFileType)方法,现在我们进一步查看其主要的逻辑实现。点进源码可以看到如下Code,将类实例的 specifiedFileType 设置为当前用户指定的格式类型(目前CSV、TEXT、ORC、SEQ、RC五种类型),当用户配置的 path 不为空时,循环其用户指定的每一个路径调用 getHDFSAllFiles(eachPath) 方法,最后返回满足条件的指定path 下的所有源文件Set集合 sourceHDFSAllFilesList

/**
 * 获取指定路径列表下符合条件的所有文件的绝对路径
 *
 * @param srcPaths          路径列表
 * @param specifiedFileType 指定文件类型
 */
public HashSet<String> getAllFiles(List<String> srcPaths, String specifiedFileType) {

    this.specifiedFileType = specifiedFileType;

    if (!srcPaths.isEmpty()) {
        for (String eachPath : srcPaths) {
            LOG.info(String.format("get HDFS all files in path = [%s]", eachPath));
            getHDFSAllFiles(eachPath);
        }
    }
    return sourceHDFSAllFilesList;
}

getHDFSAllFiles 的具体实现如下,从这个方法我们也可以看到 DataX 加载文件时的判断大体逻辑,主要分为两类,第一类就是包含正则符号的和其它方式的,包含正则类型的,或通过 hdfs 对象进一步判断指定的路径下的文件类型(文件还是文件夹),如果是文件夹则调用getHDFSAllFilesNORegex 方法,同时非正则方式的也会调用这个方法;如果是文件类型,判断文件的大小,如果为 0 输出一条 WARN 级别的日志信息提示用户某个文件长度为0将会跳过不作处理,否则调用 addSourceFileByType 进行下一步的处理。

public HashSet<String> getHDFSAllFiles(String hdfsPath) {
    try {
        FileSystem hdfs = FileSystem.get(hadoopConf);
        //判断hdfsPath是否包含正则符号
        if (hdfsPath.contains("*") || hdfsPath.contains("?")) {
            Path path = new Path(hdfsPath);
            FileStatus stats[] = hdfs.globStatus(path);
            for (FileStatus f : stats) {
                if (f.isFile()) {
                    if (f.getLen() == 0) {
                        String message = String.format("文件[%s]长度为0,将会跳过不作处理!", hdfsPath);
                        LOG.warn(message);
                    } else {
                        addSourceFileByType(f.getPath().toString());
                    }
                } else if (f.isDirectory()) {
                    getHDFSAllFilesNORegex(f.getPath().toString(), hdfs);
                }
            }
        } else {
            getHDFSAllFilesNORegex(hdfsPath, hdfs);
        }
        return sourceHDFSAllFilesList;
    } catch (IOException e) {
        String message = String.format("无法读取路径[%s]下的所有文件,请确认您的配置项fs.defaultFS, path的值是否正确," +
                "是否有读写权限,网络是否已断开!", hdfsPath);
        LOG.error(message);
        throw DataXException.asDataXException(HdfsReaderErrorCode.PATH_CONFIG_ERROR, e);
    }

下一步我们先来看 getHDFSAllFilesNORegex 的具体逻辑,addSourceFileByType 稍后再继续查看。getHDFSAllFilesNORegex 代码如下,在这个方法中主要是迭代给定的 path 下的所有文件,如果是文件则调用 addSourceFileByType 进行处理,如果不是就再递归调用自己。

private HashSet<String> getHDFSAllFilesNORegex(String path, FileSystem hdfs) throws IOException {
    // 获取要读取的文件的根目录
    Path listFiles = new Path(path);
    // If the network disconnected, this method will retry 45 times
    // each time the retry interval for 20 seconds
    // 获取要读取的文件的根目录的所有二级子文件目录
    FileStatus stats[] = hdfs.listStatus(listFiles);
    for (FileStatus f : stats) {
        // 判断是不是目录,如果是目录,递归调用
        if (f.isDirectory()) {
            LOG.info(String.format("[%s] 是目录, 递归获取该目录下的文件", f.getPath().toString()));
            getHDFSAllFilesNORegex(f.getPath().toString(), hdfs);
        } else if (f.isFile()) {
            addSourceFileByType(f.getPath().toString());
        } else {
            String message = String.format("该路径[%s]文件类型既不是目录也不是文件,插件自动忽略。",
                    f.getPath().toString());
            LOG.info(message);
        }
    }
    return sourceHDFSAllFilesList;
}

从上面文件递归检索给定文件夹下的所有文件的代码中我们又能看到 addSourceFileByType 方法,下面在详细查看此方法的具体逻辑,这个方法主要的逻辑就是判断传入进来的文件是不是用户指定格式的文件,如果是就添加到原文件的 Set 集合中,如果不是就输出ERROR 级别的信息,并抛出一个异常给框架。

// 根据用户指定的文件类型,将指定的文件类型的路径加入sourceHDFSAllFilesList
private void addSourceFileByType(String filePath) {
    // 检查file的类型和用户配置的fileType类型是否一致
    boolean isMatchedFileType = checkHdfsFileType(filePath, this.specifiedFileType);
    if (isMatchedFileType) {
        LOG.info(String.format("[%s]是[%s]类型的文件, 将该文件加入source files列表", filePath, this.specifiedFileType));
        sourceHDFSAllFilesList.add(filePath);
    } else {
        String message = String.format("文件[%s]的类型与用户配置的fileType类型不一致," +
                        "请确认您配置的目录下面所有文件的类型均为[%s]"
                , filePath, this.specifiedFileType);
        LOG.error(message);
        throw DataXException.asDataXException(
                HdfsReaderErrorCode.FILE_TYPE_UNSUPPORT, message);
    }
}

而判断文件是不是指定类型的主要逻辑又是通过 checkHdfsFileType 来实现的,其代码如下所示,根据用户指定的文件类型,调用不同的文件格式判断方法,如果传入的文件是用户指定的返回true,否则返回 false,如果是(目前)CSV、TEXT、ORC、SEQ、RC五种类型之外的则会输出ERROR 级别的日志,并抛出一个异常给框架。

public boolean checkHdfsFileType(String filepath, String specifiedFileType) {
    Path file = new Path(filepath);
    try {
        FileSystem fs = FileSystem.get(hadoopConf);
        FSDataInputStream in = fs.open(file);
        if (StringUtils.equalsIgnoreCase(specifiedFileType, Constant.CSV)
                || StringUtils.equalsIgnoreCase(specifiedFileType, Constant.TEXT)) {
            boolean isORC = isORCFile(file, fs, in);// 判断是否是 ORC File
            if (isORC) {
                return false;
            }
            boolean isRC = isRCFile(filepath, in);// 判断是否是 RC File
            if (isRC) {
                return false;
            }
            boolean isSEQ = isSequenceFile(filepath, in);// 判断是否是 Sequence File
            if (isSEQ) {
                return false;
            }
            // 如果不是ORC,RC和SEQ,则默认为是TEXT或CSV类型
            return !isORC && !isRC && !isSEQ;
        } else if (StringUtils.equalsIgnoreCase(specifiedFileType, Constant.ORC)) {
            return isORCFile(file, fs, in);
        } else if (StringUtils.equalsIgnoreCase(specifiedFileType, Constant.RC)) {
            return isRCFile(filepath, in);
        } else if (StringUtils.equalsIgnoreCase(specifiedFileType, Constant.SEQ)) {
            return isSequenceFile(filepath, in);
        }
    } catch (Exception e) {
        String message = String.format("检查文件[%s]类型失败,目前支持ORC,SEQUENCE,RCFile,TEXT,CSV五种格式的文件," +
                "请检查您文件类型和文件是否正确。", filepath);
        LOG.error(message);
        throw DataXException.asDataXException(HdfsReaderErrorCode.READ_FILE_ERROR, message, e);
    }
    return false;
}

这里以 ORC文件类型的判断为例,则会执行 isORCFile 方法,这个方法会首先获取给定文件的长度值,然后以获取的文件大小值和假设的一个默认值(16 * 1024)取最小为读取缓冲区分配的大小,但在此之前会先 seek 给定的文件的偏移量。

// 判断file是否是ORC File
private boolean isORCFile(Path file, FileSystem fs, FSDataInputStream in) {
    try {
        // figure out the size of the file using the option or filesystem
        long size = fs.getFileStatus(file).getLen();

        //read last bytes into buffer to get PostScript
        int readSize = (int) Math.min(size, DIRECTORY_SIZE_GUESS);
        in.seek(size - readSize);
        ByteBuffer buffer = ByteBuffer.allocate(readSize);
        in.readFully(buffer.array(), buffer.arrayOffset() + buffer.position(),
                buffer.remaining());

        //read the PostScript
        //get length of PostScript
        int psLen = buffer.get(readSize - 1) & 0xff;
        int len = OrcFile.MAGIC.length();
        if (psLen < len + 1) {
            return false;
        }
        int offset = buffer.arrayOffset() + buffer.position() + buffer.limit() - 1
                - len;
        byte[] array = buffer.array();
        // now look for the magic string at the end of the postscript.
        if (Text.decode(array, offset, len).equals(OrcFile.MAGIC)) {
            return true;
        } else {
            // If it isn't there, this may be the 0.11.0 version of ORC.
            // Read the first 3 bytes of the file to check for the header
            in.seek(0);
            byte[] header = new byte[len];
            in.readFully(header, 0, len);
            // if it isn't there, this isn't an ORC file
            if (Text.decode(header, 0, len).equals(OrcFile.MAGIC)) {
                return true;
            }
        }
    } catch (IOException e) {
        LOG.info(String.format("检查文件类型: [%s] 不是ORC File.", file.toString()));
    }
    return false;
}

6 升级项目的 maven-assembly-plugin

项目父模块的 pom.xml 引入的插件如下,会默认引入 2.2-beta-5 ,并且 <finalName> 标签无法识别。
在这里插入图片描述

因此在改动源码修复之前,我们先将 maven-assembly-plugin 升级到了 2.6 版本

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.6</version>
                <configuration>
                    <finalName>datax</finalName>
                    <descriptors>
                        <descriptor>package.xml</descriptor>
                    </descriptors>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                    </execution>
                </executions>
            </plugin>

【注意】在2.2 版本的时候添加了对 id 标签的校验(连接),因此我们需要在src/main/assembly/package.xml 中的id 标签中填写上内容(如果为空编译时会报错),在项目根目录下package.xml 中可以看到 它会将各个模块编译后的 target/datax 下的输出到 datax 文件夹并打包,所以这里统一改为 <id>plugin</id>,这样每个插件编译后的文件就可以统一放到了 target/datax-plugin 下 。

<assembly
        xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
    <id>plugin</id>
    <formats>
        <format>dir</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <fileSets>
        <fileSet>
            <directory>src/main/resources</directory>
            <includes>
                <include>plugin.json</include>
                <include>plugin_job_template.json</include>
            </includes>
            <outputDirectory>plugin/reader/hdfsreader</outputDirectory>
        </fileSet>
        <fileSet>
            <directory>target/</directory>
            <includes>
                <include>hdfsreader-${project.version}.jar</include>
            </includes>
            <outputDirectory>plugin/reader/hdfsreader</outputDirectory>
        </fileSet>
        <!--<fileSet>-->
            <!--<directory>src/main/cpp</directory>-->
            <!--<includes>-->
                <!--<include>libhadoop.a</include>-->
                <!--<include>libhadoop.so</include>-->
                <!--<include>libhadoop.so.1.0.0</include>-->
                <!--<include>libhadooppipes.a</include>-->
                <!--<include>libhadooputils.a</include>-->
                <!--<include>libhdfs.a</include>-->
                <!--<include>libhdfs.so</include>-->
                <!--<include>libhdfs.so.0.0.0</include>-->
            <!--</includes>-->
            <!--<outputDirectory>plugin/reader/hdfsreader/libs</outputDirectory>-->
        <!--</fileSet>-->
    </fileSets>

    <dependencySets>
        <dependencySet>
            <useProjectArtifact>false</useProjectArtifact>
            <outputDirectory>plugin/reader/hdfsreader/libs</outputDirectory>
            <scope>runtime</scope>
        </dependencySet>
    </dependencySets>
</assembly>

其它各个模块可以类似改动,最后将项目跟目录下的 package.xml 中进行如下修改

<assembly
        xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
    <id>all</id>
    <formats>
        <format>tar.gz</format>
        <format>dir</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <fileSets>
        <fileSet>
            <directory>transformer/target/datax/</directory>
            <includes>
                <include>**/*.*</include>
            </includes>
            <outputDirectory>datax</outputDirectory>
        </fileSet>
        <fileSet>
            <directory>core/target/datax-core/</directory>
            <includes>
                <include>**/*.*</include>
            </includes>
            <outputDirectory>datax</outputDirectory>
        </fileSet>

        <!-- reader. 按字典顺序添加 -->
        <fileSet>
            <directory>hdfsreader/target/datax-plugin</directory>
            <includes>
                <include>**/*.*</include>
            </includes>
            <outputDirectory>datax</outputDirectory>
        </fileSet>
		…………

        <!-- writer. 按字典顺序添加-->
        <fileSet>
            <directory>hdfswriter/target/datax-plugin/</directory>
            <includes>
                <include>**/*.*</include>
            </includes>
            <outputDirectory>datax</outputDirectory>
        </fileSet>
		………
    </fileSets>
</assembly>

7 修复源码的Bug并增强正则功能

通过前面源码分析 addSourceFileByType 方法在为文件类型时都会调用,因此我们直接再这个方法中修复代码,添加上对文件长度的校验

// 根据用户指定的文件类型,将指定的文件类型的路径加入sourceHDFSAllFilesList
private void addSourceFileByType(FileStatus fileStatus) {
    String filePath = fileStatus.getPath().toString();
    // 当为文件时会调用,判断文件的长度是否为 0
    if(fileStatus.getLen()==0){
        LOG.warn("文件[{}]长度为0,将会跳过不作处理!", filePath);
    }else{
        // 检查file的类型和用户配置的fileType类型是否一致
        boolean isMatchedFileType = checkHdfsFileType(filePath, this.specifiedFileType);
        if (isMatchedFileType) {
            LOG.info(String.format("[%s]是[%s]类型的文件, 将该文件加入source files列表", filePath, this.specifiedFileType));
            sourceHDFSAllFilesList.add(filePath);
        } else {
            String message = String.format("文件[%s]的类型与用户配置的fileType类型不一致," +
                            "请确认您配置的目录下面所有文件的类型均为[%s]"
                    , filePath, this.specifiedFileType);
            LOG.error(message);
            throw DataXException.asDataXException(
                    HdfsReaderErrorCode.FILE_TYPE_UNSUPPORT, message);
        }
    }
}

在这里插入图片描述
同时源码中对path正则表的判断只简单判断了是否包含*或者?,这里对其修改为完善版的正则判断,让其支持*、?、[abc]、[a-b]、[^a]、{ab,cd}、{ab,c{de,fh}} 形式的正则语句,同时代码修改为如下。

public HashSet<String> getHDFSAllFiles(String hdfsPath) {
        try {
            FileSystem hdfs = FileSystem.get(hadoopConf);
            //判断hdfsPath是否包含正则符号:*、?、[abc]、[a-b]、[^a]、{ab,cd}、{ab,c{de,fh}}
            if (Pattern.compile("\\*|\\?|\\[\\^?\\w+\\]|\\[\\^?\\w-\\w\\]|\\{[\\w\\{\\}\\,]+\\}")
		.matcher(hdfsPath).find()) {
                Path path = new Path(hdfsPath);
                FileStatus stats[] = hdfs.globStatus(path);
                for (FileStatus f : stats) {
                    if (f.isFile()) {
                        addSourceFileByType(f);
                    } else if (f.isDirectory()) {
                        getHDFSAllFilesNORegex(f.getPath().toString(), hdfs);
                    }
                }
            } else {
                getHDFSAllFilesNORegex(hdfsPath, hdfs);
            }
            return sourceHDFSAllFilesList;
        } catch (IOException e) {
            String message = String.format("无法读取路径[%s]下的所有文件,请确认您的配置项fs.defaultFS, path的值是否正确," +
                    "是否有读写权限,网络是否已断开!", hdfsPath);
            LOG.error(message);
            throw DataXException.asDataXException(HdfsReaderErrorCode.PATH_CONFIG_ERROR, e);
        }
    }

其它地方调用 addSourceFileByType 方法时只需要改为传递 FileStatus 参数即可,也就是不用再 gitPath().toString()

在这里插入图片描述

最后使用 IDEA 的maven 插件打包 hdfsreader 模块即可,但是打包之前需要先一次 install一下几个模块:datax-commondatax-transformerdatax-coreplugin-unstructured-storage-util。最后将打包的新的 hdfsreader-0.0.1-SNAPSHOT.jar 上传替换原来的 plugin/reader/hdfsreader 下的这个包(替换前最好将这个jar 包备份),然后再次执行(path不管是普通形式还是正则形式都可以)发现可以正常执行。


8 修复Bug 之后的DataX 测试

测试项同 4.3 相同,测试结果如下,我们需要的功能已经成功实现。

path 测试结果
/yore/regex/data_t_0? 测试结果正常
/yore/regex/data_t_* 测试结果正常
/yore/regex/data_t_0[12] 测试结果正常
/yore/regex/data_t_0[0-9] 测试结果正常
/yore/regex/data_t_[^a-z]? 测试结果正常
/yore/regex/data_{t,tb}_0[0-9] 测试结果正常
/yore/regex/data_{t,t{b,bl}}_* 测试结果正常

再次执行 问题复现 小节 3.3 普通方式指定path 的测试也可以成功执行。

本次源码 Bug 修复的代码可以先到我的 GitHue 仓库下获取 yoreyuan / DataX

编译完整的项目请执行:mvn clean package -DskipTests assembly:assembly,打包完毕后的项目代码在项目根目录下的 target
编译成功后的包



没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 点我我会动 设计师: 上身试试
应支付0元
点击重新获取
扫码支付

支付成功即可阅读