自己動手寫Impala UDF

網易雲發表於2018-05-09

本文由  網易雲 釋出。

 

概述

出於對可擴充套件性和效能的考慮,UDF已變成大資料生態圈查詢引擎的必備功能之一,無論是Calcite、Hive、Impala都對其進行支援,但是UDF的支援有利也有弊,好處在於它提供了對某些使用者獨有需求的支援,例如某些產品需要將表中的某欄位使用自定義的方式解析成可讀欄位,例如需要實現特殊的聚合函式;它的弊端在於它對使用者開發,這樣對於惡意的使用者可能執行非正常的邏輯,例如在函式中刪除或者拷貝其它檔案內容,從而對非授權資料造成破壞,因此對於一個SQL引擎來說,我們需要UDF的集中管理,所有使用者自定義的UDF都需要管理員審查原始碼,不允許普通使用者自己上傳UDF,從而避免意外的發生。

對於通常UDF的需求,個人覺得有兩方面的需求:1、系統提供的函式完成不了的需求,或者需要使用系統函式進行拼湊才能完成的需求。2、使用當前系統提供的函式效能太差,需要做一些特別的優化。另外,對於UDF還分為兩類:自定義函式(UDF)和自定義聚合函式(UDAF),前者會處理每一條輸入的記錄,轉換成處理後的結果,類似於map的功能,後者對於多條記錄進行聚合,輸出聚合之後的值,類似於reduce的功能。

眾所周知Impala使用了Java和C++實現(雖然大多數時候我們都說Impala是C++實現的,所以效能更好,但是它的SQL解析部分的確是Java實現的),Impala同樣也支援兩種語言的UDF,但是UDAF目前只能支援C++實現,本文分別介紹這些方法如何在

Impala中使用的。

目標

我們這裡的example實現一個UDF和一個UDAF,分別實現如下的需求: 我們需要實現的UDF為int level(int),功能為根據value的值計算出距離該值最近的2的冪數的冪值,如果有多個值則取最大的。例如15,距離它最近的2的冪數為16,則level(15)=4, level(9)=3, level(12)=3或4則level(12)=4. 我們需要實現的UDAF為int sum_str(string),功能為計算name中出現的第一個整數的和值,如果該字串不出現整數則為0,例如”abcd123ef”和”efdg23sd24″的和值為123+23=146. 這兩個需求算是比較奇葩了吧,看看如何在impala中利用UDF和UDAF實現它們。

實現

Java版UDF

瞭解Impala的都知道,Impala可以直接使用Hive的metestore作為後設資料庫,同樣Impala也可以直接使用Hive的UDF,所以對於之前奮鬥在Hive第一線的同學們使用Impala有了不少親切感,那麼在這裡就順便溫習一遍Hive的UDF使用流程吧。

使用Java實現的UDF必須繼承org.apache.hadoop.hive.ql.exec.UDF類,然後在該類中實現名為evaluate的函式,猜測 Hive/Impala是根據函式名來找到具體的實現的,當然一個類裡面也可以過載多個evaluate方法。該方法實現如下:

packagecom.netease.hive.udf;

importorg.apache.hadoop.hive.ql.exec.UDF;

publicclass LevelUDF extends UDF {

     public Integer evaluate(Integer value) {          if(value == null)

            return null;

         double temp = value;           int cnt = 0;           int max = 1;

          while(temp > 1) {                cnt ++;

            temp /= 2;             max *= 2;

          }

        if(max – value > (value – max / 2))

              cnt –;

          return cnt;

      }

      public static void main(String[] args) {

            System.out.println(newLevelUDF().evaluate(9));

      }

}

然後編譯成jar上傳到HDFS上,Impala的自定義函式,無論是Java實現還是C++實現的都需要手動放到HDFS上,而非像Hive那樣直接可以add jar命令,然後在impala shell中執行create function函式。

 

> hadoop fs-put ./udf.jar hdfs://namenode-or-nameservice/tmp/nrpt/

