【大數(shù)據(jù)】Kerberos 認(rèn)證問題導(dǎo)致服務(wù) OOM

三年前來到公司大數(shù)據(jù)團(tuán)隊(duì),算是入了大數(shù)據(jù)的坑。一開始對大數(shù)據(jù)的組件不是很了解,一路走來在不停地學(xué)習(xí)探索。上周遇到了一個(gè)問題,我們數(shù)據(jù)地圖的服務(wù)在預(yù)發(fā)環(huán)境觸發(fā) POD 級(jí)別的 OOM ,新啟了個(gè) pod;由于上了個(gè)大需求到預(yù)發(fā)環(huán)境所以比平常更關(guān)注了一下,
簡單排查發(fā)現(xiàn):

  • POD 內(nèi)存飚到了 93%
  • 線程數(shù)飆到了近 700

大家可以思考下為啥我沒去看 Java 堆內(nèi)存?結(jié)尾給答案。
正常情況下線程數(shù)最多 250 (web 線程以及一些雜七雜八的線程) 左右,飆到近 700 肯定是有問題的,由于預(yù)發(fā)環(huán)境已經(jīng)重啟,當(dāng)時(shí)立馬想的線上會(huì)不會(huì)也出現(xiàn)同樣的問題,抱著僥幸的心態(tài)去看了下巧了也飆起來直逼 800 。直接登錄到 POD 容器上用 jstack 命令把線程棧導(dǎo)出到文件里,元兇立馬浮出水面:

部分異常線程數(shù)

熟悉大數(shù)據(jù)的同學(xué)可能已經(jīng)看出來了這就是 Kerberos 續(xù)期 TGT(票據(jù)授予票據(jù)) 的線程,可是為什么會(huì)這么多呢?我打上馬賽克的位置是用戶名而且都是同一個(gè),查了資料發(fā)現(xiàn)是在調(diào)用 UserGroupInformation.loginUserFromKeytab 時(shí),在開啟自動(dòng)續(xù)期 TGT 的情況就會(huì)創(chuàng)建一個(gè) daemon 線程去續(xù)期,代碼如下:

@InterfaceAudience.Public
  @InterfaceStability.Evolving
  public
  static void loginUserFromKeytab(String user,
                                  String path
                                  ) throws IOException {
    if (!isSecurityEnabled())
      return;

    UserGroupInformation u = loginUserFromKeytabAndReturnUGI(user, path);
    if (isKerberosKeyTabLoginRenewalEnabled()) {
      u.spawnAutoRenewalThreadForKeytab();
    }

    setLoginUser(u);

    LOG.info(
        "Login successful for user {} using keytab file {}. Keytab auto"
            + " renewal enabled : {}",
        user, new File(path).getName(), isKerberosKeyTabLoginRenewalEnabled());
  }

private void spawnAutoRenewalThreadForKeytab() {
    if (!shouldRelogin() || isFromTicket()) {
      return;
    }

    // spawn thread only if we have kerb credentials
    KerberosTicket tgt = getTGT();
    if (tgt == null) {
      return;
    }
    long nextRefresh = getRefreshTime(tgt);
    executeAutoRenewalTask(getUserName(),
            new KeytabRenewalRunnable(tgt, nextRefresh));
  }

  /**
   * Spawn a thread to do periodic renewals of kerberos credentials from a
   * keytab file. NEVER directly call this method.
   *
   * @param userName Name of the user for which login needs to be renewed.
   * @param task  The reference of the login renewal task.
   */
  private void executeAutoRenewalTask(final String userName,
                                      AutoRenewalForUserCredsRunnable task) {
    kerberosLoginRenewalExecutor = Optional.of(
            Executors.newSingleThreadExecutor(
                  new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                      Thread t = new Thread(r);
                      t.setDaemon(true);
                      t.setName("TGT Renewer for " + userName);
                      return t;
                    }
                  }
            ));
    kerberosLoginRenewalExecutor.get().submit(task);
  }

因?yàn)樯厦娉霈F(xiàn)了很多個(gè)對同一個(gè)賬號(hào)續(xù)期的線程,也就是程序里可能有個(gè)地方會(huì)多次對同一個(gè)賬號(hào)進(jìn)行登錄,查詢代碼還真發(fā)現(xiàn)了一處:

private <R> R doActionWithRetry(Action<R> action) throws TException {
        try {
            synchronized (client) {
                return action.call();
            }
        } catch (TException e) {
            try {
                synchronized (client) {
                    kerberosClient.login();
                    client.reconnect();
                }
            } catch (MetaException | IOException unHandleException) {
                throw e;
            }
            synchronized (client) {
                return action.call();
            }
        }
    }

這是內(nèi)部封裝的一個(gè)支持重試的方法,在 catch 住 TException 時(shí)會(huì)調(diào)用 kerberosClient.login() ,而 kerberosClient#login 的方法里就是調(diào)用 UserGroupInformation.loginUserFromKeytab , 這里暫時(shí)不討論為啥會(huì)拋出 TException ,因?yàn)榍闆r是多種多樣的,直接解決可以用下面這個(gè)方法代替:

private void reLoginExpiringKeytabUser() throws MetaException {
        if (!UserGroupInformation.isSecurityEnabled()) {
            return;
        }
        try {
         
            //獲取當(dāng)前UGI 實(shí)例(例如:賬號(hào):data.map)
            UserGroupInformation ugi = UserGroupInformation.getLoginUser();
            if (ugi.isFromKeytab()) {
                //這方法會(huì)判斷當(dāng)前的 TGT 是否過期,如果過期會(huì)重新登錄,不會(huì)啟動(dòng)一個(gè)新的線程
                ugi.checkTGTAndReloginFromKeytab();
            }
        } catch (IOException e) {
            String msg = "Error doing relogin using keytab " + e.getMessage();
            throw new MetaException(msg);
        }
    }

現(xiàn)在還有一個(gè)問題就是已經(jīng)配置了自動(dòng)續(xù)期為啥還會(huì)拋這個(gè)錯(cuò)呢?下期再說

結(jié)語

我們來回答下開篇的問題:為啥沒去看 Java 堆內(nèi)存? 因?yàn)檫@是 pod 級(jí)別的 OOM ,而我們的數(shù)據(jù)地圖服務(wù)在啟動(dòng)時(shí)就給了最大內(nèi)存,不會(huì)變的,只能是其他部分占用內(nèi)存不正常才導(dǎo)致 POD OOM 的,這算是一個(gè)小小的思維點(diǎn),祝大家生活愉快。

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

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