Hive UDF使用資源文件及動態更新方案
背景
注: 本文中的“函數”等同於UDF,默認情況下特指永久函數。
Hive 0.13版本開始支持自定義永久函數(Permanent Function),可以將函數註冊到Hive Metastore,通過Hive/Beeline/Spark SQL可以直接引用,不需要類似於臨時函數(Temporary Function) ,每次使用時均需要顯式聲明創建的過程。
函數創建語句:
CREATE FUNCTION [db_name.]function_name AS class_name
[USING JAR|FILE|ARCHIVE 'file_uri' [, JAR|FILE|ARCHIVE 'file_uri'] ];
函數刪除語句:
DROP FUNCTION [IF EXISTS] function_name;
假設業務場景:自定義函數的計算邏輯過程中,需要使用 數據,且 數據 存在需要動態(定時或臨時)更新的情況。
-
數據量較小
數據量較小,且需要動態更新的場景下,可以將數據存儲至類似於MySQL的數據引擎中,數據需要更新時,直接更新MySQL中的數據即可;函數初始化時,可以將MySQL中的數據一次性加載到內存中,用於函數後續計算邏輯使用。
複雜一點的情況,可以考慮給MySQL中的每一條數據附加一個時間戳,以每次數據的更新時間爲準;也可以寫入新的數據時,使用一個較舊的時間戳,數據寫入完成之後,統一調整爲新的時間戳;函數初始化時,加載具有最新時間戳的數據。
注: 爲什麼需要一次性完成數據加載?Hive UDF運行於MapReduce或Spark分佈式計算應用中,如果UDF處理每一行數據均需要訪問MySQL,海量數據場景下,將會導致同一時刻有大量的併發連接,很容易壓垮MySQL。
-
數據量較大
數據量較大,且需要動態更新的場景下,不適用於將數據存儲至類似於MySQL的數據引擎,有以下2方面的考慮:
a. 數據加載時間較長;
b. 數據使用內存空間較大;這種情況下,建議使用 資源文件,即預先將數據寫入文件(文本文件或索引文件),將文件存儲至HDFS;函數初始化時,可以直接讀取HDFS中的文件,也可以將HDFS中的文件下載至本地後再讀取。
注: 索引文件可以應用於數據使用內存空間較大的場景下,如:將數據生成Lucene索引文件並上傳至HDFS,函數使用時將索引文件下載至本地,基於磁盤即可快速檢索數據,無需裝入內存。
資源文件更新策略參見下文。
資源文件動態更新
HDFS中的文件僅支持追加(Append)操作,不支持更新(Update)操作,因此無法通過直接修改HDFS中已有的資源文件內容完成更新。假設HDFS中資源文件的存儲目錄爲“/tmp/resource/”,資源文件名稱爲“resource.data”,以天爲粒度進行更新,方案如下:
- 使用當天的最新數據,創建資源文件,名稱:resource.data;
- 將資源文件resource.data上傳至HDFS,名稱:/tmp/resource/resource.data;
- 將已上傳至HDFS中的資源文件 /tmp/resource/resource.data 重命名爲 /tmp/resource/resource.data.yyyy_MM_dd;其中,yyyy_MM_dd 爲當天日期;
- 函數初始化時,根據資源文件的日期後綴,讀取或下載最新的資源文件;
資源文件存儲目錄示例如下:
/tmp/resource/resource.data.2020_05_01
/tmp/resource/resource.data.2020_05_02
/tmp/resource/resource.data.2020_05_03
...
注: 如果直接上傳帶有最新時間戳的資源文件,文件較大情況下,耗時可能較長,資源文件未上傳完成之前,可能會被正在運行的函數讀取或下載,因文件不完整引發異常;HDFS中的重命名爲原子操作,使用重命名的方式可以避免這種問題。
Hive UDF Jar 動態更新
Hive UDF目前僅支持創建(Create)/刪除(Drop),不支持更新(Update)。
創建一個名稱爲“udf_map_test”的函數:
CREATE FUNCTION udf_map_test AS 'com.weibo.dip.hubble.mis.udf.UdfMapTest' USING JAR 'viewfs://c9/user_ext/weibo_rd_dip/udf/jar/udf_map_test.jar';
該函數依賴位於HDFS的Jar文件:viewfs://c9/user_ext/weibo_rd_dip/udf/jar/udf_map_test.jar。
函數的計算邏輯需要更新時,可以修改函數相關的代碼,重新編譯、打包,然後可以選擇以下2種方式之一進行更新:
- 刪除函數,刪除HDFS Jar文件,重新上傳新的Jar文件至HDFS,創建函數;
- 刪除HDFS Jar文件,重新上傳新的Jar文件(兩者路徑名稱須保持一致);
上述2種方式或者其它類似方式,本質目的在於替換HDFS Jar文件。一般情況下,更新過程需要的時間較短,但 替換 過程並不是 原子 操作;如果更新過程中,恰好(概率大小取決於集羣規模及應用數量)有計算應用需要使用該函數,則會引發異常。
更好的方案是借鑑資源文件動態更新的策略。Hive UDF Jar中僅包含業務接口,不包含業務接口的實現類;業務接口的實現類位於額外的一個Jar中,這個Jar的存儲方式類似於資源文件動態更新,也使用時間戳後綴的命名方式;Hive UDF初始化時,根據類名稱及Jar名稱前綴從HDFS中選取最新的Jar,並從中加載具體的業務接口實現類。
業務接口與函數定義相對應,在函數的輸入參數個數及類型、返回結果類型不發生變化的情況化下,業務接口無須變更,業務接口生命週期相對穩固。函數的計算邏輯發生變化時,僅需要修改業務接口實現類,然後重新編譯、打包,並按照最新的時間戳上傳即可(過程參考資源文件更新)。
首先,需要一個兼容HDFS的類加載器,代碼如下:
import com.google.common.base.Preconditions;
import java.net.URL;
import java.net.URLClassLoader;
import org.apache.commons.lang.ArrayUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FsUrlStreamHandlerFactory;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.PathFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* UdfClassLoader.
*
* @author yurun
*/
public class UdfClassLoader extends URLClassLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(UdfClassLoader.class);
static {
try {
URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory());
} catch (Error e) {
if (!e.getMessage().equals("factory already defined")) {
throw new ExceptionInInitializerError(e);
}
}
}
private String directory;
private String file;
/**
* Initialize a instance.
*
* @param directory hdfs directory path
* @param file hdfs jar name prefix
*/
public UdfClassLoader(String directory, String file) {
super(new URL[] {}, UdfClassLoader.class.getClassLoader());
try {
Path hdfsDirectory = new Path(directory);
FileSystem fs = hdfsDirectory.getFileSystem(new Configuration());
FileStatus[] statuses =
fs.listStatus(
hdfsDirectory,
new PathFilter() {
@Override
public boolean accept(Path path) {
return path.getName().startsWith(file);
}
});
Preconditions.checkState(
ArrayUtils.isNotEmpty(statuses),
String.format("File %s not exist in directory %s", file, directory));
Path jarPath = statuses[statuses.length - 1].getPath();
LOGGER.info("Directory: {}, File: {}, Jar: {}", directory, file, jarPath.getName());
addURL(jarPath.toUri().toURL());
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
}
UdfClassLoader 繼承自 URLClassLoader,其中,
URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory());
FsUrlStreamHandlerFactory,設置 URLClassLoader 兼容HDFS數據訪問協議。
private String directory;
private String file;
directory 和 file 用於定義業務接口實現類所屬的Jar文件位於HDFS的存儲路徑和文件名稱(前綴)。
Path hdfsDirectory = new Path(directory);
FileSystem fs = hdfsDirectory.getFileSystem(new Configuration());
......
Path jarPath = statuses[statuses.length - 1].getPath();
LOGGER.info("Directory: {}, File: {}, Jar: {}", directory, file, jarPath.getName());
addURL(jarPath.toUri().toURL());
檢索具有最新時間戳的Jar文件,並加入 URLClassLoader 的類搜索路徑。
然後,定義、創建業務接口及其實現類;
public interface RelationService extends UdfService {
String get(String key);
}
public class RelationServiceImpl implements RelationService {
private static final Logger LOGGER = LoggerFactory.getLogger(RelationServiceImpl.class);
@Override
public String get(String key) {
...
return ...;
}
}
接着,創建Hive UDF;
public class UdfMapDynamicLoadClassFromHdfsTest extends GenericUDF {
private static final Logger LOGGER =
LoggerFactory.getLogger(UdfMapDynamicLoadClassFromHdfsTest.class);
private static final RelationService RELATION_SERVICE;
static {
try {
UdfClassLoader udfClassLoader =
new UdfClassLoader(
"viewfs://c9/user_ext/weibo_rd_dip/udf/jar",
"udf_map_dynamic_load_class_from_hdfs_test_impl");
RELATION_SERVICE =
(RelationService)
udfClassLoader
.loadClass("com.weibo.dip.hubble.mis.udf.example.dynamic.RelationServiceImpl")
.newInstance();
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public ObjectInspector initialize(ObjectInspector[] arguments) throws UDFArgumentException {
......
}
@Override
public Object evaluate(DeferredObject[] arguments) throws HiveException {
String key = (String) converter.convert(arguments[0].get());
RELATION_SERVICE.get(key);
return ...;
}
@Override
public String getDisplayString(String[] children) {
......
}
@Override
public void close() throws IOException {
......
}
}
注意,在UDF實現類的初始化過程中,完成業務接口的聲明,以及業務接口實現類的加載及實例創建。業務接口 RELATION_SERVICE 使用靜態實例的形式,主要是考慮Hive UDF的序列化及線程安全,這裏不深入討論。
private static final RelationService RELATION_SERVICE;
static {
try {
UdfClassLoader udfClassLoader =
new UdfClassLoader(
"viewfs://c9/user_ext/weibo_rd_dip/udf/jar",
"udf_map_dynamic_load_class_from_hdfs_test_impl.jar");
RELATION_SERVICE =
(RelationService)
udfClassLoader
.loadClass("com.weibo.dip.hubble.mis.udf.example.dynamic.RelationServiceImpl")
.newInstance();
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
其中,“viewfs://c9/user_ext/weibo_rd_dip/udf/jar”表示HDFS存儲Jar文件的目錄,“udf_map_dynamic_load_class_from_hdfs_test_impl.jar”表示業務接口實現類的Jar文件名稱,“com.weibo.dip.hubble.mis.udf.example.dynamic.RelationServiceImpl”表示業務接口實現類的名稱。UDF的實現時僅需要簡單調度業務接口即可:
RELATION_SERVICE.get(key);
最後,編譯、打包及上傳。
打包時,UDF、業務接口以及類加載器需要打包爲一個Jar文件,業務接口實現類需要打包爲另一個Jar文件。除函數首次創建之外,計算邏輯變更僅需要重新編譯、打包及上傳業務接口實現類即可。
結束語
本文中描述的Hive UDF資源文件、計算邏輯動態更新方案,可以較好地滿足複雜場景下業務變更且不會引發異常的需求,但一定程度上也增加了Hive UDF實現及打包的複雜度,可以根據實際的的需求或應用場景決定是否使用。