并发 kinit 导致 kerberos GSS initiate failed 定位与解决

线上任务调度工具执行 Hadoop Hive 相关的任务会偶发任务执行失败的问题,日志报错为 Kerberos 认证失败。查看调度工具的运行逻辑:每次执行任务前先 kinit 认证,再执行任务,考虑到最近迁移任务量大,且多为单个租户的任务,怀疑是该租户并发 kinit 导致,经一系列的测试与验证,笔者确认了问题的原因,使用一种更优的方案解决了集群租户并发认证的问题。

1.问题发现

大数据平台 Hadoop 的安全认证是基于 Kerberos 实现的。 Kerberos 是一个网络身份验证协议,用户只需输入身份验证信息,验证通过获取票据即可访问多个接入 Kerberos 的服务。

Kerberos 认证方式有两种:

  1. 使用密码认证:使用用户密码通过 kinit 认证, 获取到的 TGT 存在本地凭证缓存当中, 供后续访问服务认证使用。一般在交互式访问中使用。
  2. 使用 keytab 认证:用户通过导出的 keytab 可以免密码进行用户认证, 后续步骤一致。一般在应用程序中配置使用。

目前大数据平台租户执行任务,主要采用第一种 kinit 的方式实现认证, 为了保证 kerberos 认证不过期,调度系统在每次执行任务时会先 kinit 认证刷新凭证 echo $passwd | kinit

这种方式简单可用,但最近总是频发认证失败的问题。报错如下:
GSS initiate failed: No valid credentials provided Failed to find any kerberos tgt

2.问题定位

每次执行前都会 kinit,怎么会还找不到 kerberos tgt,为了定位于解决问题,我们先来看下 kinit 的解释

kinit 命令用于获取和缓存 principal 的初始票证授予票证(凭证)。此票证用于 Kerberos 系统进行验证。只有拥有 Kerberos 主体的用户才可以使用 Kerberos 系统。有关 Kerberos 主体的信息,请参见 kerberos(5)。
当使用 kinit 而未指定选项时,实用程序将提示您输入 principal 和 Kerberos 口令,并尝试使用本地 Kerberos 服务器验证您的登录。如果需要,可以在命令行上指定 principal。
如果登录尝试通过了 Kerberos 的验证,kinit 将检索您的初始票证授予票证并将其放到票证高速缓存中。缺省情况下,票证存储在 /tmp/krb5cc_uid 文件中,其中 uid 表示用户标识号。票证将在指定的生命周期后过期,之后必须再次运行 kinit。高速缓存中的任何现有内容都将被 kinit 销毁。[1]

考虑到最近单个租户的并发任务量大(大量任务在调度系统配置同一时间,定时调度),怀疑是并发 kinit 导致 - kinit 操作会重新覆盖写 krb 文件 - 即并发场景下有 krb 文件不可读的情况。

为了验证猜想,做了如下测试:

  1. 删除用户的凭证文件: /tmp/krb5cc_uid,执行 hdfs 命令。报同样的认证错误
  2. 编写测试脚本,并发 kinit 后执行 hdfs 任务,此认证报错问题必现

3.问题解决

如何保证租户并发场景下,认证成功且不过期,想到两种解决思路:

  1. 认证失败后重试,重新 kinit,必须封装平台级 API 才行(并发依然可能出错)
  2. 不用每次执行任务都 kinit,改为定期按需进行 kinit 认证更新凭证 , 且保证并发的场景不出错

根据当前场景,采用第二种方案,简单快速,可实现,这种方案的优点如下:

  1. 按需 kinit,过滤绝对多数的重复的 kinit, Kerberos KDC 认证服务器的压力可以降低 90% 以上。
  2. 最小化代码入侵

解决过程如下:
首先通过 klist 查看 认证相关信息,如 ticket 的文件路径和名称,失效时间等

[chenyao_yy@CD/WJ2FD-L350-ZYC1Q-colletion-DELLR730-SV013 ~]$ klist
Ticket cache: FILE:/tmp/krb5cc_30022_3zIUZUTNbD
Default principal: chenyao_yy@KDC

Valid starting     Expires            Service principal
08/14/18 17:02:12  08/16/18 17:02:12  krbtgt/KDC@KDC
    renew until 08/21/18 17:02:12

