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 [email protected]: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 修復源碼的Bug

在修復之前,因爲我們將 maven-assembly-plugin 升級到了 2.6 版本,在2.2 版本的時候添加了對 id 標籤的校驗(連接),因此我們需要在src/main/assembly/package.xml 中的id 標籤中填寫上內容,這裏改爲 <id>${project.artifactId}</id>

<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>${project.artifactId}</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-0.0.1-SNAPSHOT.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>

通過前面源碼分析 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不管是普通形式還是正則形式都可以)發現可以正常執行。


7 修復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



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章