HiveServer2 架構(gòu)源碼詳解

https://blog.csdn.net/u013332124/article/details/94182021

以下內(nèi)容都基于hive-2.3.3版本。

一、HiveServer2的啟動

通常我們是通過調(diào)用${hive_home}/bin下的start-hiveserver2.sh腳本來啟動HiveServer2的。start-hiveserver2.sh腳本實際是調(diào)用${hive_home}/bin/hive腳本:

#! /bin/bash
export HIVE_HOME=/www/hive
${HIVE_HOME}/bin/hive --service hiveserver2 >> ${HIVE_HOME}/logs/hiveserver2.out 2>&1 &

之后hive腳本中會根據(jù)傳入的--service的參數(shù)去調(diào)用${hive_home}/bin/ext/hiveserver2.sh腳本,最終在hiveserver2.sh腳本中才調(diào)用了HiveServer2類的main方法。

//HiveServer2.java
public static void main(String[] args) {
    HiveConf.setLoadHiveServer2Config(true);
    try {
      ServerOptionsProcessor oproc = new ServerOptionsProcessor("hiveserver2");
        //解析傳入的參數(shù)
      ServerOptionsProcessorResponse oprocResponse = oproc.parse(args);

      String initLog4jMessage = LogUtils.initHiveLog4j();
      LOG.debug(initLog4jMessage);
      HiveStringUtils.startupShutdownMessage(HiveServer2.class, args, LOG);

      LOG.debug(oproc.getDebugMessage().toString());
    //調(diào)用對應Executor的execute方法
      oprocResponse.getServerOptionsExecutor().execute();
    } catch (LogInitializationException e) {
      LOG.error("Error initializing log: " + e.getMessage(), e);
      System.exit(-1);
    }
  }

HiveServer2的main函數(shù)執(zhí)行時會先解析傳入的參數(shù),然后返回一個response,里面封裝了一個ServerOptionsExecutor。之后調(diào)用ServerOptionsExecutor#execute方法即可執(zhí)行對應的邏輯。

ServerOptionsExecutor只是一個接口,它的實現(xiàn)主要有:

在這里插入圖片描述

比如如果傳入的ServiceName是hiveserver2,則會執(zhí)行StartOptionExecutor的execute方法,這個方法中調(diào)用了HiveServer2的startHiveServer2方法。

這時,HiveServer才開始啟動它的各個組件并提供服務。

二、HiveServer2的各個服務組件

Hive定義了一個接口Service,很多服務組件都實現(xiàn)了這個接口。這個接口定義了一個組件基本的生命周期,以及組件的各種狀態(tài):

public interface Service {
  public enum STATE {
    NOTINITED,
    INITED,
    STARTED,
    STOPPED
  }
  void init(HiveConf conf);
  void start();
  void stop();
  void register(ServiceStateChangeListener listener);
  void unregister(ServiceStateChangeListener listener);
  String getName();
  HiveConf getHiveConf();
  STATE getServiceState();
  long getStartTime();
}

Service接口的具體實現(xiàn)有:

在這里插入圖片描述

可以看到,包括HiveServer2其實也算Hive的一個服務組件。HiveServer2繼承了CompositeService類,這里用到了設計模式中的組合模式。這樣,HiveServer2可以同時持有多個服務組件。在HiveServer2啟動時,會相應啟動它的那些服務組件。

HiveServer2的服務組件主要有如下這些:

在這里插入圖片描述

1、ThriftCLIService

一個Server的服務組件,它會監(jiān)聽指定的端口來對外提供服務,主要基于Thrift實現(xiàn)的rpc服務。HiveServer2在啟動ThriftCLIService時,會將CLIService的實例也傳給它。這樣,ThriftCLIService收到請求后,就可以委托給CLIService處理了。

另外,ThriftCLIService啟動時,根據(jù)hive.server2.transport.mode參數(shù)的值來決定是啟動ThriftHttpCLIService還是ThriftBinaryCLIService。默認是ThriftBinaryCLIService:

    if (isHTTPTransportMode(hiveConf)) {
      thriftCLIService = new ThriftHttpCLIService(cliService, oomHook);
    } else {
      thriftCLIService = new ThriftBinaryCLIService(cliService, oomHook);
    }

2、CLIService

CLIService主要封裝了處理命令的邏輯,一條命令發(fā)到HiveServer2后,ThriftCLIService會委托給CLIService來處理。不同的命令會調(diào)用不同的CLIService方法。比如執(zhí)行Sql就是調(diào)用CliService#executeStatementAsync()方法。

3、SessionManager

CLIService在啟動時,會初始化一個SessionManager,用來管理會話。

當要建立一個會話時,會調(diào)用SessionManager#createSession()來獲取到唯一的SessionHandle返回給客戶端(session的唯一性是通過SessionHandle來標識的)。之后客戶端發(fā)送命令時將對應的SessionHandle帶上,SessionManager就可以根據(jù)這個SessionHandle獲取到具體的HiveSession對象。