> create function level(int) returns intlocation `hdfs://namenode-or-nameservice/tmp/nrpt/udf.jar` symbol=`com.neteas

 

在impala中建立UDF必須指定引數和返回值,並且執行symbol為類名。根據引數和返回值在執行時查詢該函式,如果引數和返回值和類中實現有差別則會出現執行時錯誤(create function還是能夠成功的)。

 

> select level(15);

+——————-+

| default.level(15) |

+——————-+

| 4                 |

+——————-+

> select level(9);

+——————+

| default.level(9) |

+——————+

| 3                |

+——————+

 

使用Java實現UDF是非常簡單的 ,那麼為什麼還需要使用C++的實現呢,主要是效能的考慮,畢竟相同的邏輯使用C++實現無論是效能還是資源消耗都會比JAVA好得多,所以Impala官方是非常推崇使用C++實現UDF,並且聲稱效能會有10倍的提升。 

C++實現UDF

使用C++實現UDF就不那麼輕鬆了,首先要面臨的就是系統庫的差別,所以一般要求UDF開發機和Impala執行的機器使用相同的 Linux發行版,最好在其中的一臺Impalad機器上開發,避免出現不可預測的問題,C++開發UDF需要首先下載impalaudf devel開發包和一下其他依賴的工具:

sudo yum install gcc-c++ cmakeboost-devel sudo yum install impala-udf-devel

 

g++、cmake工具一般開發機上都是有的,boost開發包需要手動安裝,當然如果你的函式實現不需要boost也不需要安裝這個,不過Impala本身都是使用boost開發的,不過使用boost庫可以提高開發效率和效能,impala-udf-devel這個包在安裝的時候發現 ubuntu和debain上根本找不到,不過不用擔心,其實安裝這些包主要就是把這個包下面的.h檔案放到系統的include目錄下,把.so 檔案放到系統的lib下,只要有原始碼自己也可以編譯。可以在https://github.com/laserson/impala-udf-devel下載它們的原始碼(就兩個.h檔案和一個.cc檔案)。對於UDF開發更推薦這種方式,省的自己寫CMakeList.txt檔案了,clone到本地之後可以直接執行 cmake生成Makefile檔案。

 

> git clonehttps://github.com/laserson/impala-udf-devel.git

> cd impala-udf-devel/ > cmake.

 

此時會發現出現了錯誤:

CMake Error atCMakeLists.txt:46 (add_library):

  Cannot find source file:

 

 my-udf-file-1.cc

開啟CMakeList.txt檔案會發現add_library(myudf SHARED my-udf-file-1.cc my-udf-file-2.cc)這一行,它表示根據my-udf-file-

 

1.cc和my-udf-file-2.cc檔案生成一個動態連結庫myudf,我們可以通過該一下這個配置然後編寫自己的udf檔案,我們把 CMakeList.txt檔案的最後幾行改為如下:

 

# Build the UDA/UDFs into a shared library.  You can have multiple UDFs per # file, and/orspecify multiple files here. add_library(level-udfSHARED level-udf.cc)

 

# The resulting LLVM IR module will have the same name asthe .cc file if (CLANG_EXECUTABLE)   COMPILE_TO_IR(level-udf.cc) endif(CLANG_EXECUTABLE)然後編輯程式碼level-udf.h(建立):

#ifndefLEVEL_UDF_H

#defineLEVEL_UDF_H

#include”udf/udf.h”

using namespace impala_udf;

IntVal level(FunctionContext*context, const IntVal& value);

#endif                                                                                                                                                                                    

編輯level-udf.cc檔案(建立):

#include”level-udf.h”

using namespacestd;

IntVal level(FunctionContext* context,const IntVal& value) {    if(value.is_null)         returnIntVal::null();

   int original = value.val;    double temp = (double) original;    int cnt = 0;     int max = 1;     while(temp > 1) {         cnt ++;         temp /= 2;         max *= 2;

    }

   if((max – original) > (original – max / 2))         cnt –;     return IntVal(cnt);

}

這裡需要說明一下的是FunctionContext物件,這個物件是呼叫UDF函式之前由Impala自己建立的,它包含當前查詢的id、查詢執行的使用者名稱、使用該物件分配記憶體,並且可以向Impala日誌系統輸出warning日誌,或者直接輸出一條error日誌以結束該查詢。而每一個函式的輸入和輸出是IntVal型別,這個是在udf.h中定義的一個型別,它實際上就是包含null的int型別,除了該型別之外還有

 

AnyVal; BooleanVal; TinyIntVal;SmallIntVal; IntVal; BigIntVal; StringVal; TimestampVal這幾種型別,和Impala中的型別一一對應,其中AnyVal是其他型別的父類,它可以標識該物件是否null,其他的基本型別可以通過val成員獲取原始值(在不是null的情況下),StringVal可以通過ptr和len獲取首地址和長度,TimestampVal則可以獲取date和time_of_date分別獲取日期和納秒數(這樣的話TimestampVal就可以包含Date了,況且Impala還不支援Date型別)。這裡面沒有使用boost庫(大多數情況下是需要使用的),然後編譯,連結生成動態連結庫:

 

> cmake .

> make

 

執行完成之後在當前目錄下產生了build目錄(類似於maven構建之後的target目錄),該目錄下存在連結完成的liblevel-udf.so檔案,庫名為之前在CMakeList.txt檔案中修改之後的名字。

 

然後按照之前的方式首先上傳動態連結庫到HDFS,然後create function。

> hadoop fs-put ./liblevel-udf.so hdfs://namenode-or-nameservice/tmp/nrpt/

> createfunction level_c(INT) returns int location `hdfs://namenode-or-nameservice/tmp/nrpt/liblevel-udf.so`symbol=`

