sqoop系列-sqoop MySQL 導入Hive JSON 字段亂碼

問題簡介

最近業務方反饋依照導入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導出亂碼;但在這次問題排查中也收穫了

  1. sqoop導入源碼流程的熟悉,字段和類型的處理
  2. 如何更新oozie共享jar,加深了對oozie的掌控
  3. 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列表(正常應該有多條數據)
發佈了11 篇原創文章 · 獲贊 5 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章