Sqoop-1.4.6 Merge源碼分析與改造使其支持多個merge-key

  Sqoop中提供了一個用於合併數據集的工具sqoop-merge。官方文檔中的描述可以參考我的另一篇博客Sqoop-1.4.5用戶手冊
  Merge的基本原理是,需要指定新數據集和老數據集的路徑,根據某個merge-key,在reduce過程中,優先取出新數據集中的數據,共同合併成新的全量數據。具體的邏輯分析可以稍後通過看Sqoop-1.4.6的源碼來進一步瞭解。
  但是,在原生的Sqoop中,目前只支持merge-key爲一個字段的情況,本文通過分析源代碼並對源代碼進行更改,可以在使用Sqoop的Merge功能時支持任意多個merge-key。

一、Sqoop Merge Tool使用示例

  在這裏模擬一次數據增量同步到hive中的過程。

1、數據準備

  有一張分區表sqoop_merge_all,分區字段pt,在hdfs上的文件存儲路徑爲:hdfs://m000/user/hive/warehouse/sqoop_merge_all,表結構如下:

字段 類型
id int
type string
comments string
updatetime string

  另外有一張增量表sqoop_merge_inc,該表的hdfs路徑爲hdfs://m000/user/hive/warehouse/sqoop_merge_inc
  看一下這兩張表中的數據:
sqoop_merge_all (pt=20160810)
  這裏寫圖片描述
假設這裏存的是20160810的全量數據。
sqoop_merge_inc
  這裏寫圖片描述
假設20160811當天,對type=type1的記錄進行了更新,並且新增了一條type=type3的記錄。
  現在,需要把sqoop_merge_inc中的兩條記錄與sqoop_merge_all分區 (pt=20160810)中的兩條記錄進行合併,合併後的數據存入sqoop_merge_all的分區 (pt=20160811)
  按照正常邏輯,id=1的那條記錄被更新成id=3的那一條,id=4的記錄新增,那麼最終sqoop_merge_all的分區 (pt=20160811)中應該包含的記錄分別爲id=2,id=3,id=4這三條。

2、Sqoop Merge操作

  完整的sqoop merge命令如下:

sqoop merge \
  --new-data hdfs://m000/user/hive/warehouse/sqoop_merge_inc \
  --onto hdfs://m000/user/hive/warehouse/sqoop_merge_all/pt=20160810 \
  --target-dir hdfs://m000/user/hive/warehouse/sqoop_merge_all/pt=20160811 \
  --jar-file /usr/local/sqoop/bindir/sqoop_merge.jar \
  --class-name sqoop_merge \
  --merge-key type

  簡單說明一下,上面這句命令中表示,將新的數據集(參數--new-data)與已有的數據集(參數--onto)進行合併,合併後的數據存入(參數--target-dir)路徑下。需要指定這次合併中使用的表的結構jar包和class。根據type字段進行合併。
  最終結果如下,與上面期望的相一致。
  這裏寫圖片描述

  我們看一眼sqoop_merge.jar以及sqoop_merge.class的內容,基本上可以理解成這個類是sqoop_merge表的一個java bean定義。
  這裏寫圖片描述

二、Sqoop Merge Tool實現原理

  那麼,Sqoop是如何實現上面這個功能的呢?

1、腳本分析

  在$SQOOP_HOME/bin路徑下,有一個sqoop-merge腳本。
(1)sqoop-merge
  該腳本主要邏輯就是調用了sqoop merge命令,並把類似於--onto之類的參數都傳入。

prgm=`readlink -f $0`
bin=`dirname ${prgm}`
bin=`cd ${bin} && pwd`

exec ${bin}/sqoop merge "$@" 

(2)sqoop
  該腳本中,對參數進行處理後,最終執行下面這一句。把merge也一併傳入。

source ${bin}/configure-sqoop "${bin}"
exec ${HADOOP_COMMON_HOME}/bin/hadoop org.apache.sqoop.Sqoop "$@"

2、Java源代碼

  查看Sqoop源代碼,可以看到Sqoop是使用Java語言實現的。我們首先找到上面腳本中使用的類。由於每個類中的方法調用過程比較麻煩,接下來只分析主要代碼,首先,完整的調用鏈如下所示。

org.apache.sqoop.Sqoop#main
--> org.apache.sqoop.Sqoop#runTool(args) 
--> org.apache.sqoop.Sqoop#runTool(args, new Configuration())
--> org.apache.sqoop.Sqoop#runSqoop(Sqoop sqoop, args[1...end])
--> org.apache.hadoop.util.ToolRunner.run()
--> tool.run()

  最後這個tool的類型會詳細分析。
  
