三年前來到公司大數(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ù)據(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),祝大家生活愉快。