该问题属于hadoop低版本的bug,升级到hadoop3.0.0+版本后,自动解决。
问题:
最近CDH集群增加了kerberos认证,发现了JavaWeb应用启动后,超过24小时后,kerberos凭证过期导致查询Hbase失败的问题。
Spark程序连接CDH时,通过principal和keytab配置方式,内部会将凭证到hdfs上,供Executor和Driver使用。当凭证Ticket快要失效时,会通过Keytab重新生成凭证。
spark2-submit \
--master yarn \
--deploy-mode cluster \
--num-executors 3 \
--driver-memory 4g \
--executor-memory 4g \
--executor-cores 2 \
--conf spark.default.parallelism=10 \
--conf spark.shuffle.file.buffe=1024k \
--conf spark.executor.memoryOverhead=4096 \
--class com.hypers.streaming.PanoramaMain \
--conf "spark.executor.extraJavaOptions=-verbose:gc -XX:+PrintGCDetails" \
--principal xxxx \
--keytab xxxx.keytab \
xxxx.jar
但我们自己开发的JavaWeb服务没有该机制,导致Ticket在程序运行时过期失效,最终查询Hbase时提示没有有效凭证,查询失败。
源码调试:
本地调试源码验证后,发现确实是因为Ticket的到期时间每次启动时候都会指定成一个固定的时间,后续也没有进行更新,最终过期导致应用查询Hbase失败。
重新编译了源码,增加了Ticket凭证相关的日志,最终确认了问题就是Ticket没有更新到期有效时间,导致认证过期。打印的日志如下:
checkTGTAndReloginFromKeytab! shouldRenewImmediatelyForTests:false, Time.now():1606492834061, getRefreshTime(tgt):1606526141000, tgt:Ticket (hex) =
0000: 61 82 01 40 30 82 01 3C A0 03 02 01 05 A1 0D 1B a..@0..<........
0010: 0B 45 53 53 45 4E 43 45 2E 43 4F 4D A2 20 30 1E .ESSENCE.COM. 0.
0020: A0 03 02 01 02 A1 17 30 15 1B 06 6B 72 62 74 67 .......0...krbtg
0030: 74 1B 0B 45 53 53 45 4E 43 45 2E 43 4F 4D A3 82 t..ESSENCE.COM..
0040: 01 02 30 81 FF A0 03 02 01 12 A1 03 02 01 01 A2 ..0.............
0050: 81 F2 04 81 EF B4 0C DB 10 F7 5F 24 41 A8 91 70 .........._$A..p
0060: A0 76 13 DE 32 C2 29 82 D3 2D D0 50 34 E3 A9 B1 .v..2.)..-.P4...
0070: 4B B0 CF 55 CF 2E 83 8C CB AE 05 2E 77 C5 14 6B K..U........w..k
0080: 56 B0 63 DD 32 A7 C9 BA DE 51 9B FF 06 C5 01 6F V.c.2....Q.....o
0090: 16 01 AD E2 BB C4 6E C5 B3 BC 35 FE 0D AF 20 49 ......n...5... I
00A0: CF F7 4C 90 C7 7F A0 F5 A1 19 A2 FF 3D FF AB 67 ..L.........=..g
00B0: EF 91 FA C4 ED 59 C1 53 38 7A BD B2 FF FD FC 84 .....Y.S8z......
00C0: E5 16 DE 9F 08 62 49 65 57 86 57 6D 03 A2 96 83 .....bIeW.Wm....
00D0: F9 80 1E E6 F7 E4 4B 4F 9C 00 55 B9 4A A2 DA E2 ......KO..U.J...
00E0: F2 01 86 11 1C B5 B1 01 9A F6 29 75 C2 D1 80 8A ..........)u....
00F0: 90 7F 35 C2 D0 A2 65 C3 9A 8B 9C 00 5E 20 EB 6C ..5...e.....^ .l
0100: CF 1A 04 FC 20 8C 7B 4B 98 0A 0F 08 36 EC 94 7E .... ..K....6...
0110: AF 71 4D A1 E1 DA 95 4A 50 5A A8 1B 39 4A F0 B6 .qM....JPZ..9J..
0120: 99 60 71 C7 E4 05 1A 54 0C 05 50 C5 42 B0 97 08 .`q....T..P.B...
0130: 29 5A 48 7E 8F A7 C9 BD A6 9B 19 E4 A7 2A ED 48 )ZH..........*.H
0140: 8F 5F 1A D2 ._..
Client Principal = @xx.COM
Server Principal = xxxx/xx.COM@xx.COM
Session Key = EncryptionKey: keyType=18 keyBytes (hex dump)=
0000: A2 ED 15 B4 25 87 D1 B8 70 23 96 41 48 4B B6 79 ....%...p#.AHK.y
0010: 96 60 1A 1D EF D7 50 C0 31 C6 F5 63 8A 65 56 9E .`....P.1..c.eV.
Forwardable Ticket true
Forwarded Ticket false
Proxiable Ticket false
Proxy Ticket false
Postdated Ticket false
Renewable Ticket false
Initial Ticket false
Auth Time = Fri Nov 27 14:03:41 CST 2020
Start Time = Fri Nov 27 14:03:41 CST 2020
End Time = Sat Nov 28 14:03:41 CST 2020
Renew Till = null
Client Addresses Null
主要是End Time、Renew Till字段,源码里的解释:
/**
* Returns the start time for this ticket's validity period.
*
* @return the start time for this ticket's validity period
* or null if not set.
*/
public final java.util.Date getStartTime() {
return (startTime == null) ? null : (Date)startTime.clone();
}
/**
* Returns the expiration time for this ticket's validity period.
*
* @return the expiration time for this ticket's validity period.
*/
public final java.util.Date getEndTime() {
return (endTime == null) ? null : (Date) endTime.clone();
}
/**
* Returns the latest expiration time for this ticket, including all
* renewals. This will return a null value for non-renewable tickets.
*
* @return the latest expiration time for this ticket.
*/
public final java.util.Date getRenewTill() {
return (renewTill == null) ? null: (Date)renewTill.clone();
}
解决思路:
最后找到的解决方案是需要我们自己去定期刷新(重新登录)凭证来使凭证永久生效,或者指定一个较长的 ticket_lifetime 凭证有效期,来让应用尽可能推迟到期时间。
参考:HBase Kerberos connection renewal strategy - Stack Overflow
最终解决方案:
在连接Hbase,触发了首次登录Kerberos的时候,启动一个定时任务,每次定时调用checkTGTAndReloginFromKeytab()方法来更新凭证,来达到定时更新的效果。
conf.addResource("hbase-site-test.xml");
conf.addResource("core-site-test.xml");
conf.addResource("hdfs-site-test.xml");
try {
System.setProperty("java.security.krb5.conf", Constants.KRB5_CONF_PATH);
UserGroupInformation.setConfiguration(conf);
UserGroupInformation.loginUserFromKeytab(Constants.KEYTAB_USER, Constants.KEYTAB_PATH);
startCheckKeytabTgtAndReloginJob(); // 定时更新凭证任务
} catch (IOException e) {
logger.error("Login failure", e);
}
/**
* * 定时更新凭证
*/
private static void startCheckKeytabTgtAndReloginJob() {
//10分钟循环 达到距离到期时间一定范围就会更新凭证
ThreadPool.updateConfigThread.scheduleWithFixedDelay(() -> {
try {
UserGroupInformation.getLoginUser().checkTGTAndReloginFromKeytab();
logger.warn("get tgt:{}", UserGroupInformation.getLoginUser().getTGT());
logger.warn("Check Kerberos Tgt And Relogin From Keytab Finish.");
} catch (IOException e) {
logger.error("Check Kerberos Tgt And Relogin From Keytab Error", e);
}
}, 0, 10, TimeUnit.MINUTES);
logger.warn("Start Check Keytab TGT And Relogin Job Success.");
}
注:也可通过reloginFromTicketCache()方法进行更新,但依赖于服务器上的认证缓存。或者直接调用kinit -R来更新(参考Hadoop源码中的续期方式)。
// 参考Hadoop源码中的续期方式
try {
String cmd = "kinit";
Shell.execCommand(cmd, "-R");
log.warn("exec kinit -R!");
} catch (IOException e) {
log.warn("relogin error.",e);
}
try {
UserGroupInformation.getLoginUser().reloginFromTicketCache();
} catch (IOException e) {
log.warn("relogin error.",e);
}
观察结论:
Ticket的有效期已经更新,目前运行了几天,一起正常。后续继续观察。