(1)org.apache.sqoop.Sqoop
  這個類是整個調用鏈的入口,這個類中主要方法的邏輯如下。
  在第二個runTool方法調用處,會根據傳入的第一個參數(即我們傳入的”merge”),生成一個SqoopTool類型的tool對象。

  public static int runTool(String [] args, Configuration conf) {
      ...
      String toolName = expandedArgs[0];
      Configuration pluginConf = SqoopTool.loadPlugins(conf);
      SqoopTool tool = SqoopTool.getTool(toolName);
      ...
      Sqoop sqoop = new Sqoop(tool, pluginConf);
      // 除去"merge"參數之外的其他參數,一起傳入runSqoop中
      return runSqoop(sqoop, Arrays.copyOfRange(expandedArgs, 1, expandedArgs.length));
  }

  那麼這個tool對象到底是個什麼呢?跟蹤進入com.cloudera.sqoop.tool#getTool方法,進入org.apache.sqoop.tool.SqoopTool#getTool方法。

(2)org.apache.sqoop.tool.SqoopTool
  這裏面,是從一個名爲TOOLS的Map中,根據toolName獲取對應的類對象。從下面代碼中我們可以看到Sqoop支持的所有Tool,並且merge對應的Tool是MergeTool類型的。

private static final Map<String, Class<? extends SqoopTool>> TOOLS;
TOOLS = new TreeMap<String, Class<? extends SqoopTool>>();
...
// registerTool方法最終都會向TOOLS中put一個對象
registerTool("codegen", CodeGenTool.class,
    "Generate code to interact with database records");
registerTool("create-hive-table", CreateHiveTableTool.class,
    "Import a table definition into Hive");
registerTool("eval", EvalSqlTool.class,
    "Evaluate a SQL statement and display the results");
registerTool("export", ExportTool.class,
    "Export an HDFS directory to a database table");
registerTool("import", ImportTool.class,
    "Import a table from a database to HDFS");
registerTool("import-all-tables", ImportAllTablesTool.class,
    "Import tables from a database to HDFS");
registerTool("import-mainframe", MainframeImportTool.class,
        "Import datasets from a mainframe server to HDFS");
registerTool("help", HelpTool.class, "List available commands");
registerTool("list-databases", ListDatabasesTool.class,
    "List available databases on a server");
registerTool("list-tables", ListTablesTool.class,
    "List available tables in a database");
registerTool("merge", MergeTool.class,
    "Merge results of incremental imports");
registerTool("metastore", MetastoreTool.class,
    "Run a standalone Sqoop metastore");
registerTool("job", JobTool.class,
    "Work with saved jobs");
registerTool("version", VersionTool.class,
    "Display version information");

  最前面那個調用鏈最後那個tool對象,對應的就是MergeTool類型。那麼接下來我們進入到MergeTool#run中。

(3)org.apache.sqoop.tool.MergeTool
  在這個方法中,生成一個MergeJob對象,然後通過該mergeJob的runMergeJob方法,運行一個MapReduce任務。

  public int run(SqoopOptions options) {
    try {
      // Configure and execute a MapReduce job to merge these datasets.
      MergeJob mergeJob = new MergeJob(options);
      if (!mergeJob.runMergeJob()) {
        LOG.error("MapReduce job failed!");
        return 1;
      }
    } catch (IOException ioe) {
      ...
    }
    return 0;
  }

(4)org.apache.sqoop.mapreduce.MergeJob
  這個類的runMergeJob方法是一個標準的MapReduce程序。我們主要跟蹤其Mapper類和Reducer類。

  public boolean runMergeJob() throws IOException {
      ...
      if (ExportJobBase.isSequenceFiles(jobConf, newPath)) {
        job.setInputFormatClass(SequenceFileInputFormat.class);
        job.setOutputFormatClass(SequenceFileOutputFormat.class);
        job.setMapperClass(MergeRecordMapper.class);
      } else {
        job.setMapperClass(MergeTextMapper.class);
        job.setOutputFormatClass(RawKeyTextOutputFormat.class);
      }
      ...
      job.setReducerClass(MergeReducer.class);
  }

  使用的Reducer類是MergeReducer,根據文件類型分別生成MergeRecordMapperMergeTextMapper類型的Mapper類。但是,不管表文件類型是什麼,這兩個Mapper類,最終共同繼承了MergeMapperBase類,並且在各自的map方法中,調用了MergeMapperBase#processRecord方法,map階段的主要邏輯也就在該方法中。

(5)org.apache.sqoop.mapreduce.MergeMapperBase
  這裏我們只分析processRecord方法。
  在這個方法中我們看到,每一條記錄對應一個MergeRecord對象,這個對象最後會在map的輸出中輸出到reduce階段。fieldMap是一個Map類型,其key就是我們通過參數--merge-key指定的字段,根據該字段名稱,從fieldMap中取出當前記錄該字段的值,轉化成String後,當作map的輸出,與MergeRecord對象一起交給Reduce來處理。

  protected void processRecord(SqoopRecord r, Context c)
      throws IOException, InterruptedException {
    MergeRecord mr = new MergeRecord(r, isNew);
    Map<String, Object> fieldMap = r.getFieldMap();
    if (null == fieldMap) {
      throw new IOException("No field map in record " + r);
    }
    Object keyObj = fieldMap.get(keyColName);
    if (null == keyObj) {
      throw new IOException("Cannot join values on null key. "
          + "Did you specify a key column that exists?");
    } else {
      c.write(new Text(keyObj.toString()), mr);
    }
  }