使用C++函式時symbol執行函式名,通過該函式名可以在liblevel-udf.so中找到該函式。

> select level_c(12);

+———————+

|default.level_c(12) |

+———————+

| 4                   |

+———————+

> select level_c(9);

+——————–+

|default.level_c(9) |

+——————–+

| 3                  |

+——————–+

 

至於C++實現和Java實現的效能對比,可以在實際的場景下使用不同的語言實現相同的邏輯,只要你的C++程式碼寫的不是太挫,那麼至少會有兩倍以上的效能差別的,這裡也推薦使用C++實現UDF,不過一定要注意記憶體洩漏、段錯誤等情況,如果實現不當可能到 impalad掛掉。對於 C++ 實現UDF的詳細說明可以參考Impala官方文件: http://www.cloudera.com/documentation/archive/impala/2-x/2-1-x/topics/impala_udf.html,更多的C++實現的UDF例項可以參考https://github.com/cloudera/impala-udf-samples

 

C++實現UDAF

 

實現一個聚合函式需要不像簡簡單單的實現一個UDF一樣,畢竟需要初始化環境,對於每一個輸入記錄進行聚合,這就要求它在整個執行過程中保持一個狀態,例如計算SUM時需要保持一個sum變數記錄已經處理的記錄的和,這樣的方式和reducer有所差別,它是通過對輸入的值迴圈呼叫aggr函式,而reducer則是將這個列表作為輸入處理。在Impala中實現一個UDAF需要實現下面這些函式:

 

INIT_FN:初始化操作,在查詢執行時執行一次,例如清理計數器,分配快取等。例如SUM時sum=0。 UPDATE_FN:在單機上執行的merge操作,對於每一個節點的每一條記錄呼叫一次該函式,例如SUM時執行sum+= value。

 

MERGE_FN:節點之間的merge,通常邏輯上和UPDATE_FN操作類似,例如SUM時執行sum+=value。

 

