最近接连排查了几个问题,居然都是同一个参数引起的,本文就通过实际案例讲述下该参数如何引发问题的,以及问题最终又是如何解决的~
【First Blood】
在我们的环境中,RM 是基于 HA 的方式部署的,并且 RM 是基于容器的方式运行的,即两个 RM 运行在各自的容器中;同时,我们还开启了 kerberos 认证,因此两个 RM 的 hostname 配置的是域名。主要的配置信息如下所示:
<property>
<name>yarn.resource.ha.enabled</name>
<value>true</value>
</property>
<property>
<name>yarn.resourcemanager.ha.automatic-failover.enable</name>
<value>true</value>
</property>
<property>
<name>yarn.resourcemanager.ha.rm-ids</name>
<value>rm1,rm2</value>
</property>
<property>
<name>yarn.resourcemanager.hostname.rm1</name>
<value>rm-0.svc.cluster.local</value>
</property>
<property>
<name>yarn.resourcemanager.hostname.rm2</name>
<value>rm-1.svc.cluster.local</value>
</property>
复制代码
在一次测试过程中,RM 的其中一个(容器所在)节点异常宕机了,此后向 RM 提交了一个任务,但该任务的 AM 启动后就失败了,报错信息为:
java.lang.IllegalArgumentException: java.net.UnknownHostException: hadoop-resourcemanager-0.hadoop-resourcemanager.hncscwc-198.svc.cluster.local1
at org.apache.hadoop.security.SecurityUtil.buildTokenService(SecurityUtil.java:418)
at org.apache.hadoop.yarn.client.ClientRMProxy.getTokenService(ClientRMProxy.java:153)
at org.apache.hadoop.yarn.client.ClientRMProxy.getAMRMTokenService(ClientRMProxy.java:138)
at org.apache.hadoop.yarn.client.ClientRMProxy.setAMRMTokenService(ClientRMProxy.java:80)
at org.apache.hadoop.yarn.client.ClientRMProxy.getRMAddress(ClientRMProxy.java:99)
at org.apache.hadoop.yarn.client.ConfiguredRMFailoverProxyProvider.getProxyInternal(ConfiguredRMFailoverProxyProvider.java:76)
at org.apache.hadoop.yarn.client.ConfiguredRMFailoverProxyProvider.getProxy(ConfiguredRMFailoverProxyProvider.java:90)
at org.apache.hadoop.io.retry.RetryInvocationHandler$ProxyDescriptor.<init>(RetryInvocationHandler.java:197)
at org.apache.hadoop.io.retry.RetryInvocationHandler.<init>(RetryInvocationHandler.java:317)
at org.apache.hadoop.io.retry.RetryInvocationHandler.<init>(RetryInvocationHandler.java:311)
at org.apache.hadoop.io.retry.RetryProxy.create(RetryProxy.java:59)
at org.apache.hadoop.yarn.client.RMProxy.createRMProxy(RMProxy.java:120)
at org.apache.hadoop.yarn.client.RMProxy.createRMProxy(RMProxy.java:93)
at org.apache.hadoop.yarn.client.ClientRMProxy.createRMProxy(ClientRMProxy.java:72)
at org.apache.hadoop.mapreduce.v2.app.rm.RMCommunicator.createSchedulerProxy(RMCommunicator.java:311)
at org.apache.hadoop.mapreduce.v2.app.rm.RMCommunicator.serviceStart(RMCommunicator.java:117)
at org.apache.hadoop.mapreduce.v2.app.rm.RMContainerAllocator.serviceStart(RMContainerAllocator.java:263)
复制代码
RM 配置了高可用,其中一个宕机,另外一个 RM 也确实提升为 Active 了,任务也能正确提交和调度,但为什么运行就报错了呢?
顺着报错的堆栈信息,走读相关的代码,我们发现了问题的所在。
在《YARN 任务运行中的 token》中提到了 yarn 任务的 AM 在启动后,会从指定的文件中加载 AMRMToken,而 rm 的客户端在初始化时需要给 token 设置服务端的地址,也就是 rm 的地址。
关键代码如下所示:
获取token服务
获取token服务对应的ip地址
在 buildTokenService 中,判断如果必须使用 IP(userIpForTokenService),则会对 rm 的域名进行解析,如果无法解析出具体的 ip 地址,则抛出异常;异常会逐层往上抛,最终导致程序退出。
结合实际情况来分析,由于其中一个 rm 出现了宕机,其域名确实无法解析出对应的 ip 来,因此这也就是导致任务失败的根本原因。
至于 useIpForTokenService 的值,是由配置项 hadoop.security.token.service.use_ip 来决定的,默认为 true,即 tokenService 需要使用 ip,而不是域名。
最终,将该配置参数设置为 false 后,再次测试验证,在同样的场景下,任务可以正确提交和运行。
【Double Kill】
在上面问题解决后的第二天,重新部署环境时,发现 jobHistoryServer 由于无法正确进行 kerberos 认证,导致启动失败,具体报错信息为:
org.apache.hadoop.yarn.exceptions.YarnRuntimeException: History Server Failed to login
at org.apache.hadoop.mapreduce.v2.hs.JobHistoryServer.serviceInit(JobHistoryServer.java:130)
at org.apache.hadoop.service.AbstractService.init(AbstractService.java:163)
at org.apache.hadoop.mapreduce.v2.hs.JobHistoryServer.launchJobHistoryServer(JobHistoryServer.java:231)
at org.apache.hadoop.mapreduce.v2.hs.JobHistoryServer.main(JobHistoryServer.java:241)
Caused by: java.io.IOException: Login failure for hadoop/172.16.20.18@BIGDATA.COM from keytab /home/hncscwc/hadoop/etc/hadoop/hdfs.keytab: javax.security.auth.login.LoginException: Unable to obtain password from user
at org.apache.hadoop.security.UserGroupInformation.loginUserFromKeytab(UserGroupInformation.java:1144)
at org.apache.hadoop.security.SecurityUtil.login(SecurityUtil.java:286)
at org.apache.hadoop.mapreduce.v2.hs.JobHistoryServer.doSecureLogin(JobHistoryServer.java:183)
at org.apache.hadoop.mapreduce.v2.hs.JobHistoryServer.serviceInit(JobHistoryServer.java:128)
... 3 more
Caused by: javax.security.auth.login.LoginException: Unable to obtain password from user
at com.sun.security.auth.module.Krb5LoginModule.promptForPass(Krb5LoginModule.java:901)
at com.sun.security.auth.module.Krb5LoginModule.attemptAuthentication(Krb5LoginModule.java:764)
at com.sun.security.auth.module.Krb5LoginModule.login(Krb5LoginModule.java:618)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at javax.security.auth.login.LoginContext.invoke(LoginContext.java:755)
at javax.security.auth.login.LoginContext.access$000(LoginContext.java:195)
at javax.security.auth.login.LoginContext$4.run(LoginContext.java:682)
at javax.security.auth.login.LoginContext$4.run(LoginContext.java:680)
at java.security.AccessController.doPrivileged(Native Method)
at javax.security.auth.login.LoginContext.invokePriv(LoginContext.java:680)
at javax.security.auth.login.LoginContext.login(LoginContext.java:587)
at org.apache.hadoop.security.UserGroupInformation$HadoopLoginContext.login(UserGroupInformation.java:522)
at org.apache.hadoop.security.UserGroupInformation.loginUserFromKeytab(UserGroupInformation.java:1135)
... 6 more
复制代码
问题出现后,先对 jobHistoryServer 的配置文件进行了确认,发现与之前是一样的,并没有什么不对的地方,关键配置项如下所示:
<property>
<name>mapreduce.jobhistory.address</name>
<value>172.16.20.18</value>
</property>
<property>
<name>mapreduce.jobhistory.principal</name>
<value>hadoop/_HOST@BIGDATA.COM</value>
</property>
复制代码
jobHistoryServer 启动后进行 kerberos 登陆时,会对 principal 中的_HOST 进行替换,替换内容为 mapreduce.jobhistory.address 的值,即最终以 ip 形式进行了替换,导致认证失败。
正常来说,向 kdc 登陆认证的 principal 应该是包含服务的主机名,而不是 ip 地址,并且之前在这种配置下也都没有任何问题,怎么突然就不正常了?
还是结合源码进行分析,找到了问题所在:
配置项 hadoop.security.token.service.use_ip 配置为 true 或 false 时,内部会产生不同的主机地址解析对象:
两者对于 host 地址为 ipv4 的解析有所不同
对于 StandardHostResolver:
StandardHost解析方式
对于 QualifiedHostResolver:
QualifiedHost解析方式
也就是说,StandardHostResolver 可以通过 getByName 正确解析出 ip 对应的主机名,而后者直接将 ip 返回。
之前 hadoop.security.token.service.use_ip 配置为 true,因此配置项 mapreduce.jobhistory.address 即便配置为 ip,也能正确解析出对应的主机名,然后在 principal 替换_HOST 时,也是正确的。
修改上面的问题后,将配置的值改为了 false,就导致了该问题的出现。
最后,我们通过将配置项 mapreduce.jobhistory.address 的值修改为主机名解决了该问题。
【Triple Kill】
没有问题的日志维持了两三天,再次遇到问题,这次的现象是在 sparkHistory 节点上向 hdfs 上传文件失败。
首先,我们先在其它节点上进行了同样的操作,发现向 hdfs 上传文件是没有任何问题的,同时查看 nn/dn 的进程情况,结合对应日志确认 nn/dn 都是正常的。
接着,我们重新回到该节点上,检查了 hdfs-site.xml 中的相关配置项,没有发现异常的地方,然后向 nn/dn 的节点执行了 ping 操作,确认网络也没有问题,但发现 hdfs 相关的所有操作都失败。
最后,通过 tcpdump 进行了抓包分析,发现与 nn 建立连接时,tcp 的源端地址为 127.0.0.1,这导致 syn 发送后,根本得不到 sync ack 应答。而正常情况下,源 ip 应该是该节点自身的 ip。
抓包信息
进一步调整日志为 TRACE 后,发现是在 hdfs 的客户端进行了 bind 的操作。
22/04/19 17:10:50 TRACE ipc.ProtobufRpcEngine: 1: Call -> namenode.svc.cluster.local/172.16.22.234:9000: getFileInfo {src: "/"}
22/04/19 17:10:50 DEBUG ipc.Client: The ping interval is 60000 ms.
22/04/19 17:10:50 DEBUG ipc.Client: Connecting to namenode.svc.cluster.local/172.16.22.234/9000
22/04/19 17:10:50 TRACE secruity.SecruityUtil: Name lookup for spark-history.svc.cluster.local took 3ms.
22/04/19 17:10:50 DEBUG ipc.Client: Binding hadoop/spark-history.svc.cluster.local@BIGDATA.COM to spark-history.svc.cluster.local.localdomain/127.0.0.1
复制代码
既然知道是 hdfs 客户端中对 socket 进行了源 IP 的绑定动作,那么就结合源码梳理下 hdfs 客户端向 nn 建立连接的逻辑:
在开启 kerberos 认证的场景中,客户端向 nn 建立连接的流程包括:创建 socket,然后从 ticket 中解析出 bind 地址并进行 bind 操作,最后进行连接。
从 ticket 中解析出本地 bind 地址的具体步骤又分为:
对于主机名解析又分为两种情况
如果配置项 "hadoop.security.token.service.use_ip" 的值为 true,则直接获取主机名对应的 ip,如果为 false,则继续按下面的逻辑解析(其本意是想要获取主机名对应的完全合规域名)
如果主机名为 ipv4,通过 ip 地址获取对应的全域名
如果主机名以 "." 结尾,直接获取主机名对应的全域名
如果主机名包含 ".",先在主机名末尾加上 ".",并继续上一步的逻辑解析,如果解析出的域名为空,则在主机名末尾依次添加 "/etc/resolve.conf" 中的 "search" 指定的域,进行主机名的解析
以实际情况来分析:
sparkHistory 进程 kerberos 登陆使用的 principal 为 "hadoop/spark-history.svc.cluster.local@BIGDATA.COM"
从 pincipal 中解析出主机名为 "spark-history.svc.cluster.local"
配置项 "hadoop.security.token.service.use_ip"的值为 false,因此进入全域名的解析流程。主机名不是完全合规(即不是以"."结尾),但又包含了".",因此先在末尾加上".",使其成为完全合规域名,并按照该域名来解析。
而由于 sparkHistory 所在的容器,配置了就绪探针,容器未就绪时,无法解析出任何地址。因此继续在 "spark-history.svc.cluster.local." 后再加上 "/etc/resolve.conf" 中 search 指定的域,便利进行全域名解析,如果其中任意一个能解析出地址,则退出循环
该节点中 "/etc/resolve.conf" 中的 search 中仅有一个 localdomain,因此以 "spark-history.svc.cluster.local.localdomain" 来解析,解析出的 ip 恰好就是 127.0.0.1,导致了问题的出现
该节点的 /etc/resolve.conf 文件中之所以只有 "search localdomain",怀疑是人为进行了修改导致的。最后,修改该文件,问题得以解决。
【总结】
通过这三个问题分析定位解决,对 "hadoop.security.token.service.use_ip" 有了较深入的理解,同时也深刻领会源码是不会骗人的。
评论