拿到對應的HiveSession對象后,CLIService把具體的操作繼續(xù)委托給HiveSession的方法執(zhí)行。

4、OperationManager

SessionManager在啟動時,會初始化一個OperationManager,主要用來生成Operation(每一條命令都可以理解為是一個Operation)。

SessionManager在創(chuàng)建新的HiveSession時,會將OperationManager的實例也傳給HiveSession。后面HiveSession根據(jù)命令執(zhí)行對應的操作時會通過OperationManager獲取到對應的Operation,之后調(diào)用Operation#run()執(zhí)行具體的邏輯。

也就是說,客戶端發(fā)來的命令最終生成一個Operation對象然后執(zhí)行。不同的命令會對應不同的Operation的實現(xiàn):

在這里插入圖片描述

很明顯,執(zhí)行Sql最終會調(diào)用SQLOperation#run()方法。

三、一個命令的具體處理過程

1、一個命令的處理流程

在這里插入圖片描述
  1. ThriftCLIService服務啟動后,會監(jiān)聽指定的端口,之后有請求進來,就會獲取對應的Processor處理(可以參考thrift的架構(gòu)設計)
  2. 如果沒有配置kerberos,最終的Processor會是TSetIpAddressProcessor。TSetIpAddressProcessor繼承自TCLIService.Processor,TCLIService.Processor中定義了具體的命令要執(zhí)行哪些ProcessFunction
  3. 通過具體的命令找到對應的ProcessFunction后,就執(zhí)行ProcessFunction的getResult()方法。之后,各個ProcessFunction實現(xiàn)類的getResult()方法最終又會調(diào)用ThriftCLIService的相關(guān)方法
  4. ThriftCLIService從請求中獲取到SessionHandle后,就委托給CLIService的相關(guān)方法來處理
  5. CLIService拿著SessionHandle從SessionManager獲取到對應的HiveSession,之后繼續(xù)把命令委托給HiveSession處理
  6. HiveSession根據(jù)具體的命令從OperationManager中獲取到對應的Operation,這個Operation就是真正的操作對象。
  7. 最終調(diào)用Operation#run()方法獲取到一個OperationHandle,后面客戶端還可以通過這個OperationHandle標識來獲取到此次操作的一些狀態(tài)信息。如果是要執(zhí)行Sql,就會走到SQLOperation#run()方法,之后就進入Driver.run()方法,然后開始編譯執(zhí)行sql了

2、關(guān)于SessionHandle和OperationHandle

在hive中,一個session表示一個會話。我們可以理解為一個beeline控制臺就是一個session,在我們通過!conn命令連接到集群后,HiveServer2就會創(chuàng)建一個HiveSession,然后交給SessionManager管理,之后返回一個SessionHandle給客戶端,這個SessionHandle就是此次session的唯一標識了。后面客戶端發(fā)送命令的時候都需要帶上這個SessionHandle,這樣HiveServer2才可以辨認出是哪個session發(fā)來的請求。

在一個session中發(fā)起一個請求,HiveServer2收到請求進行處理后,會返回一個OperationHandle來作為此次操作的唯一標識。后面客戶端可以通過這個OperationHandle標識來獲取此次操作的具體信息(發(fā)請求時帶上這個OperationHandle信息),比如獲取操作的執(zhí)行狀態(tài)、日志等信息。

這也就是說,一個session其實可以發(fā)起多個操作,只要維護好返回的OperationHandle,我們可以并行查詢這些操作的相關(guān)狀態(tài)。在beeline的控制臺中,我們發(fā)送完一個命令后,會阻塞在那里,給我們的感覺好像一個session只能同時處理一個命令。

另外,在阻塞的過程中,beeline客戶端其實也不斷的再想HiveServer2發(fā)送請求獲取日志并輸出。

beeline客戶端是如何獲取日志輸出的

只有當hive.server2.logging.operation.enabled設置為true,才會在Beeline的控制臺輸出HiveServer2那邊的相關(guān)操作日志。另外,通過hive.server2.logging.operation.level還可以調(diào)整輸出級別。

如果開啟了operation的log日志功能,Driver組件在輸出日志時就會往另外一個臨時的日志文件也輸出一份。這個臨時的日志文件是一個Operation一份。后面客戶端根據(jù)OperationHandle發(fā)送fetchResult(fetchType=1)請求來獲取對應的日志信息。這時HiveServer2只要直接去Operation對應的臨時日志文件中拉取數(shù)據(jù)即可。

Operation臨時日志文件的存放路徑和hive.server2.logging.operation.log.location參數(shù)有關(guān)。

四、HiveServer2中的那些重要線程

1、rpc請求處理線程——HiveServer2-Handler-Pool

這個線程池主要是HiveServer2用來處理rpc請求的線程。線程池的coreSize和maxSize和參數(shù)hive.server2.thrift.min.worker.threadshive.server2.thrift.max.worker.threads相關(guān),默認值分別是5和500。

這個線程和netty的worker線程類似,有客戶端發(fā)送請求給ThriftServer,最終都會由這個線程來處理。

