Hive實戰UDF 外部依賴檔案找不到的問題

大資料技術派發表於2021-12-16


關注公眾號:大資料技術派,回覆“資料”,領取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 資料遷移用法詳解

數倉建模分層理論

相關文章