(6)org.apache.sqoop.mapreduce.MergeReducer
  這個類中,reduce方法的邏輯如下。  
  取出MergeRecord集合中相同key的所有記錄,如果新數據集中不包含當前字段值的記錄,則從舊的數據集中取該條記錄。如果新舊數據集中都有該記錄,則從新的數據集中取出該記錄。

  public void reduce(Text key, Iterable<MergeRecord> vals, Context c)
      throws IOException, InterruptedException {
    SqoopRecord bestRecord = null;
    try {
      for (MergeRecord val : vals) {
        if (null == bestRecord && !val.isNewRecord()) {
          // Use an old record if we don't have a new record.
          bestRecord = (SqoopRecord) val.getSqoopRecord().clone();
        } else if (val.isNewRecord()) {
          bestRecord = (SqoopRecord) val.getSqoopRecord().clone();
        }
      }
    } catch (CloneNotSupportedException cnse) {
      throw new IOException(cnse);
    }

    if (null != bestRecord) {
      c.write(bestRecord, NullWritable.get());
    }
  }

三、Sqoop Merge Tool源碼修改

  從上面源代碼過程分析可以看到,merge過程只能指定一個字段,如果指定多個字段時,會報如下的錯,提示當前指定的字段不存在。

16/08/22 15:54:15 INFO mapreduce.Job: Task Id : attempt_1470135750174_2508_m_000004_2, Status : FAILED
Error: java.io.IOException: Cannot join values on null key. Did you specify a key column that exists?
    at org.apache.sqoop.mapreduce.MergeMapperBase.processRecord(MergeMapperBase.java:79)
    at org.apache.sqoop.mapreduce.MergeTextMapper.map(MergeTextMapper.java:58)
    at org.apache.sqoop.mapreduce.MergeTextMapper.map(MergeTextMapper.java:34)
    at org.apache.hadoop.mapreduce.Mapper.run(Mapper.java:145)
    at org.apache.hadoop.mapred.MapTask.runNewMapper(MapTask.java:763)
    at org.apache.hadoop.mapred.MapTask.run(MapTask.java:339)
    at org.apache.hadoop.mapred.YarnChild$2.run(YarnChild.java:162)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAs(Subject.java:415)
    at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1491)
    at org.apache.hadoop.mapred.YarnChild.main(YarnChild.java:157)

  並且我們發現使用merge key對記錄進行合併主要發生在map階段,所以如果需要支持多個字段的merge時我們只需要修改MergeMapperBase#processRecord方法即可。修改後的代碼如下
  在使用sqoop merge時,多個字段用逗號分隔,把每個字段對應的值取出來拼接成新的key。

    protected void processRecord(SqoopRecord r, Context c)
            throws IOException, InterruptedException {
        MergeRecord mr = new MergeRecord(r, isNew);
        Map<String, Object> fieldMap = r.getFieldMap();
        if (null == fieldMap) {
            throw new IOException("No field map in record " + r);
        }
        Object keyObj = null;
        if (keyColName.contains(",")) {
            String connectStr = new String(new byte[]{1});
            StringBuilder keyFieldsSb = new StringBuilder();
            for (String str : keyColName.split(",")) {
                keyFieldsSb.append(connectStr).append(fieldMap.get(str).toString());
            }
            keyObj = keyFieldsSb;
        } else {
            keyObj = fieldMap.get(keyColName);
        }

        if (null == keyObj) {
            throw new IOException("Cannot join values on null key. "
                    + "Did you specify a key column that exists?");
        } else {
            c.write(new Text(keyObj.toString()), mr);
        }
    }

  上面需要注意的一點是,我的拼接符使用了一個byte的String,這樣可以避免以下這種情況。
  假設使用“+”當拼接符,如果存在兩條記錄:

Field a Field b
a+ b
a +b

  使用字段a,b進行merge時,上面兩條不一樣的記錄最終會被程序認爲是相同的,由此會產生新的數據不準確問題。
  
  有關該問題的更多信息可以參考[SQOOP-3002]

四、多字段的merge

  還是以上面兩張表爲例進行測試,表sqoop_merge_all使用兩個新的分區pt=20160801,pt=20160802
  sqoop_merge_all (pt=20160801)數據
  這裏寫圖片描述
  sqoop_merge_inc數據
  這裏寫圖片描述
  指定--merge-key type,comments進行merge,理論上只有id=1的那一條記錄被更新成id=5的那一條。合併後的數據應該包含id=2,id=3,id=4,id=5這四條記錄。

sqoop merge \
  --new-data hdfs://m000/user/hive/warehouse/sqoop_merge_inc \
  --onto hdfs://m000/user/hive/warehouse/sqoop_merge_all/pt=20160801 \
  --target-dir hdfs://m000/user/hive/warehouse/sqoop_merge_all/pt=20160802 \
  --jar-file /usr/local/sqoop/bindir/sqoop_merge.jar \
  --class-name sqoop_merge \
  --merge-key type,comments

  merge後,最終sqoop_merge_all (pt=20160802)的數據爲:
  這裏寫圖片描述

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