之前文章写过kafka的鉴权,以及集成ranger插件的配置使用。但真正在用起来后,发现里面有个坑,本文就来聊聊这个坑的情况以及排查过程。
【问题现象】
kafka在集成了ranger插件实现鉴权功能后,发现过一段时间后,controller无法正确连接上broker,并有如下报错:
// server.log中的日志
[2022-12-06 15:32:48,068] ERROR [Controller id=0, targetBrokerId=0] Connection to node 0 failed authentication due to: Authentication failed due to invalid credentials with SASL mechanism GSSAPI (org.apache.kafka.clients.NetworkClient)
// controller.log中的日志
[2022-12-06 15:32:48,663] WARN [RequestSendThread controllerId=0] Controller 0's connection to broker kafka-0.bigdata:9090 (id: 0 rack: null) was unsuccessful (kafka.controller.RequestSendThread)
【问题分析】
1. 初步分析
问题咋一看,稍微有点懵,跑得好好的怎么突然就报错了。从日志也不太能看出问题产生的前后逻辑。所以先花了些时间整理相关的逻辑流程、涉及的类以及彼此之间的关系。
1)在kafka的controller管理中,维护了一个broker信息的hashmap,其ID为broker的ID,broker信息则以ControllerBrokerStateInfo实例对象记录。
2)每个ControllerBrokerStateInfo内部都有一个队列,controller需要给broker的发送的请求,均直接发送到该队列中;除了队列,每个ControllerBrokerStateInfo还有一个请求发送线程,循环从队列中取出消息发送给对应的broker。而network则记录与该broker的网络连接相关信息。
3) ControllerBrokerStateInfo中的network,往下细分,又包含Selector、KafkaChannel、ChannelBuilder、TransportLayer、Authenticator这么几层。
Selector:充当IO轮询处理的衔接,内部为不同broker,封装为不同的KafkaChannel。
ChannelBuilder:这是一个接口,具体有不同的实现,实际会根据配置中指定的协议类型(SASL、SSL、Plaintext)构造对应的实例类对象。该channelBuilder后续会再根据指定的模式(客户端或服务端)创建对应的传输层(TransportLayer)和鉴权(Authenticator)。
TransportLayer:也是一个接口,会根据实际的协议构造对应的实例类对象,负责完成请求与响应数据的传输
Authenticator:同样是一个接口,根据协议类型构造对应的实例类对象,完成交互过程中可能需要的鉴权,对于SASL又会区分客户端和服务端
KafkaChannel:对应一个broker的连接信息,内部有TransportLayer、Authenticator的实例对象作为类成员,记录连接相关的信息。
对于开启kafka鉴权时,配置采用了SASL_PLAINTEXT的协议,同时在jaas中指定了keytab文件与对应的principal。
那么controller在与broker交互过程中,会根据协议类型使用SaslChannelBuilder,同时读取jaas配置文件中的principal,并将其传递给SaslClientAuthenticator、SaslServerAuthenticator,在内部分别构造SaslClient、SaslServer时也会透传该principal信息。
2. 锁定问题出现场景
初步梳理了整个流程后,未发现存在问题的地方,也没搞清楚问题出现的时机。于是只能改变思路,模拟制造一些异常场景来分析可能出现问题的时机。
通过制造与zookeeper之间的网络异常、与kerberos之间的网络异常、进程重启、断电等场景来尝试复现问题,但问题就是没有出现。
感觉再次陷入死胡同时,中途插入另外一个紧急的事情,于是将环境搁置了一天,等忙完紧急事情后,发现问题复现了。联想到我们的kerberos票据有效期恰好是1天时间,于是尝试手动缩短kdc的票据时间来加速问题复现,当票据过期后,惊喜地发现问题复现了。
也就是说, 在kerberos票据过期后,尤其是触发controller重新选举,会立即出现该问题。
那么,在这种情形下,前一个版本是否也有同样的问题呢?
回退到前一个版本,再次执行同样的步骤尝试复现,但此时新的controller都能正确的连接到broker。
这也就说明只有当前版本存在这个问题。而当前版本的改动点主要就是引入的ranger插件实现kafka的权限控制。
通过尝试关闭kafka的鉴权、以及使用kafka原生自带的鉴权方式来再次复现问题时,结果都正常。
这样,也就真正锁定了方向,该问题就是使用ranger的kafka插件后,在kerberos票据过期后,必然出现新的controller与所有broker均连接失败的问题。
3. 再次分析
确定问题出现的场景后,终于可以有的放矢进行分析了,重新复现问题后,先通过arthas对错误打印对应代码处进行了跟踪,发现和正常情况下的值有所不同(代码如下所示)
// Jdk中的GssKrb5Server.java
public byte[] evaluateResponse(byte[] responseData) throws SaslException {
...
if (protocolSaved != null &&
!protocolSaved.equalsIgnoreCase(me.split("[/@]")[0])) {
throw new SaslException(
"GSS context targ name protocol error: " + me);
}
...
}
正常情况下"protocolSaved"应该是一个空值,而出现问题时却是"hadoop"。
一开始怀疑是kafka所使用的keytab文件的问题(该文件中包含了两个principal,一个是kafka所使用的principal: kafka/_<HOSTNAME@bigdata.com>,另外一个则是hadoop),手动将keytab中hadoop的principal剔除后,发现出现问题时对应的值依旧是hadoop。
又一次查看整个流程,也没有看到有可能将hadoop赋值进去的地方,分析再一次陷入僵局。
再次转变思路,既然问题是引入ranger插件后引发的,先分析下ranger插件里面都做了些什么事情。
在ranger插件初始化时,会根据kafka中jaas指定的principal构造一个UGI(hadoop中的UserGroupInformation类),后续都会使用该UGI完成审计信息的记录。
但是:在构造UGI的时候,会在原有的subject中添加进程启动的系统用户的principal(对于我们的场景而言,kafka就是以hadoop用户来启动的。hadoop也就是在此时添加进去的)
这样一来,在subject中就同时增加了两个principal。
注意,subject中的首个principal还是jaas中指定的,因为是先依次构造的subject,然后在构造UGI时,才添加了进程对应系统用户的principal。
当kerberos票据过期后,keytab中对应的principal会从subject中移除,系统用户对应的principan会变成subject中的首个principal。
此后,kafka的controller连接broker的交互过程中,broker作为服务端创建saslServer时,由于subject中的首个principal已经变为系统用户,与客户端指定的服务端principal不符,导致出现问题。
【问题解决】
问题出现后,先在社区上逛了一圈,恰好发现有人已经修复了此问题,本质上就是在ranger插件中,修改调用UGI的方法,避免在原有subject中引入进程对应系统用户的principal。我们引入该patch后,问题也就自然而然地解决了。
对应的issue: https://issues.apache.org/jira/browse/RANGER-2810
【总结】
在这个问题排查过程中,有很多东西还可以再深入下,例如controller与broker交互的源码,jaas的原理等等,有兴趣的可以自己去看看。另外就是,遇到问题,先不要慌,偶现问题找规律变成必现问题,这样离解决问题就不远了。最后,开源的问题很多时候可以去社区上找找答案。
好了,这就是本文的全部内容,如果觉得本文对您有帮助,请点赞+转发,也欢迎加我微信交流~