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
報錯信息如下
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
...