Hive UDF使用資源文件及動態更新方案

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;

 
假設業務場景:自定義函數的計算邏輯過程中,需要使用 數據,且 數據 存在需要動態(定時或臨時)更新的情況。

  1. 數據量較小

    數據量較小,且需要動態更新的場景下,可以將數據存儲至類似於MySQL的數據引擎中,數據需要更新時,直接更新MySQL中的數據即可;函數初始化時,可以將MySQL中的數據一次性加載到內存中,用於函數後續計算邏輯使用。

    複雜一點的情況,可以考慮給MySQL中的每一條數據附加一個時間戳,以每次數據的更新時間爲準;也可以寫入新的數據時,使用一個較舊的時間戳,數據寫入完成之後,統一調整爲新的時間戳;函數初始化時,加載具有最新時間戳的數據。

    注: 爲什麼需要一次性完成數據加載?Hive UDF運行於MapReduce或Spark分佈式計算應用中,如果UDF處理每一行數據均需要訪問MySQL,海量數據場景下,將會導致同一時刻有大量的併發連接,很容易壓垮MySQL。

  2. 數據量較大

    數據量較大,且需要動態更新的場景下,不適用於將數據存儲至類似於MySQL的數據引擎,有以下2方面的考慮:

    a. 數據加載時間較長;
    b. 數據使用內存空間較大;

    這種情況下,建議使用 資源文件,即預先將數據寫入文件(文本文件或索引文件),將文件存儲至HDFS;函數初始化時,可以直接讀取HDFS中的文件,也可以將HDFS中的文件下載至本地後再讀取。

    注: 索引文件可以應用於數據使用內存空間較大的場景下,如:將數據生成Lucene索引文件並上傳至HDFS,函數使用時將索引文件下載至本地,基於磁盤即可快速檢索數據,無需裝入內存。

    資源文件更新策略參見下文。

資源文件動態更新

HDFS中的文件僅支持追加(Append)操作,不支持更新(Update)操作,因此無法通過直接修改HDFS中已有的資源文件內容完成更新。假設HDFS中資源文件的存儲目錄爲“/tmp/resource/”,資源文件名稱爲“resource.data”,以天爲粒度進行更新,方案如下:

  1. 使用當天的最新數據,創建資源文件,名稱:resource.data;
  2. 將資源文件resource.data上傳至HDFS,名稱:/tmp/resource/resource.data;
  3. 將已上傳至HDFS中的資源文件 /tmp/resource/resource.data 重命名爲 /tmp/resource/resource.data.yyyy_MM_dd;其中,yyyy_MM_dd 爲當天日期;
  4. 函數初始化時,根據資源文件的日期後綴,讀取或下載最新的資源文件;

資源文件存儲目錄示例如下:

/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種方式之一進行更新:

  1. 刪除函數,刪除HDFS Jar文件,重新上傳新的Jar文件至HDFS,創建函數;
  2. 刪除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;

directoryfile 用於定義業務接口實現類所屬的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實現及打包的複雜度,可以根據實際的的需求或應用場景決定是否使用。

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