Kylin2.0-Hbase0.98重啓問題

背景:目前當HBase添加、刪除節點、重啓、移動rgroup或者hbase table移動region server等操作後,均需要Kylin的所有節點重啓,理論上這些操作可以對上層應用透明或者只有短暫的不可用後自行恢復。但是目前咱們有8套Kylin集羣均需要在HBase變動後進行Kylin重啓纔可以繼續提供相應的服務,對上層服務影響較大,HBase的變更也會變得複雜。但是咱們提供的Kylin 3.0版本卻沒有此問題,Kylin 3.0版本對應的HBase版本是1.4.8版本,有問題的八套Kylin集羣的版本爲2.0,HBase版本爲0.98。本文就如何排查和解決此問題進行總結。

注:大家可以跳過前面第一和第二步直接從第三步(排查Hbase0.98和1.4.8代碼區別)開始閱讀,第三步爲引發此問題的代碼所在。

一、定位問題

首先分別在Kylin3.0和2.0集羣上找到一個cube對應的hbase table表,然後移動對應table的rgroup即換存儲的機器。發現Kylin 3.0仍然可以查詢,但是2.0無法在獲取到結果,Kylin 2.0拋出的異常如下:

2019-07-18 16:43:59,148 ERROR [http-bio-8088-exec-438] controller.BasicController:54 :
org.apache.kylin.rest.exception.InternalErrorException: Error while executing SQL "select * from DPS_DATA_CENTER.SYS_PROBE limit 2": java.net.SocketTimeoutException: callTimeout=1200000, callDuration=1200108: row '' on table 'DPS_DATA_CENTER:KYLIN_N59AHNZIMB' at region=DPS_DATA_CENTER:KYLIN_N59AHNZIMB,,1542085088058.6b1f069c03aa1cfc6649b6762bc79451., hostname=bigdata-dnn-hbase33.gs.com,60020,1551705541907, seqNum=3
        at org.apache.kylin.rest.service.QueryService.doQueryWithCache(QueryService.java:402)
        at org.apache.kylin.rest.controller.QueryController.query(QueryController.java:71)
        at sun.reflect.GeneratedMethodAccessor201.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
        at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136)
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:743)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:672)
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:82)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:933)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:867)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:951)
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:853)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:650)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:827)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)

通過錯誤可以看到查詢此表的某個region時還是去之前的機器(bigdata-dnn-hbase33.gs.com)查詢,理論上這個查詢是會自動失敗重試到新機器查詢的,但是Kylin的這個錯誤顯然還是到了之前的節點查詢。根據之前的經驗和其他HBase的用戶並無此問題,大致猜想可能是沒有配置HBase失敗重試(實際後來排查配置了此選項且配置的35次)或Kylin調用代碼有問題,所以有了下面2.0代碼和3.0代碼的對比。

二、排查Kylin 2.0和3.0代碼區別

首先定位查詢服務的Kylin代碼邏輯(主要是Kylin通過rpc調用對應的hbase table所在節點的coprocessor進行查詢),分別對比Kylin2.0和Kylin3.0的代碼,發現雖然兩邊的代碼在寫法上有些許不同,但是與HBase通信的最底層的地方並無差別。分別進行了如下嘗試:

  • Debug 3.0的服務,意在確定Kylin本身是否會獲取region的location或者進行相應的失敗重試(實際沒有,均是Hbase自身的失敗重試);

  • 修改3.0的的失敗重試機制到0(修改爲0後,無論是否移動group均無法正確獲取到數據,拋出異常失敗重試大於0次,說明配置的HBase失敗重試的參數是生效的);

  • 修改3.0的失敗重試機制到1(修改爲1後,移動前可以正確查詢並獲得結果,移動後無法查詢獲取結果並拋出異常失敗重試大於1次);

  • 修改3.0的失敗重試機制大於1(移動前後均可以正常查詢並獲得結果。)

經過上面4種嘗試和Debug首先說明HBase失敗重試配置是生效的,且移動rgroup後會導致第一次查詢失敗,需要進行二次嘗試,且本身的嘗試代碼是HBase實現,基本排除是Kylin代碼的問題,由於2.0是從其他團隊接管過來的集羣生產任務居多且暫無測試環境,只能通過硬看代碼的方式排查,比較費時。

三、排查Hbase0.98和1.4.8代碼區別(真正問題原因)

之前所有的猜想(配置或Kylin兩個版本代碼調用區別導致)基本排除掉,只能重新來看2.0查詢拋出的異常。很奇怪 2.0拋出的異常爲java.net.SocketTimeoutException異常,但是3.0失敗重試機制到達後拋出的是”Call exception, tries=“異常,決定好好看下HBase retry的代碼。果然在HBASE的 RpcRetryingCaller 類發現了java.net.SocketTimeoutException拋出的原因。HBase在retry的時候有兩個邏輯判斷是否繼續重試:

  • (1)、重試次數如果達到配置的次數則不再重試,並拋出”Call exception, tries=“等關鍵字的異常代碼(3.0中的嘗試也說明了這點);
  • (2)、重試過程中整個時長如果大於了配置的超時時長也會不再重試,並拋出”java.net.SocketTimeoutException“等關鍵字的異常代碼(2.0中正是這個異常)。

