Hive實戰UDF 外部依賴文件找不到的問題


關注公衆號:大數據技術派,回覆“資料”,領取1000G資料。

其實這篇文章的起源是,我司有數據清洗時將ip轉化爲類似中國-湖北-武漢地區這種需求。由於ip服務商提供的Demo,只能在本地讀取,我需要將ip庫上傳到HDFS分佈式存儲,每個計算節點再從HDFS下載到本地。

那麼到底能不能直接從HDFS讀取呢?跟我強哥講了這件事後,不服輸的他把肝兒都熬黑了,終於給出瞭解決方案。

關於外部依賴文件找不到的問題

其實我在上一篇的總結中也說過了你需要確定的上傳的db 文件在那裏,也就是你在hive 中調用add file之後 會出現添加後的文件路徑或者使用list 命令來看一下

今天我們不討論這個問題我們討論另外一個問題,外部依賴的問題,當然這個問題的引入本來就很有意思,其實是一個很簡單的事情。

爲什麼要使用外部依賴

重點強調一下我們的外部依賴並不是單單指的是jar包依賴,我們的程序或者是UDF 依賴的一切外部文件都可以算作是外部依賴。

使用外部依賴的的原因是我們的程序可能需要一些外部的文件,或者是其他的一些信息,例如我們這裏的UDF 中的IP 解析庫(DB 文件),或者是你需要在UDF 訪問一些網絡信息等等。

爲什麼idea 裏面可以運行上線之後不行

我們很多如人的一個誤區就是明明我在IDEA 裏面都可以運行爲什麼上線或者是打成jar 包之後就不行,其實你在idea 可以運行之後不應該直接上線的,或者說是不應該直接創建UDF 的,而是先應該測試一下jar 是否可以正常運行,如果jar 都不能正常運行那UDF 坑定就運行報錯啊。

接下來我們就看一下爲什麼idea 可以運行,但是jar 就不行,代碼我們就不全部粘貼了,只粘貼必要的,完整代碼可以看前面一篇文章

@Override
public ObjectInspector initialize(ObjectInspector[] arguments) throws UDFArgumentException {
    converter = ObjectInspectorConverters.getConverter(arguments[0], PrimitiveObjectInspectorFactory.writableStringObjectInspector);
    
    String dbPath = Ip2Region.class.getResource("/ip2region.db").getPath();
    File file = new File(dbPath);
    if (file.exists() == false) {
        System.out.println("Error: Invalid ip2region.db file");
        return null;
    }
    DbConfig config = null;
    try {
        config = new DbConfig();
        searcher = new DbSearcher(config, dbPath);
    } catch (DbMakerConfigException | FileNotFoundException e) {
        e.printStackTrace();
    }


    return PrimitiveObjectInspectorFactory.writableStringObjectInspector;

}

這就是我們讀取外部配置文件的方法,我們接下來寫一個測試

@Test
public void ip2Region() throws HiveException {
    Ip2Region udf = new Ip2Region();
    ObjectInspector valueOI0 = PrimitiveObjectInspectorFactory.javaStringObjectInspector;
    ObjectInspector[] init_args = {valueOI0};
    udf.initialize(init_args);
    String ip = "220.248.12.158";

    GenericUDF.DeferredObject valueObj0 = new GenericUDF.DeferredJavaObject(ip);

    GenericUDF.DeferredObject[] args = {valueObj0};
    Text res = (Text) udf.evaluate(args);
    System.out.println(res.toString());
}

我們發現是可以正常運行的,這裏我們把它打成jar 包再運行一下,爲了方便測試我們將這個測試方法改成main 方法,我們還是先在idea 裏面運行一下

我們發現還是可以正常運行,我們接下來打個jar包試一下