2、sql異步執(zhí)行線程 —— HiveServer2-Background-Pool

在SQLOperation#runInternal()方法中,如果請求要求異步操作,就會向BackGroundPool線程池提交一個異步任務,用來處理sql(也就是調(diào)用Driver#run()的邏輯),提交任務到線程池后會立馬返回一個OperationHandle,后續(xù)客戶端可以根據(jù)這個唯一標識實時的查詢?nèi)蝿者\行日志。

當然,如果請求沒要求異步操作,Driver#run()的操作將在HiveServer2-Handler-Pool的線程中執(zhí)行,這時會一直阻塞到任務執(zhí)行完才返回,也就是說,客戶端要等任務執(zhí)行完才能看到全部運行日志。

BackGroundPool線程池的coreSize和maxSize都由參數(shù)hive.server2.async.exec.threads來決定,默認值是100。

是否要異步執(zhí)行Driver#run()由thrift請求體TExecuteStatementReq中的參數(shù)runAsync來決定。目前看hive-jdbc的代碼,這個參數(shù)默認都是true。

另外,目前只有SQLOperation才支持異步執(zhí)行,其他的Operation都不支持

3、執(zhí)行task的線程

Driver在編譯完sql后,會生成物理執(zhí)行計劃,這個物理執(zhí)行計劃中包含了一系列的task。Driver執(zhí)行task的方式是將task放到一個線程中執(zhí)行。這個線程沒有特別指定名稱,通過日志我們看到的是[Thread-id],其中id會不斷自增。

因為task是在線程中執(zhí)行的,因此一個Operation是允許多個task并行的。單個Operation的并行度由配置hive.exec.parallel.thread.number來決定,默認值是8。

4、總結(jié)

在hive的日志中,各種任務的日志會交替出現(xiàn),日志又雜又多,因此理解好上面三種線程可以方便我們排查問題,快速定位任務相關(guān)的線程和日志。

在這里插入圖片描述

五、Beeline和HiveCli的區(qū)別

在hive中,有兩種方式可以執(zhí)行hiveQL。分別是beeline和hiveCli。

1、Beeline

beeline客戶端執(zhí)行的主類是Beeline.java。

Beeline需要連接上HiveServer2后才可以執(zhí)行命令,之后通過jdbc協(xié)議往hiveServer2發(fā)送相關(guān)請求來執(zhí)行用戶的命令。

2、HiveCli

舊版的HiveCli執(zhí)行的主類是CliDriver.java,目前新版的主類是HiveCli.java(底層還是調(diào)用Beeline.java類)。

CliDriver.java的執(zhí)行流程詳解可以看下面這篇文章:

https://segmentfault.com/a/1190000002766035 —— 主要就是創(chuàng)建一個進程,在當前進程中執(zhí)行hive的相關(guān)命令,比如執(zhí)行Sql就初始化一個Driver,然后執(zhí)行run方法。

在早期的版本中,第一代HiveServer對應的客戶端實現(xiàn)就是CliDriver,后來出現(xiàn)了HiveServer2,完全取代了第一代。HiveServer2對應的客戶端是Beeline,但是由于早期使用CliDriver的用戶太多了,因此CliDriver版本一直沒有被下掉。

后面社區(qū)又開發(fā)了新版的HiveCli,其底層其實也是調(diào)用了Beeline,這樣社區(qū)只需要維護一份客戶端的代碼即可:

  public static void main(String[] args) throws IOException {
    int status = new HiveCli().runWithArgs(args, null);
    System.exit(status);
  }

  public int runWithArgs(String[] cmd, InputStream inputStream) throws IOException {
    beeLine = new BeeLine(false);
    try {
      return beeLine.begin(cmd, inputStream);
    } finally {
      beeLine.close();
    }
  }

Beeline類有個屬性isBeeline就表示這個Beeline實例是算真正的beeline客戶端還是HiveCli客戶端。

當isBeeline屬性為false時,beeline客戶端連接的是一個內(nèi)嵌的HiveServer2服務,和HiveCli在同一個進程內(nèi)。其他的邏輯都和Beeline相同。

3、總結(jié)

  • beeline需要連接遠程的HiveServer2來交互
  • HiveCli直接在本進程執(zhí)行命令,不用進行遠程通信

在2.3.3版本中,HiveCli默認還是使用CliDriver的實現(xiàn)方式??梢酝ㄟ^在hive-env.sh中設置USE_DEPRECATED_CLI=false來將實現(xiàn)變成HiveCli.java的形式。因為HiveCli.java會啟動內(nèi)置的HiveServer2,執(zhí)行的邏輯基本和HiveServer2一樣,所以在做一些測試時會更容易發(fā)現(xiàn)HiveServer2的一些問題。

官網(wǎng)關(guān)于Beeline和HiveCli的介紹:

https://cwiki.apache.org/confluence/display/Hive/Replacing+the+Implementation+of+Hive+CLI+Using+Beeline

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容