Sqoop使用分析

Sqoop的Mysql數據導出實現分兩種,一種是使用JDBC方式從Mysql中獲取數據,一種是使用MysqlDump命令從MySql中獲取數據,默認是 JDBC方式獲取數據,如果要使用dump方式獲取數據,需要添加 -direct 參數。

先說第一種:

配置語句時,需要添加 $CONDITIONS 點位符,比如:SELECT id FROM user WHERE $CONDITIONS,Sqoop在內部實現時會把它替換成需要的查詢條件。

Sqoop起動後會先查詢元數據,它會把 $CONDITIONS 替換爲 (1=0) ,然後用得到的SQL語句查詢數據庫。這塊Sqoop的實現不太好,對於導出一個表的情況,它會使用這個SQL查詢三次數據庫,分別是:獲取 colInfo(最終得到columnTypes信息)、查詢ColumnNames信息、生成QueryResult類時 generateFields操作獲取columnTypeNames時。

Sqoop會對獲取的Fields做校驗,列不能重複,它還會處理數據庫的字段到Java屬性名的轉換

QueryResult類是通過構建類文件,然後獲取JavaCompiler,然後編譯加載,爲了提高處理性能,這塊不是使用反射 實現,這個生成類內部處理mysql到hdfs屬性值爲空和分隔符的處理。

接着它會進行下面一個Sql查詢操作,查詢結果集爲MIN(split列),MAX(split列),查詢條件的處理邏輯爲 $CONDITIONS 替換爲(1=1),然後組合 (舉例: SELECT MIN(id), MAX(id) FROM (SELECT ID,NAME,PASSPORT WHERE (1=1) ) AS t1 ),這樣就查詢出來此次導出數據最大的split列值和最小的split列值。

對於爲整數、布爾值、時間格式、Float等 的分區列,進行split構建比較容易,這裏就不多說,對於Text文本的處理方式需要解釋一下,其先會對之前獲取到的Min和Max的字串尋找它們最大 的相同字串,然後對於後面的字段轉化爲BigDecimal,結合char佔兩個字節(65536),進行處理,算法在 TextSplitter類中,比較簡單,就是一個進制轉換的問題。拆分好後,需要把Split的值再轉換爲String,然後加上公共 前綴,就構成了查詢區間了。

其對數據的獲取是在DataDrivenDBRecordReader中,在查詢時會把 $CONDITIONS 替換成 split 的範圍比如 ( id >= 1) && (id<10),使用JDBC獲取到遊標,然 後移動遊標處理數據。

第二種方法與第一種方式有下面的差別:

初始化元數據,它是在構建的查詢語句後面添加 limit 1 ,比如:SELECT t.* FROM `user` AS t LIMIT 1,因爲dump方式查詢指定獲取列是 t.*,當使用limit 0時,數據庫不會給它返回必須的元數據信息。

dump方式在map進行數據的獲取,其會構建mysqldump命令,然後使用java去調用,輸入輸出流和錯誤流,其實現了 org.apache.sqoop.util.AsyncSink抽象類,用來處理輸入輸出流和錯誤流。

優化策略:

Sqoop查詢無數據會進行三次相同的Sql查詢,可以合併查詢,不過由於查詢很快,這塊不需要修改實現。

分區列選擇對於查詢元數據和導出的查詢都有影響,應該對索引做調優,避免對分區列的排序操作,加快元數據查詢速度和導出數據的速度,儘量選擇自增加的主鍵ID做Split列,區分度好並且可以順序讀取數據。

導出操作的查詢語句中,$CONDITIONS 會被替換爲範圍區間,創建索引時,要考慮這個查詢的優化。

索引建議,考慮三個規則(使查詢數據集較少、減少點的查詢、避免排序操作),Sqoop場景下,如果分區列不是主鍵(自增加)時,把分 區列做爲聯合索引的第一個字段,其它被選擇的查詢條件做爲索引的其它字段,可優化此查詢。

分區列的選擇,要避免Split後數據不均衡。

從實現上來看-m參數是可以增加任務的並行度的,但數據庫的讀線程是一定的,所以-m過大對於數據庫會是一個壓力,當然可以限制任務的同時最多擁有資源量。在Sqoop的場景下,數據庫纔是一個影響併發的瓶頸,增加job數意義不大。