Error: Invalid ip2region.db file
java.io.FileNotFoundException: file: /Users/liuwenqiang/workspace/code/idea/HiveUDF/target/HiveUDF-0.0.4.jar!/ip2region.db (No such file or directory)
        at java.io.RandomAccessFile.open0(Native Method)
        at java.io.RandomAccessFile.open(RandomAccessFile.java:316)
        at java.io.RandomAccessFile.<init>(RandomAccessFile.java:243)
        at java.io.RandomAccessFile.<init>(RandomAccessFile.java:124)
        at org.lionsoul.ip2region.DbSearcher.<init>(DbSearcher.java:58)
        at com.kingcall.bigdata.HiveUDF.Ip2Region.main((Ip2Region.java:42)
Exception in thread "main" java.lang.NullPointerException
        at com.kingcall.bigdata.HiveUDF.Ip2Region.main(Ip2Region.java:48)

我們發現jar 包已經報錯了,那你的UDF 肯定運行不了了啊,其實如果你仔細看的話就知道爲什麼報錯了 /Users/liuwenqiang/workspace/code/idea/HiveUDF/target/HiveUDF-0.0.4.jar!/ip2region.db 其實就是這個路徑,我們很明顯看到這個路徑是不對的,所以這就是我們UDF報錯的原因

依賴文件直接打包在jar 包裏面不香嗎

上面找到了這個問題,現在我們就看一下如何解決這個問題,出現這個問題的原因就是打包後的路徑不對,導致我們的不能找到這個依賴文件,那我們爲什要這個路徑呢。這個主要是因爲我們使用的API 的原因

DbConfig config = new DbConfig();
DbSearcher searcher = new DbSearcher(config, dbPath);

也就是說我們的new DbSearcher(config, dbPath) 第二個參數傳的是DB 的路徑,所以我們很自然的想到看一下源碼是怎麼使用這個路徑的,能不能傳一個其他特定的路徑進去,其實我們從idea 裏面可以運行就知道,我們是可以傳入一個本地路徑的。

這裏我們以memorySearch 方法作爲入口

   	// 構造方法
    public DbSearcher(DbConfig dbConfig, String dbFile) throws FileNotFoundException {
        this.dbConfig = dbConfig;
        this.raf = new RandomAccessFile(dbFile, "r");
    }
    // 構造方法
    public DbSearcher(DbConfig dbConfig, byte[] dbBinStr) {
        this.dbConfig = dbConfig;
        this.dbBinStr = dbBinStr;
        this.firstIndexPtr = Util.getIntLong(dbBinStr, 0);
        this.lastIndexPtr = Util.getIntLong(dbBinStr, 4);
        this.totalIndexBlocks = (int)((this.lastIndexPtr - this.firstIndexPtr) / (long)IndexBlock.getIndexBlockLength()) + 1;
    }
		// memorySearch 方法
    public DataBlock memorySearch(long ip) throws IOException {
        int blen = IndexBlock.getIndexBlockLength();
      	// 讀取文件到內存數組
        if (this.dbBinStr == null) {
            this.dbBinStr = new byte[(int)this.raf.length()];
            this.raf.seek(0L);
            this.raf.readFully(this.dbBinStr, 0, this.dbBinStr.length);
            this.firstIndexPtr = Util.getIntLong(this.dbBinStr, 0);
            this.lastIndexPtr = Util.getIntLong(this.dbBinStr, 4);
            this.totalIndexBlocks = (int)((this.lastIndexPtr - this.firstIndexPtr) / (long)blen) + 1;
        }

        int l = 0;
        int h = this.totalIndexBlocks;
        long dataptr = 0L;

        int m;
        int p;
        while(l <= h) {
            m = l + h >> 1;
            p = (int)(this.firstIndexPtr + (long)(m * blen));
            long sip = Util.getIntLong(this.dbBinStr, p);
            if (ip < sip) {
                h = m - 1;
            } else {
                long eip = Util.getIntLong(this.dbBinStr, p + 4);
                if (ip <= eip) {
                    dataptr = Util.getIntLong(this.dbBinStr, p + 8);
                    break;
                }

                l = m + 1;
            }
        }

        if (dataptr == 0L) {
            return null;
        } else {
            m = (int)(dataptr >> 24 & 255L);
            p = (int)(dataptr & 16777215L);
            int city_id = (int)Util.getIntLong(this.dbBinStr, p);
            String region = new String(this.dbBinStr, p + 4, m - 4, "UTF-8");
            return new DataBlock(city_id, region, p);
        }
    }

其實我們看到memorySearch 方法首先是讀取DB 文件到內存的字節數組然後使用,而且我們看到有這樣一個字節數組的構造方法DbSearcher(DbConfig dbConfig, byte[] dbBinStr)

既然讀取文件不行,那我們能不能直接傳入字節數組呢?其實可以的

DbSearcher searcher=null;
DbConfig config = new DbConfig();
try {
    config = new DbConfig();
} catch (DbMakerConfigException e) {
    e.printStackTrace();
}
InputStream inputStream = Ip2Region.class.getResourceAsStream("/ip2region.db");
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int n = 0;
while (-1 != (n = inputStream.read(buffer))) {
    output.write(buffer, 0, n);
}
byte[] bytes = output.toByteArray();
searcher = new DbSearcher(config, bytes);
// 只能使用memorySearch 方法
DataBlock block = searcher.memorySearch(ip);

//打印位置信息(格式:國家|大區|省份|城市|運營商)
System.out.println(block.getRegion());

我們還是先在Idea 裏面測試,我們發現是可以運行的,然後我們還是打成jar包進行測試,這次我們發現還是可以運行中國|0|上海|上海市|聯通

也就是說我們已經把這個問題解決了,有沒有什麼問題呢?有那就是DB 文件在jar 包裏面,不能單獨更新,前面我們將分詞的時候也水果,停用詞庫是隨着公司的業務發展需要更新的 DB庫也是一樣的。

也就是說可以這樣解決但是不完美,我看到有的人是這樣做的他使用getResourceAsStream 把數據讀取到內存,然後再寫出成本地臨時文件,然後再使用,我只想說這個解決方式也太不友好了吧

  1. 文件不能更新
  2. 需要寫臨時文件(權限問題,如果被刪除了還得重寫)

只能使用memorySearch 方法

這個原因值得說明一下,因爲你使用其他兩個search 方法的時候都會拋出異常Exception in thread "main" java.lang.NullPointerException

這主要是因爲其他兩個方法都是涉及到從文件讀取數據進來,但是我們的raf 是null

學會獨立思考並且解決問題

上面我們的UDF 其實已經可以正常使用了,但是有不足之處,這裏我們就處理一下這個問題,前面我們說過了其實在IDEA 裏的路徑參數可以使用,那就說明傳入本地文件是可以的,但是有一個問題就是我們的UDF 是可能在所有節點上運行的,所以傳入本地路徑的前提是需要保證所有節點上這個本地路徑都可用,但是這樣維護成本也很高,還不如直接將依賴放在jar 包裏面。

繼承DbSearcher

其實我們是可以將這個依賴放在OSS或者是HDFS 上的,但是這個時候你傳入路徑之後,還是有問題,因爲構造方法裏面讀取文件的時候默認的是本地方法,其實這個時候你可以繼承DbSearcher 方法,然後添加新的構造方法,完成從HDFS 上讀取文件。

// 構造方法
public DbSearcher(DbConfig dbConfig, byte[] dbBinStr) {
    this.dbConfig = dbConfig;
    this.dbBinStr = dbBinStr;
    this.firstIndexPtr = Util.getIntLong(dbBinStr, 0);
    this.lastIndexPtr = Util.getIntLong(dbBinStr, 4);
    this.totalIndexBlocks = (int)((this.lastIndexPtr - this.firstIndexPtr) / (long)IndexBlock.getIndexBlockLength()) + 1;
}

讀取文件傳入字節數組

還有一個方法就是我們直接使用第二個構造方法,dbBinStr 就是我們讀取進來的字節數組,這個時候不論這個依賴是在HDFS 還是OSS 上你只要調用相關的API 就可以了,其實這個方法我們在讀取jar包裏面的文件的時候已經使用過了

下面的ctx就是OSS的上下問,用來從OSS上讀取數據,同理你可以從任何你需要的地方讀取數據。

DbConfig config = null;
try {
    config = new DbConfig();
} catch (DbMakerConfigException e) {
    e.printStackTrace();
}
InputStream inputStream = ctx.readResourceFileAsStream("ip2region.db");
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int n = 0;
while (-1 != (n = inputStream.read(buffer))) {
    output.write(buffer, 0, n);
}
byte[] bytes = output.toByteArray();
searcher = new DbSearcher(config, bytes);

總結

  1. Idea 裏面使用文件路徑是可以的,但是jar裏面不行,要使用也是本地文件或者是使用getResourceAsStream 獲取InputStream;
  2. 存儲在HDFS或者OSS 上的文件也不能使用路徑,因爲默認是讀取本地文件的;
  3. 多思考,爲什麼,看看源碼,最後請你思考一下怎麼在外部依賴的情況下使用binarySearch或者是btreeSearch方法;

猜你喜歡

數倉建模—寬表的設計

Spark SQL知識點與實戰

Hive計算最大連續登陸天數

Hadoop 數據遷移用法詳解

數倉建模分層理論

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