Hbase有問題的代碼也正是這個超時異常的代碼。接下來咱們看下這塊兒失敗重試的具體代碼:

/**
 * Retries if invocation fails.
 * @param callTimeout Timeout for this call
 * @param callable The {@link RetryingCallable} to run.
 * @return an object of type T
 * @throws IOException if a remote or network exception occurs
 * @throws RuntimeException other unspecified error
 */
@edu.umd.cs.findbugs.annotations.SuppressWarnings
    (value = "SWL_SLEEP_WITH_LOCK_HELD", justification = "na")
public synchronized T callWithRetries(RetryingCallable<T> callable, int callTimeout)
throws IOException, RuntimeException {
  this.callTimeout = callTimeout;
  List<RetriesExhaustedException.ThrowableWithExtraContext> exceptions =
    new ArrayList<RetriesExhaustedException.ThrowableWithExtraContext>();
  this.globalStartTime = EnvironmentEdgeManager.currentTimeMillis();
  for (int tries = 0;; tries++) {
    long expectedSleep = 0;
    try {
      beforeCall();
      callable.prepare(tries != 0); // if called with false, check table status on ZK
      return callable.call();
    } catch (Throwable t) {
      if (tries > startLogErrorsCnt) {
        LOG.info("Call exception, tries=" + tries + ", retries=" + retries + ", retryTime=" +
            (EnvironmentEdgeManager.currentTimeMillis() - this.globalStartTime) + "ms, msg="
            + callable.getExceptionMessageAdditionalDetail());
      }
      // translateException throws exception when should not retry: i.e. when request is bad.
      t = translateException(t);
      callable.throwable(t, retries != 1);
      RetriesExhaustedException.ThrowableWithExtraContext qt =
          new RetriesExhaustedException.ThrowableWithExtraContext(t,
              EnvironmentEdgeManager.currentTimeMillis(), toString());
      exceptions.add(qt);
      ExceptionUtil.rethrowIfInterrupt(t);
      if (tries >= retries - 1) {
        throw new RetriesExhaustedException(tries, exceptions);
      }
      // If the server is dead, we need to wait a little before retrying, to give
      //  a chance to the regions to be
      // get right pause time, start by RETRY_BACKOFF[0] * pause
      expectedSleep = callable.sleep(pause, tries);
 
      // If, after the planned sleep, there won't be enough time left, we stop now.
      long duration = singleCallDuration(expectedSleep);//有問題的代碼,實現邏輯可以看singleCallDuration方法的源碼
      if (duration > this.callTimeout) {//由於singleCallDuration實現的問題+用戶的配置,duration永遠都會大於callTimeout,所以無法真正進行失敗重試
        String msg = "callTimeout=" + this.callTimeout + ", callDuration=" + duration +
            ": " + callable.getExceptionMessageAdditionalDetail();
        throw (SocketTimeoutException)(new SocketTimeoutException(msg).initCause(t));
      }
    } finally {
      afterCall();
    }
    try {
      Thread.sleep(expectedSleep);
    } catch (InterruptedException e) {
      throw new InterruptedIOException("Interrupted after " + tries + " tries  on " + retries);
    }
  }
}

singleCallDuration方法源碼:

/**
 * @param expectedSleep
 * @return Calculate how long a single call took
 */
private long singleCallDuration(final long expectedSleep) {
/*
*此處代碼原本的意思應該是要獲取一共花費了多少時間,即從開始執行到現在 + 下一次失敗重試需要的時間 ,但是此處他還加了rpcTimeout的時間(這個值用戶
*在hbase-site.xml中可以配置,且正好2.0的8套Kylin集羣對應的HBase客戶端配置了此值爲1200000)。Hbase 3.0此處的代碼是沒有加rpcTimeout的時間的,
*理論也不應該加
*
*/
  int timeout = rpcTimeout > 0 ? rpcTimeout : MIN_RPC_TIMEOUT;
  return (EnvironmentEdgeManager.currentTimeMillis() - this.globalStartTime)
    + timeout + expectedSleep;
}

總結下原因:
Hbase失敗重試的時候判斷是否超時,首先會獲取從執行到下次失敗重試一共要花費的時間(duration),但是這塊兒在計算duration的邏輯有問題(已經花費的時間+ rpctimeout(1200000) + expectedSleep(下次嘗試的時間))),然後在用duration時間與callTimeout(默認1200000,正好與配置的rpctimeout相等)比較,看duration計算公式知道,duration如果配置的時間和callTimeout相等或者大於,則永遠無法進行失敗重試。HBase1.4.8已經修復了此問題duration的計算公式改爲(已經花費的時間 + expectedSleep(下次嘗試的時間)))。

四、解決問題

  1. 第一步:修改Kylin
    集羣所有的HBase客戶端hbase.rpc.timeout配置,將其時間由1200000改爲120000,重啓Kylin集羣,再次實驗問題解決,可以在不重啓Kylin節點的情況查詢查詢;
  2. 第二步:修復HBase此處超時判斷代碼邏輯,徹底解決問題。
  3. 補充一點: 因爲一次call請求可能會包含多次RPC,所以rpctimeout的值設置最好小於callTimeout時間.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章