下面列出Sqoop目前1.4.6版本存在的兩個問題。

查看Sqoop源碼,發現其存在兩個比較嚴重的問題。

問題 1、數據分片與Mapper的問題

Sqoop在抽取時可以指定-m的參數,但這個-m的參數是控制mapper的數量的,但它也決定了最後能夠生成的文件的數目,調節這個值可以實現對結果文件大小的控制,但是,如果產生的文件的格式不能夠被分割,那麼對這個數據的下游性能有很大影響,同時Sqoop在啓動時會啓動-m個MapperTask,會對數據庫產生m的併發讀取,需要修改Sqoop的實現,合併多個Split到同一個Mapper中。

個人建議可以加個 -split-per-map 參數,比如設置-m=4 -split-per-map=2,則對結果集分 8 片,每個Mapper處理兩片數據,最後共產生 8 個文件。

問題 2、分片效率低

Sqoop在做分片處理時有問題,其實現會使用Select Max(splitKey),Min(splitKey) From ( –select參數 ) as t1查詢分片信息,在Mysql下,這樣的查詢會產生一個以split-id爲主鍵的臨時表,如果數據量不大,臨時表數據可以在內存中,處理速度還可以保證。但如果數據量很大,內存中已經存放不下時,這些數據會被保存爲MyISAM表存放到磁盤文件中,如果數據量再大一些,磁盤文件已經存放不下臨時表時,拆分數據會失敗。如果數據量大,即使沒有查詢也會很慢,大約會佔用整個導出時間的45%,優化空間很大,如果不修改實現的話,不適合做大數據量表的全量數據導出操作。

解決方案一:

配置–boundary-query參數,指定使用的查詢語句

解決方案二:

修改:org.apache.sqoop.mapreduce.DataDrivenImportJob的

@Contract(“null, _ -> !null”)private String buildBoundaryQuery(String col, String query)

修改代碼如下

/**
   * Build the boundary query for the column of the result set created by
   * the given query.
   * @param col column name whose boundaries we're interested in.
   * @param query sub-query used to create the result set.
   * @return input boundary query as a string
   */
  private String buildBoundaryQuery(String col, String query) {
    if (col == null || options.getNumMappers() == 1) {
      return "";
    }

    // Replace table name with alias 't1' if column name is a fully
    // qualified name.  This is needed because "tableName"."columnName"
    // in the input boundary query causes a SQL syntax error in most dbs
    // including Oracle and MySQL.
    String alias = "t1";
    int dot = col.lastIndexOf('.');
    String qualifiedName = (dot == -1) ? col : alias + col.substring(dot);

    ConnManager mgr = getContext().getConnManager();
    String ret = mgr.getInputBoundsQuery(qualifiedName, query);
    if (ret != null) {
      return ret;
    }

//    return "SELECT MIN(" + qualifiedName + "), MAX(" + qualifiedName + ") "//        + "FROM (" + query + ") AS " + alias;
    return initBoundaryQuery(qualifiedName, query, alias);
  }

  private String initBoundaryQuery(String qualifiedName, String query, String alias) {
    StringBuilder regex = new StringBuilder();
    regex.append("(\\s[A|a][S|s][\\s][`]?");
    for (char c : qualifiedName.toCharArray()) {
      regex.append('[').append(c).append(']');
    }
    regex.append("[`|\\s|,])");
    final Matcher matcher1 = Pattern.compile(regex.toString()).matcher(query);
    final boolean asCheckOk = !matcher1.find();
    if(asCheckOk) {
      final Matcher matcher2 = Pattern.compile("(\\s[F|f][R|r][O|o][M|m]\\s)").matcher(query);
      int count = 0;
      while (matcher2.find()) {
        count++;
      }
      boolean fromCheckOk = count == 1;
      if(fromCheckOk) {
        final Matcher matcher = Pattern.compile("(\\s[F|f][R|r][O|o][M|m]\\s[\\s\\S]*)").matcher(query);
        while (matcher.find()) {
          return "SELECT MIN(" + qualifiedName + "), MAX(" + qualifiedName + ") "
                  + matcher.group();
        }
      }
    }
    return "SELECT MIN(" + qualifiedName + "), MAX(" + qualifiedName + ") "
            + "FROM (" + query + ") AS " + alias;
  }

