問題簡介
最近業務方反饋依照導入MySQL表導入Hive有部分字段變更亂碼,於是乎走上了解決亂碼的不歸路
。
集羣信息
服務器系統版本:centos 7.2
cdh 版本:cdh5.9.0
hadoop 版本:2.6.0+cdh5.9.0
hive 版本:1.1.0+cdh5.9.0
sqoop 版本:1.4.6+cdh5.9.0
備註:涉及敏感信息的的變量,使用${xxxx} 代替
問題定位
首先,導入亂碼想都不用想,肯定要確定mysql數據庫編碼是否有問題
於是乎打開導入mysql數據庫檢查一遍編碼
show variables like 'character%'
結果很滿意,編碼都是utf-8
檢查導入用的MySQL鏈接配置,已經添加了
useUnicode=true&characterEncoding=utf-8
這還亂碼!!!這還亂碼!!!這還亂碼!!! 不科學啊!!!!
同一張表中同樣是中文字段有的亂碼有的不亂,猜想是否字段類型問問題
查看錶字段信息
果然,字段類型不相同,但編碼都是utf-8,於是乎確定問題跟數據庫編碼和鏈接編碼無關,可能跟字段類型有關
。初步定位到sqoop導入MySQL字段類型爲JSON會亂碼。高興了,開心了,問題貌似找到了,於是乎百度,google關鍵詞:
sqoop 導入 MySQL 字段類型 JSON 亂碼
結果慘不忍睹,跟sqoop 相關的 json 亂碼沒有,好不容易找到一篇文章介紹,升級jdbc版本可以解決JAVA讀取MySQL JSON 字段亂碼的問題;還有另一篇文檔介紹:通過升級jdbc版本解決datax導入MySQL JSON 字段亂碼問題。
於是乎嘗試升級項目中使用的JDBC版本,但導入JSON亂碼還是沒解決!
度娘,google沒相關文章,可能解決的辦法嘗試了還不行。我太難啦!!我太難了!!太難了!!
沒辦法了,只能去看sqoop的原碼了,關於sqoop原碼解讀這兩篇文章很良心 文章一 文章二
擼出看源碼關心的點
關於導入字段處理的兩個點
導入數據庫類型到java的映射(org.apache.sqoop.manager.ConnManager)
/**
* Resolve a database-specific type to the Java type that should contain it.
* @param sqlType sql type
* @return the name of a Java type to hold the sql datatype, or null if none.
*/
public String toJavaType(int sqlType) {
// Mappings taken from:
// http://java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/mapping.html
if (sqlType == Types.INTEGER) {
return "Integer";
} else if (sqlType == Types.VARCHAR) {
return "String";
} else if (sqlType == Types.CHAR) {
return "String";
} else if (sqlType == Types.LONGVARCHAR) {
return "String";
} else if (sqlType == Types.NVARCHAR) {
return "String";
} else if (sqlType == Types.NCHAR) {
return "String";
} else if (sqlType == Types.LONGNVARCHAR) {
return "String";
} else if (sqlType == Types.NUMERIC) {
return "java.math.BigDecimal";
} else if (sqlType == Types.DECIMAL) {
return "java.math.BigDecimal";
} else if (sqlType == Types.BIT) {
return "Boolean";
} else if (sqlType == Types.BOOLEAN) {
return "Boolean";
} else if (sqlType == Types.TINYINT) {
return "Integer";
} else if (sqlType == Types.SMALLINT) {
return "Integer";
} else if (sqlType == Types.BIGINT) {
return "Long";
} else if (sqlType == Types.REAL) {
return "Float";
} else if (sqlType == Types.FLOAT) {
return "Double";
} else if (sqlType == Types.DOUBLE) {
return "Double";
} else if (sqlType == Types.DATE) {
return "java.sql.Date";
} else if (sqlType == Types.TIME) {
return "java.sql.Time";
} else if (sqlType == Types.TIMESTAMP) {
return "java.sql.Timestamp";
} else if (sqlType == Types.BINARY
|| sqlType == Types.VARBINARY) {
return BytesWritable.class.getName();
} else if (sqlType == Types.CLOB) {
return ClobRef.class.getName();
} else if (sqlType == Types.BLOB
|| sqlType == Types.LONGVARBINARY) {
return BlobRef.class.getName();
} else {
// TODO(aaron): Support DISTINCT, ARRAY, STRUCT, REF, JAVA_OBJECT.
// Return null indicating database-specific manager should return a
// java data type if it can find one for any nonstandard type.
return null;
}
}
導入數據庫映射到Hive的數據類型 (org.apache.sqoop.hive.HiveTypes)
/**
* Given JDBC SQL types coming from another database, what is the best
* mapping to a Hive-specific type?
*/
public static String toHiveType(int sqlType) {
switch (sqlType) {
case Types.INTEGER:
case Types.SMALLINT:
return "INT";
case Types.VARCHAR:
case Types.CHAR:
case Types.LONGVARCHAR:
case Types.NVARCHAR:
case Types.NCHAR:
case Types.LONGNVARCHAR:
case Types.DATE:
case Types.TIME:
case Types.TIMESTAMP:
case Types.CLOB:
return "STRING";
case Types.NUMERIC:
case Types.DECIMAL:
case Types.FLOAT:
case Types.DOUBLE:
case Types.REAL:
return "DOUBLE";
case Types.BIT:
case Types.BOOLEAN:
return "BOOLEAN";
case Types.TINYINT:
return "TINYINT";
case Types.BIGINT:
return "BIGINT";
default:
// TODO(aaron): Support BINARY, VARBINARY, LONGVARBINARY, DISTINCT,
// BLOB, ARRAY, STRUCT, REF, JAVA_OBJECT.
return null;
}
}
關於SQL字段類型(java.sql.Types)裏面記錄了 sql 類型對應的數值
如:
/**
* <P>The constant in the Java programming language, sometimes referred
* to as a type code, that identifies the generic SQL type
* <code>CHAR</code>.
*/
public final static int CHAR = 1;
/**
* <P>The constant in the Java programming language, sometimes referred
* to as a type code, that identifies the generic SQL type
* <code>VARCHAR</code>.
*/
public final static int VARCHAR = 12;
/**
* <P>The constant in the Java programming language, sometimes referred
* to as a type code, that identifies the generic SQL type
* <code>LONGVARCHAR</code>.
*/
public final static int LONGVARCHAR = -1;
導入獲取字段類型的源碼(org.apache.sqoop.orm.ClassWriter),
protected Map<String, Integer> getColumnTypes() throws IOException {
if (options.getCall() == null) {//導出, select xxx from table limit 1, 獲取rs 的類型
return connManager.getColumnTypes(tableName, options.getSqlQuery());
} else {//導入
return connManager.getColumnTypesForProcedure(options.getCall());
}
}
在看源碼的過程中,根據hive自動生成表字段類型,看得出, 導入的時候 sqoop 把json字段轉換成了string類型
1)懷疑是json類型 toString的時候沒有指定字符集編碼使用了機器默認的字符集導致了亂碼,然運維查看了集羣的編碼但是都是utf-8沒問題
2)源碼中無論是java還是hive的類型轉換都沒見有把 Json轉成string的,但是導入的時候沒報錯卻自動轉換成string,十分奇怪;
於是乎個單元測試 確定jdbc讀取出來的MySQL JSON類型的字段到底是什麼類型
public class test {
public static void main(String[] args) throws ParseException {
try (Connection con = JDBCUtils.getMySQLConn(
JDBCUtils.buildHiveConUrl("jdbc:mysql://${url}", ${port}, "${db}"),
"${user}", "${pwd}")) {
Statement stmt = con.createStatement();
ResultSet resultSet = stmt.executeQuery("select * from ${table} limit 1 ");
while (resultSet.next()) {
ResultSetMetaData metaData = resultSet.getMetaData();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
System.out.println(metaData.getColumnName(i) +
" ---> " + metaData.getColumnTypeName(i) +
" ---> " + metaData.getColumnType(i));
}
}
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
id ---> INT UNSIGNED ---> 4
uid ---> CHAR ---> 1
question_text ---> VARCHAR ---> -1
question_options ---> JSON ---> 1
question_answer ---> VARCHAR ---> -1
question_explain ---> VARCHAR ---> -1
question_type ---> INT ---> 4
question_diff ---> INT ---> 4
chapter_ids ---> JSON ---> 1
knowledge_ids ---> JSON ---> 1
tag_ids ---> JSON ---> 1
app_code ---> VARCHAR ---> 12
create_time ---> DATETIME ---> 93
結果發現jdbc在讀取json給的時候getColumnTypeName叫JSON但是類型(getColumnType)居然和char是相同的(ps:解答了JSON導入hive 成了String類型)。並且通過jdbc讀取出來的json字符串toSTring並沒有亂碼;
萬分無奈,因爲低版本jdbc讀取json亂碼,會不會是jdbc jar衝突呢
於是乎查找了 oozie 的 sqoop導入的日誌,發現還真是jar版本衝突
我自己的項目中指定了jdbc的版本,但是還加載了oozie的 sharelib jdbc;
對於oozie jar 衝突有兩種解決辦法
1)修改自己衝突的jar命名與sharelib中的已知,指定優先使用戶自定義的jar (jar名字必須與sharelib中一樣,否則還是會加載 sharelib的jar)
2)替換 sharelib 中的jar 並更新jar信息,更新jar信息很重要如果不更新用到sharelib會報錯;
總結
這次 sqoop MySQL導入Hive Json字段亂碼排查很複雜,但到最後去發現是JDBC衝突問題,加載了舊版的jdbc導致json導出亂碼;但在這次問題排查中也收穫了
- sqoop導入源碼流程的熟悉,字段和類型的處理
- 如何更新oozie共享jar,加深了對oozie的掌控
- JDBC中對MySQL字段類型的判定和處理,CHAR和JSON是同一個類型
幾個更新Oozie更新共享jar的命令
bin/oozie-setup.sh sharelib create -fs hdfs://${集羣} -locallib ${共享jar本地路徑} #從本地目錄向hdfs複製sharelib
bin/oozie admin -oozie http://${oozie-server-host}:11000/oozie -sharelibupdate #更新oozie的sharelib
bin/oozie admin -oozie http://${oozie-server-host}:11000/oozie -shareliblist #查看sharelib列表(正常應該有多條數據)