1. 摘要
近期 HiveServer2 (HS2) 发生大规模 SASL 认证失败,导致集群服务不可用。经深度排查,确认该事故由环境配置隐患与代码变更两个核心因素叠加导致:
- 环境配置隐患(静态因素):Linux 系统工具 (
kinit) 与 JDK 在解析krb5.conf配置时存在行为差异。kinit会根据renew_lifetime智能推断并生成带有 RENEWABLE 标记的票据,而 JDK 严格要求配置文件中显式设置renewable = true。这种不一致导致 JDK 在尝试续期时无法解析续期截止时间,引发 TGT Renewer 守护线程因参数异常崩溃。 - 代码变更触发(动态因素):Paimon Connector 引入了不当的登录逻辑 (
loginUserFromSubject)。该操作不仅触发了上述 JDK Bug,更致命的是篡改了 Hadoop UGI (UserGroupInformation) 的内部状态,将登录上下文从 Keytab-based 强制切换为 Subject-based,导致原本健壮的 Keytab 自动兜底机制失效。
2. 背景知识补充
在深入分析故障之前,有必要理解涉及的关键 Kerberos 和 Hadoop 安全概念。
2.1 核心概念
- TGT (Ticket Granting Ticket):票据授予票据。由 KDC 颁发,是用户获取特定服务票据的“通行证”。TGT 有有效期,通常默认为 24 小时。
- TGT Renewal (续期):在不重新进行身份验证(即不重新输入密码或使用 Keytab)的情况下,延长 TGT 的有效期。这对于长时间运行的服务(如 HiveServer2)至关重要。
- UGI (UserGroupInformation):Hadoop 中用于管理用户信息和凭证的类。它维护了当前的登录用户状态,包括登录方式(Keytab 或 Subject)以及相关的 Kerberos 凭证。
- Keytab vs. Subject:
- Keytab:包含加密的 Principal 密钥,允许无人值守地获取或续期 TGT。这是服务端进程的标准登录方式。
- Subject:Java 安全层面的用户身份集合,包含凭证。
- Delegation Token (委托令牌):Hadoop 引入的机制,允许后续作业(如 MapReduce/Spark Task)在不直接访问 KDC 的情况下,以用户身份访问 HDFS 等服务。这解释了为何 HDFS 读写在 HS2 故障期间仍能短暂工作。
2.2 认证流程简述
- HS2 启动时,通过 Keytab 登录 KDC 获取 TGT。
- 后台线程定期尝试续期 TGT。
- 客户端连接 HS2 时,HS2 通过
doAs逻辑模拟用户身份。 - HS2 连接 Hive Metastore (HMS) 时,需要先向 HMS 获取 Delegation Token 或进行 Kerberos 认证。
3. 故障详细时间线与证据链
本次故障经历了潜伏、爆发和雪崩三个阶段。
timeline title HiveServer2 故障时间线 section 阶段一:潜伏期 3点05分 : HS2 最后一次成功向 KDC<br>发起 TGS_REQ 4点43分 : TGT Renewer 线程崩溃<br>(IllegalArgumentException) section 阶段二:爆发期 5点整 : 离线任务早高峰到达<br>大量 OpenSession 请求涌入 5点02分 : 第一个线程获取全局锁<br>尝试连接 HMS 失败 5点05分 : HMS 侧出现海量<br>SASL negotiation failure section 阶段三:雪崩与死锁 5点05分 - 9点30分 : 线程池爆满<br>大量线程 BLOCKED<br>服务完全不可用
3.1 阶段一:潜伏期
在此阶段,HS2 表面看似正常,但内部的 Kerberos 续期机制已经失效。
- T-Days:HS2 持续运行,由于代码或业务逻辑问题,到 HMS 的连接数缓慢泄露至 4.9 万+。
- 12-31 03:05:KDC 日志显示,这是 HS2 最后一次成功向 KDC 发起 TGS_REQ。此后长达 7 小时无续期请求。
- 12-31 04:43:08 (关键节点):HS2 内部 TGT Renewer 线程崩溃。
关键报错日志:
ERROR [TGT Renewer for hive/hs2.venus.sohurdc.com@VENUS.SOHURDC.COM]: security.UserGroupInformation - TGT is destroyed. Aborting renew thread for hive/hs2.venus.sohurdc.com@VENUS.SOHURDC.COM.
org.apache.hadoop.security.KerberosAuthException: Login failure for user: hive/hs2.venus.sohurdc.com@VENUS.SOHURDC.COM
javax.security.auth.login.LoginException: java.lang.IllegalArgumentException: The renewable period end time cannot be null for renewable tickets.
at javax.security.auth.kerberos.KerberosTicket.init(KerberosTicket.java:317)
...
at org.apache.hadoop.security.UserGroupInformation$TicketCacheRenewalRunnable.relogin(UserGroupInformation.java:1066)
at org.apache.hadoop.security.UserGroupInformation$AutoRenewalForUserCredsRunnable.run(UserGroupInformation.java:975)定性:这是故障的“零号时刻”。此时 HS2 进入“无政府状态”,内存中的 TGT 被标记为销毁或不可续期。
3.2 阶段二:爆发期
随着早高峰流量到来,潜在的认证问题迅速转化为服务不可用。
- 12-31 05:00:00:离线任务早高峰到达。大量
OpenSession请求涌入,触发doAs逻辑,需要向 HMS 申请 Delegation Token。 - 12-31 05:02:00:
- 现象:第一个线程抢到了 CLIService 全局锁。
- 动作:尝试连接 HMS。由于 TGT 状态异常(已被标记 Destroyed)或文件句柄不足,触发底层异常。
- HMS 侧:收到海量积压连接的冲击(惊群效应),Thrift Server 线程池爆满,无法处理 SASL 握手。
HMS 侧证据:
ERROR ... Peer indicated failure: GSS initiate failed3.3 阶段三:雪崩与死锁
- 12-31 05:05 - 09:30:
- 死循环:持有锁的线程进入
RetryingMetaStoreClient逻辑 → 报错 → 持有锁休眠 (Thread.sleep) → 重试。 - 死亡接力:一个线程超时/失败退出 → 释放锁 → 下一个线程拿到锁 → 瞬间掉入同样的坑 → 继续持有锁休眠。
- 死循环:持有锁的线程进入
Thread Dump 证据:
3 Blocked by 81 (hiveserver2-web-81-acceptor-3...)
792 Blocked by 3718368 (HiveServer2-Handler-Pool: Thread-3718368)
Stack trace:
org.apache.hive.service.cli.CLIService.getDelegationTokenFromMetaStore(CLIService.java:570)
...
State: BLOCKED
Blocked by 3718368 (HiveServer2-Handler-Pool: Thread-3718368)HDFS 幸存之谜:HDFS 客户端之所以能用,是因为它使用的是长效的 Delegation Token(7天有效期),不需要每次都跟 KDC 交互;而 HMS 连接需要实时的 Kerberos 认证。
4. 根因深度技术分析
4.1 根因一:JDK 与 OS 对 Kerberos 协议理解的偏差
原理描述
Linux 系统的 Kerberos 库(MIT Kerberos)与 Java 的实现 (sun.security.krb5) 在解析 /etc/krb5.conf 时存在逻辑不一致。
- OS 行为:当配置
renew_lifetime = 7d时,kinit智能推断用户需要续期能力,自动为生成的凭证缓存(/tmp/krb5cc_xxx)打上RENEWABLE(R) 标记。 - JDK 行为:Java 实现严格依赖显式配置。如果
[libdefaults]中未显式设置renewable = true,JDK 默认认为不可续期。
异常触发链
- HS2 尝试读取由
kinit生成的缓存文件。 - JDK 读取文件流,解析出 Ticket 包含 R 标记。
- 在构建
KerberosTicket对象时,JDK 根据krb5.conf配置(默认为 false)未计算/填充renewTill(续期截止时间)字段,导致该字段为null。 - Crash Point:
KerberosTicket构造函数执行严格校验,抛出异常。
JDK 8 源码分析
// javax.security.auth.kerberos.KerberosTicket.java:317
// 如果票据有 Renewable 标记 (从文件读入)
if (flags != null && flags[RENEWABLE]) {
// 但 renewTill 时间为空 (因配置未开启)
if (renewTill == null) {
// 抛出致命异常,导致 Renewer 线程退出循环
throw new IllegalArgumentException("The renewable period end time cannot be null for renewable tickets.");
}
this.renewTill = new Date(renewTill.getTime());
}配置对比证据
- krb5.conf:未显式配置
renewable=true,仅配置了renew_lifetime=7d。 - klist 输出:显示 Flags 包含
R(Renewable)。 - Java 获取的配置:缺失
renewable=true项。
4.2 根因二:Paimon 代码修改导致的 UGI 状态篡改
问题代码
Paimon 引入了如下逻辑来处理 Proxy User 登录,这是导致服务瘫痪的直接导火索。
// 修改后的 Paimon KerberosLoginProvider.java
} else if (!isProxyUser(UserGroupInformation.getCurrentUser())) {
// ...
} else {
// 致命修改:对 Proxy User 强行尝试基于 Subject 的登录
UserGroupInformation.loginUserFromSubject(null);
}技术危害分析
-
触发异常: 该调用强制 UGI 重新加载凭证。由于回退机制,UGI 尝试读取
/tmp/krb5cc_xxx缓存文件,直接触发了 4.1 中描述的 JDK Bug,导致 TGT Renewer 线程从TIMED_WAITING(正常休眠)状态转变为TERMINATED(异常退出)。 -
破坏 Keytab 兜底机制(核心死因):
- 正常状态:HS2 启动时通过
loginUserFromKeytab初始化,UGI 内部标记为 Keytab-based。当 TGT 过期且 Renewer 线程死亡时,Hadoop 安全框架会触发 lazy relogin,自动使用 Keytab 文件重新申请票据。 - 篡改后状态:
loginUserFromSubject(null)的调用将当前 UGI 的登录上下文重置为 Subject-based。 - 后果:UGI 丢失了“我是通过 Keytab 登录”的记忆。当 TGT 最终过期时,惰性重登逻辑不再尝试读取 Keytab,而是错误地再次尝试读取缓存或 Subject,最终因凭证无效导致 SASL 认证全面失败。
- 正常状态:HS2 启动时通过
5. 故障逻辑全景图
下图展示了 7月30日(幸存)与 12月31日(宕机)的逻辑分支对比:
flowchart TD Start["TGT Renewer 线程崩溃"] --> CheckUGI{"检查 UGI 登录方式"} subgraph "场景一: Paimon 变更前 (7月) - 幸存" CheckUGI -->|Keytab-based| NormalRelogin["触发 Lazy Relogin"] NormalRelogin --> ReadKeytab["读取 Keytab 文件"] ReadKeytab --> GetNewTGT["成功获取新 TGT"] GetNewTGT --> ServiceStable["服务保持稳定"] end subgraph "场景二: Paimon 变更后 (12月) - 故障" CheckUGI -->|"Subject-based (被篡改)"| FailRelogin["触发 Lazy Relogin"] FailRelogin --> TryReadCache["尝试读取 Subject / 缓存文件"] TryReadCache --> JDKBug["触发 JDK Bug: renewable period end time is null"] JDKBug --> AuthFail["SASL Negotiation Failure"] AuthFail --> ServiceDown["服务完全不可用"] end Start -.->|触发条件| CheckUGI
6. 结论与风险评估
6.1 结论
本次事故是环境配置缺陷与不当代码变更共同作用的结果。
- JDK Bug 导致了 TGT 自动续期线程的死亡(这是长期存在的隐患)。
- Paimon 的代码修改不仅诱发了该 Bug,更关键的是它卸载了 HS2 的安全防御机制,导致系统失去容错能力。
6.2 潜在风险
如果不进行修复,当前集群面临以下风险:
- 定时炸弹:每次 HS2 重启或重新登录后,服务寿命仅等于 TGT 的有效期(24小时)。一旦到期,服务必挂。
- 权限混乱:Paimon 的修改可能导致 Proxy User 身份丢失,引发 HDFS 文件 Owner 错误(变为 hive 用户而非业务用户)。
关联专栏
- Hive:HiveServer2 的架构与认证机制
- Kerberos 安全认证:Kerberos 协议与 TGT 续期机制
- JVM:JDK Bug 与线程调度问题
- HDFS:Proxy User 与 HDFS 文件权限