問題 3、對非整形和布爾型的字段分區可能有數據丟失風險

Sqoop實現分區數據替換時,沒有使用Prepared statement來做,而是簡單的在查詢時會把 $CONDITIONS 替換成 split 的範圍比如 ( id >= xxxx) && (id<yyyy),但當String進行分區時,得到的xxxx和yyyy有很大可能是亂碼,然後就會引起查詢問題,這個在使用非direct方式時,可以通過修改爲Prepared Statement解決(未測試)。但在Direct方式下其導出數據使用的是mysqldump方式,通過命令行傳遞查詢參數,就無法解決了。(其實可以修改它的Split算法,使得範圍區間不會產生亂碼),所以建議不要使用非整形的列做拆分列。

問題4、Mysql中數據影響導出

Mysql在的timestamp列允許 “0000:00:00 00:00:00″ 這樣的數據存儲,在JDBC的實現中,Timestamp的格式是會被轉化爲java.sql.Timestamp的對象的,但java.sql.Timestamp對象無法表示 “0000:00:00 00:00:00″,所以在調用java.sql.Timestamp getTimestamp(int columnIndex) throws SQLException;這個方法時 SQLException 會被拋出來,Sqoop的JDBC方式導出數據到HDFS的實現就是採用這個方法去讀取Timestamp的數據,當數據中出現這樣的時間存儲時,就直接拋出了SQLException異常,這個異常沒有被捕獲,導致整個導出失敗。

我們可以在Sqoop做相應的修改,讓它避免拋出異常,使任務可以執行下去。

// 代碼在// org.apache.sqoop.orm.ClassWriter private void myGenerateDbRead(Map<String, Integer> columnTypes,//                                  String[] colNames,//                                  StringBuilder sb,//                                  int methodNumber,//                                  int size,//                                  boolean wrapInMethod)
...
  if("java.sql.Timestamp".equals(javaType)) {
    sb.append("    try {\n");
  }
  sb.append("   this." + col + " = JdbcWritableBridge." +  getterMethod
      + "(" + (i + 1) + ", __dbResults);\n");
  if("java.sql.Timestamp".equals(javaType)) {
    sb.append("    } catch (SQLException e) {\n    this." + col + " = null;\n    }");
  }
...

問題4、Sqoop導出時數據中特殊字符的替換

Sqoop抽取時可以對Hive默認的分隔符做替換,它們是\n \r \01,可以使用 –hive-drop-import-delims做替換,但是它的實現是寫死的,如果我們採用的不是Hive默認的分隔符,那麼它就不會做相應的替換操作,在Hive中很多人習慣使用\t做列分隔,因爲mysql的客戶端導出文本默認就是以\t導出的,Sqoop不會對這個數據進行替換。

有兩種方法可以解決這個問題。

方法1:修改Sqoop實現,代碼在 org.apache.sqoop.lib.FieldFormatter方法2:由Mysql做替換,Sql語句可以寫爲: replace(colname, “\t”, “”) as colname

問題5、sqoop導入mysql數據出錯

這個是由於mysql-connector-java的bug造成的,出錯時我用的是mysql-connector-java-5.1.10-bin.jar,更新成mysql-connector-java-5.1.32-bin.jar就可以了。mysql-connector-java-5.1.32-bin.jar的下載地址爲http://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.32.tar.gz。下載完後解壓,在解壓的目錄下可以找到mysql-connector-java-5.1.32-bin.jar

報錯信息如下

14/12/03 16:37:58 ERROR manager.SqlManager: Error reading from database: java.sql.SQLException: Streaming result set com.mysql.jdbc.RowDataDynamic@54b0a583 is still active. No statements may be issued when any streaming result sets are open and in use on a given connection. Ensure that you have called .close() on any active streaming result sets before attempting more queries.

java.sql.SQLException: Streaming result set com.mysql.jdbc.RowDataDynamic@54b0a583 is still active. No statements may be issued when any streaming result sets are open and in use on a given connection. Ensure that you have called .close() on any active streaming result sets before attempting more queries.
...
14/12/03 16:37:58 ERROR tool.ImportTool: Encountered IOException running import job: java.io.IOException: No columns to generate for ClassWriter
...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章