SERIALIZE_FN:對於INIT、UPDATE、MERGE階段的結果進行序列化,例如需要對包含指標的值使用它所執行的內容序列化,然後釋放該指標。一般情況下不需要設定該函式。

 

FINALIZE_FN:最終獲取結果的函式,只呼叫一次。

 

我們這裡需要實現的是統計某一個string列中每一個成員第一次出現的整數的和,我們定義為int sum_first_int(string),這裡不牽扯到指標的序列化,所以不需要實現SERIALIZE_FN函式。

 

該需求的實現sum-udaf.h(新建):

#ifndef_SUM_UDAF_H_

#define_SUM_UDAF_H_

#include”udf/udf.h”

using namespaceimpala_udf;

void init_func(FunctionContext*context, BigIntVal* result);

void update_func(FunctionContext*context, const StringVal& input, BigIntVal* result);

void merge_func(FunctionContext*context, const StringVal& input, BigIntVal* result);

BigIntVal finalize_func(FunctionContext*context, const BigIntVal& val);

#endif

 

實現了四個函式的定義,其中init_func函式傳遞了一個BigIntVal函式,它是由函式的返回值決定的,result引數作為儲存聚合結果,update_func函式對於每一個輸入的記錄進行處理,input引數為該記錄中該列的值,merge_func對於不同節點的聚合結果進行再聚合,finalize_func則是返回最終結果。可以看出每一個聚合組的狀態資訊儲存在result引數中。

 

實現檔案sum-udaf.cc(新建):

#include”sum-udaf.h”

#include<ctype.h>

void init_func(FunctionContext* context, BigIntVal* result) {

    result->is_null = false; result->val = 0;

}

void update_func(FunctionContext*context, const StringVal& input, BigIntVal* result) {     if(input.is_null) return;     int cur = 0;     uint8_t *ptr = input.ptr;     int len = input.len;     int i = 0;

   for(i = 0 ; i < len && !isdigit(ptr[i]); ++ i);     while(i < len &&isdigit(ptr[i])) {         cur = cur * 10 + (ptr[i] – `0`);

        ++ i;

    }

    result->val += cur;

}

void merge_func(FunctionContext*context, const BigIntVal& input, BigIntVal* result) {     result->val += input.val;

BigIntVal finalize_func(FunctionContext*context, const BigIntVal& val) {    return val;

}

完成之後再修改CMakeList.txt檔案,增加對sum-udaf.cc的編譯和連結,然後執行cmake和make(同上),就可以生成新的.so連結庫了。使用相同的方式將該庫上傳到HDFS然後再impala shell上建立聚合函式。

> createaggregate function sum_first_int(string) returns bigint

> location`hdfs://namenode-or-nameservice/tmp/nrpt/liblevel-udf.so` init_fn=`init_func`update_fn=`update_func` merg

此時可以使用該聚合函式了,如下:

> CREATETABLE default.test (id string);

> insertinto test values(“abc1234edf”), (“12shd45”), (“dhhf”),(“qwe3”), (“2016-09-01”);

> select sum_first_int(id) fromtest;

+—————————+

|default.sum_first_int(id) |

+—————————+

| 3265                      |

+—————————+

總結

 

本文詳細講述了在Impala中實現自定義函式和自定義聚合函式的方式並給出實現例項以供參考,使用UDF和UDAF我們可以很輕易的實現特定需求的計算邏輯,也可以用效能更好的方式實現一些相對固定的需求,例如我們可以使用UDAF函式實現更好效能的 distinct count計算(利用hyoerloglog實現有誤差的去重計數),但是開放的介面也可能存在一些安全性上的問題,給系統運維的也帶來了一定的負擔,所以對於自定義函式的引入也需要謹慎。

 

 

想要了解網易大資料,請戳這裡網易大資料|專業的私有化大資料平臺

 

瞭解 網易雲 :
網易雲官網:https://www.163yun.com/
新使用者大禮包:https://www.163yun.com/gift
網易雲社群:https://sq.163yun.com/

相關文章