每次执行任务前,klist 查看票据失效时间,当快要失效时(目前定义 2 hours),才进行 kinit,至此解决了按需 kinit 的问题;

并发的问题借鉴了锁的概念,通过锁互斥执行 kinit。保证一次只有一个进程能 kinit 成功。 经查询 linux 有文件锁的概念,经测试可用 - flock[2]

3.1 Linux flock 文件锁

简单说下使用:
flock -xn /tmp/${user}_kinit_lock -c "echo $passwd | kinit"

  • 参数说明:-x:独占锁 ;-n:未获取到锁立马退出,避免阻塞
  • 获取文件 /tmp/${user}_kinit_lock 独占锁,获取到才能执行 kinit,执行结束释放锁(租户的文件锁通过用户名来区分)。
  • 未获取到文件锁直接退出(跳过 -c 后的命令)

这里需要特殊说明是:未获取到锁或者 -c 命令执行失败都会返回 1($?=1)

3.2 kinit 脚本实现

具体实现脚本如下:

#!/bin/bash
# kinit helper - 解决单用户并发 kinit 报错问题
# 只有当 expiredTime 剩余时间小于 2 小时才进行 kinit 操作,且只允许一次 kinit
source /etc/profile

# 输入参数 $1=租户 $2=租户密码
proxyUser=$1
passwd=$2
# 是否需要 kinit (默认需要=0)
needKinit=0
# 过期剩余时间阈值 2 小时
let maxLeftTime=2*60*60

# 检查是否需要重新 kinit,存在如下两种情况:
# 1.krb 文件不存在
# 2.klist 过期时间,还剩不到 2 小时
check_if_need_kinit(){
  klist > /dev/null
  if [ $? -eq 0 ]; then

    # check principal is match
    principal=$(klist | awk '{print $3}' | sed -n '2p' | cut -d@ -f1)
    echo "default principal: $principal"
    if [ "$principal" != "${proxyUser}" ]; then
       echo "expect principal: $proxyUser, will re-kinit"
       return
    fi

    # check klist has renew info
    klist | grep renew
    if [ $? -eq 1 ]; then
       klist
       echo "klist info exception !"
       return
    fi

    expiredTime=$(klist | sed -n "5,1p" | awk '{print $2,$3}')
    expiredTimeFT=$(date -d "${expiredTime}" +"%Y-%m-%d %H:%M:%S")
    expiredTimestamp=$(date -d "$expiredTimeFT" +%s)
    currentTimestamp=$(date +%s)
    if [ $expiredTimestamp -gt $currentTimestamp ]; then
      let free=$expiredTimestamp-$currentTimestamp
      # 剩余时间大于所设阈值,则不需要 kinit
      if [ $free -gt $maxLeftTime ]; then
         nowTime=$(date "+%Y-%m-%d %H:%M:%S")
         echo "NowTime: $nowTime, ExpiredTime: $expiredTimeFT, no need to kinit!"
         needKinit=1
      fi
    fi
  else
    echo "krb file does not exist!"
  fi
}

# 传入参数检查
if [  $# != 2 -o "$proxyUser" = "" -o "$passwd" = "" ]; then
    echo "some curtionl params is null ! please check your prams"
    exit 1
fi

check_if_need_kinit
if [ $needKinit -eq 0 ]; then
  user=$(whoami)
  # 抢占独占锁,进行 kinit 操作
  echo "try to kinit, command: [ echo password | kinit $proxyUser ]"
  flock -xn /tmp/${user}_kinit_lock -c "echo '$passwd' | kinit $proxyUser"
  if [ $? -eq 0 ]; then
    echo "kinit success!"
  else
    echo "kinit faild: may be kinit by other concurrent process or password is incorret!"
  fi
fi

实际脚本中 考虑了认证用户错位 - principal 不匹配,认证 klist 信息不全认证失败等场景,较大程度提高了认证的可用性。

参考:
[1] https://docs.oracle.com/cd/E56344_01/html/E54075/kinit-1.html
[2] http://www.jusene.me/2017/02/22/flock/

你可能感兴趣的:(并发 kinit 导致 kerberos GSS initiate failed 定位与解决)