转自:http://www.aboutyun.com/thread-14977-1-1.html
最近公司HBase(CDH-4.6.0)遇到了一个麻烦问题,以为有必要记录下整个解决的过程。html
用户在跑mapreduce任务,从hdfs读取文件想写入到hbase table的时候失败了(这是hbase提供的一种mapred能力)。这个问题发如今A环境(一个测试环境),自从启用了kerberos以后。运行了用户给的程序和本身写的sample以后,发现程序最后挂在NullPointerException上。这个NPE指示的是服务端的一个叫currentKey的变量为null。java
org.apache.hadoop.hbase.ipc.ExecRPCInvoker$1@58e395e8,java.io.IOException: java.io.IOException: java.lang.NullPointerException at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.createPassword(AuthenticationTokenSecretManager.java:129) at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.createPassword(AuthenticationTokenSecretManager.java:57) at org.apache.hadoop.security.token.Token.<init>(Token.java:70) at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.generateToken(AuthenticationTokenSecretManager.java:162) at org.apache.hadoop.hbase.security.token.TokenProvider.getAuthenticationToken(TokenProvider.java:91) at sun.reflect.GeneratedMethodAccessor56.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.apache.hadoop.hbase.regionserver.HRegion.exec(HRegion.java:5610) at org.apache.hadoop.hbase.regionserver.HRegionServer.execCoprocessor(HRegionServer.java:3918) at sun.reflect.GeneratedMethodAccessor39.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.apache.hadoop.hbase.ipc.SecureRpcEngine$Server.call(SecureRpcEngine.java:311)
AuthenticationTokenSecretManagernode
@Override protected byte[] createPassword(AuthenticationTokenIdentifier identifier) { long now = EnvironmentEdgeManager.currentTimeMillis(); AuthenticationKey secretKey = currentKey; //currentKey赋给secretKey identifier.setKeyId(secretKey.getKeyId()); //NPE在这里抛出的,也就是currentKey为null identifier.setIssueDate(now); identifier.setExpirationDate(now + tokenMaxLifetime); identifier.setSequenceNumber(tokenSeq.getAndIncrement()); return createPassword(WritableUtils.toByteArray(identifier), secretKey.getKey()); }
既然currentKey为null,那咱们就去找它在哪里赋值的。阅读源码以后,了解到整个过程是这样的:
1.在开启kerberos以后,每一个RegionServer都会有一个AuthenticationTokenSecretManager用来管理token。
2.这些manager中,只有一个leader,只有它能生产token,而后放到zookeeper里。其它manager经过感知zookeeper的变化来同步leader生产的token。leader经过竞争产生,谁先在ZK上建立 /hbase/tokenauth/keymaster 节点,谁就是leader。
AuthenticationTokenSecretManager$LeaderElector:apache
public void run() { zkLeader.start(); zkLeader.waitToBecomeLeader(); //没有成为leader的人会一直阻塞在这里,直到感知到当前leader挂掉才会开始新一轮竞争 isMaster = true; while (!stopped) { long now = EnvironmentEdgeManager.currentTimeMillis(); // clear any expired removeExpiredKeys(); //清除过时的token,同时也把它从ZK上移除 if (lastKeyUpdate + keyUpdateInterval < now) { //默认的周期是1天 // roll a new master key rollCurrentKey(); //就是这个函数产生新的token,替换currenKey } try { Thread.sleep(5000); } catch (InterruptedException ie) { if (LOG.isDebugEnabled()) { LOG.debug("Interrupted waiting for next update", ie); } } } }
AuthenticationTokenSecretManager:app
synchronized void rollCurrentKey() { if (!leaderElector.isMaster()) { LOG.info("Skipping rollCurrentKey() because not running as master."); return; } long now = EnvironmentEdgeManager.currentTimeMillis(); AuthenticationKey prev = currentKey; AuthenticationKey newKey = new AuthenticationKey(++idSeq, Long.MAX_VALUE, // don't allow to expire until it's replaced by a new key generateSecret()); allKeys.put(newKey.getKeyId(), newKey); currentKey = newKey; //滚动currentKey,置为newKey zkWatcher.addKeyToZK(newKey); //把新的token放到zookeeper lastKeyUpdate = now; if (prev != null) { // make sure previous key is still stored prev.setExpiration(now + tokenMaxLifetime); //prev是原来的newKey,是不会过时的,当有新的newKey替代它后,它的期限默认设置是7天 allKeys.put(prev.getKeyId(), prev); zkWatcher.updateKeyInZK(prev); } }
3.既然token是由leader生产的,除非没有leader,才会没人生产。验证这个想法,我在zookeeper和一些region server启动当天的日志里找到了证据:
a) zk中的 /hbase/tokenauth/keymaster 节点用来存放leader的信息,而后进入zookeeper-client查看了下,根本没这个节点。
b) 一些尚有保留集群启动当天日志的region server上找到了以下异常:ide
org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager: Zookeeper initialization failed org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /hbase/tokenauth/keys at org.apache.zookeeper.KeeperException.create(KeeperException.java:113) at org.apache.zookeeper.KeeperException.create(KeeperException.java:51) at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:783) at org.apache.hadoop.hbase.zookeeper.RecoverableZooKeeper.createNonSequential(RecoverableZooKeeper.java:421) at org.apache.hadoop.hbase.zookeeper.RecoverableZooKeeper.create(RecoverableZooKeeper.java:403) at org.apache.hadoop.hbase.zookeeper.ZKUtil.createWithParents(ZKUtil.java:1164) at org.apache.hadoop.hbase.zookeeper.ZKUtil.createWithParents(ZKUtil.java:1142) at org.apache.hadoop.hbase.security.token.ZKSecretWatcher.start(ZKSecretWatcher.java:58) at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.start(AuthenticationTokenSecretManager.java:105) at org.apache.hadoop.hbase.ipc.SecureRpcEngine$Server.startThreads(SecureRpcEngine.java:275) at org.apache.hadoop.hbase.ipc.HBaseServer.start(HBaseServer.java:1650) at org.apache.hadoop.hbase.regionserver.HRegionServer.startServiceThreads(HRegionServer.java:1728) at org.apache.hadoop.hbase.regionserver.HRegionServer.handleReportForDutyResponse(HRegionServer.java:1105) at org.apache.hadoop.hbase.regionserver.HRegionServer.run(HRegionServer.java:753) at java.lang.Thread.run(Thread.java:662)
这是AuthenticationTokenSecretManager启动时候失败了,启动的时候会先在ZK上建立/hbase/tokenauth/keys这个目录(即使这个目录已经存在也会执行这个操做,这是一种保证),这个目录用来存放leader生成的token。结果你们都没有/hbase/tokenauth的权限,因此都失败了(NoAuth for /hbase/tokenauth/keys,这里的提示有点瑕疵,实际上/hbase/tokenauth没有权限致使的)。然而发生这样的严重错误,server的启动并无被终止,而是继续运行下去,留下了隐患。
AuthenticationTokenSecretManager:函数
public void start() { try { // populate any existing keys this.zkWatcher.start(); //这里抛出的KeeperException // try to become leader this.leaderElector.start(); //这里竞争leader,可是由于异常这里不会被执行,因此没有人去竞争leader } catch (KeeperException ke) { LOG.error("Zookeeper initialization failed", ke); //发生异常,仅仅是打印一条error信息,而没有abort。在Hbase的不少地方,发生这样的错误都是会abort server的。 } }
4.错误缘由就是/hbase/tokenauth权限问题,在zookeeper-client里查看了下它的权限是这样的:oop
[zk: localhost:2181(CONNECTED) 0] getAcl /hbase/tokenauth 'sasl,'hbase/svr4048hw2285.hadoop.xxx.com@DC.SH.XXX.COM : cdrwa
但很奇怪的是无论我切换什么帐户也没法访问这个节点,想经过setAcl设置它的权限为anyone也是失败的。缘由很显然,由于我不是“hbase/svr4048hw2285.hadoop.xxx.com@DC.SH.XXX.COM”,我没任何权限操做。测试
Authentication is not valid : /hbase/tokenauth
可为何4048这台机子也没能成为leader呢(问题[1])?ui
尝试各类办法也没法得到/hbase/tokenauth的控制权,咱们只好暂时经过在zookeeper配置文件zoo.cfg添加参数skipACL=yes,重启zookeeper,这样不会验证ACL。
重启hbase,触发AuthenticationTokenSecretManager.start,你们开始竞争成为leader,因而有了leader,leader是4048这台机子。
而后再经过zookeeper-client的setAcl命令把这个点的权限改为anyone,再关闭skipACL,重启zookeeper。
这些是我同事操做的,操做完以后集群一切正常,mapreduce也能够跑了。不过还有一个隐患,我注意到了/hbase/tokenauth/keys的权限也是4048专属,若是4048挂掉了,别人也没法顺利成为leader,可是想一想它挂掉的几率比较低,等它挂掉再说吧,因而就没去理会了。
今天中午的时候,集群忽然奔溃了,全部region server都挂掉了。 我上去查了一下日志,结果居然和我昨天考虑到的隐患同样,4048挂掉了,而后其余人竞争leader的时候没有权限也挂掉了。4048为何会挂掉(问题[2])? 当时我没怎么看4048的日志,不知道它为何挂掉,只以为很巧。
这是从4050这台机子的region server上截取的两条日志,它先是成为了leader,而后由于没有权限维护/hbase/tokenauth/keys,天然想访问里面的key也是失败的。其余机子挂掉的缘由也同样。
2015-08-25 14:35:08,273 DEBUG org.apache.hadoop.hbase.zookeeper.ZKLeaderManager: Claimed the leader znode as 'SVR4050HW2285.hadoop.xxx.com,60020,1440397852179' 2015-08-25 14:35:08,288 FATAL org.apache.hadoop.hbase.regionserver.HRegionServer: ABORTING region server SVR4050HW2285.hadoop.xxx.com,60020,1440397852179: Unable to synchronize secretkey 3 in zookeeper org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /hbase/tokenauth/keys/3 at org.apache.zookeeper.KeeperException.create(KeeperException.java:113) at org.apache.zookeeper.KeeperException.create(KeeperException.java:51) at org.apache.zookeeper.ZooKeeper.setData(ZooKeeper.java:1266) at org.apache.hadoop.hbase.zookeeper.RecoverableZooKeeper.setData(RecoverableZooKeeper.java:349) at org.apache.hadoop.hbase.zookeeper.ZKUtil.updateExistingNodeData(ZKUtil.java:814) at org.apache.hadoop.hbase.security.token.ZKSecretWatcher.updateKeyInZK(ZKSecretWatcher.java:197) at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.rollCurrentKey(AuthenticationTokenSecretManager.java:257) at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager$LeaderElector.run(AuthenticationTokenSecretManager.java:317)
添加skipACL后重启ZK,重启HBase。就这样暂时保持skipACL开启,保证hbase正常运行。
咱们总不能这样开着skipACL,这对资源隔离不是很友好。我查看了下HBase的ZKUtil.java的代码。
这是建立ZNode时候,建立ACL的函数。它对一些特定节点使用CREATOR_ALL_AND_WORLD_READABLE权限,其他使用CREATOR_ALL_ACL权限。前者是建立者有全部权限,其他人有只读权限。后者是建立者有全部权限。
private static ArrayList<ACL> createACL(ZooKeeperWatcher zkw, String node) { if (isSecureZooKeeper(zkw.getConfiguration())) { // Certain znodes are accessed directly by the client, // so they must be readable by non-authenticated clients if ((node.equals(zkw.baseZNode) == true) || (node.equals(zkw.rootServerZNode) == true) || (node.equals(zkw.masterAddressZNode) == true) || (node.equals(zkw.clusterIdZNode) == true) || (node.equals(zkw.rsZNode) == true) || (node.equals(zkw.backupMasterAddressesZNode) == true) || (node.startsWith(zkw.assignmentZNode) == true) || (node.startsWith(zkw.masterTableZNode) == true) || (node.startsWith(zkw.masterTableZNode92) == true)) { return ZooKeeperWatcher.CREATOR_ALL_AND_WORLD_READABLE; } return Ids.CREATOR_ALL_ACL; } else { return Ids.OPEN_ACL_UNSAFE; } }
/hbase/tokenauth及其子节点显然使用的是CREATOR_ALL_ACL权限。那4048建立了key,而后又挂掉的话,那其它机子显然不可能成为leader。这种权限设定彷佛有点不科学。
由于B环境权限都很正常的,没出什么问题,我又对比了下A和B的权限和配置。
B leader生产的token的权限:
[zk: localhost:2181(CONNECTED) 4] getAcl /hbase/tokenauth/keys/67 'sasl,'hbase : cdrwa
A leader生产的token的权限:
[zk: localhost:2181(CONNECTED) 1] getAcl /hbase/tokenauth/keys/2 'sasl,'hbase/svr4048hw2285.hadoop.xxx.com@DC.SH.XXX.COM : cdrwa
前者很是统一的使用hbase这个principal,后者则带上了hostname。
问题一定出在这里!
我又对比了hbase的zk-jaas.conf,没区别。这个配置文件里配置了访问zk的principal,它们都是带hostname的。
Client { com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true useTicketCache=false keyTab="/etc/hbase.keytab" principal="hbase/svr4048hw2285.hadoop.xxx.com@DC.SH.XXX.COM"; };
可为何B最后的principal却没带hostname,我又对比了zookeeper的配置文件zoo.cfg。
B的有下面两行设置:
kerberos.removeHostFromPrincipal=true kerberos.removeRealmFromPrincipal=true
而A呢?竟然也有。。。
和同事讨论了下,他告诉我A这两行配置不是一开始就有的,是后来加上去的,当时A最先上kerberos还出了不少问题。我瞬间就懂了,一切疑惑都解开了。
问题[1]:为何4048这台机子也没能成为leader呢?
由于当初集群最先上kerberos启动的时候没加那两行remove配置,因此/hbase/tokenauth和/hbase/tokenauth/keys的权限都是归4048专属。后来由于出了问题,这两行配置被加上去,hbase重启。此时你们的principal都变成了hbase(包括4048),没有人能访问这个4048专属的目录。因而包括4048在内,没人成为leader。
问题[2]:4048为何会挂掉?
这个是由于咱们第一次解决的时候,只修复了/hbase/tokenauth而没有修复/hbase/tokenauth/keys,它的权限依然是4048全部。
[zk: localhost:2181(CONNECTED) 0] getAcl /hbase/tokenauth/keys 'sasl,'hbase/svr4048hw2285.hadoop.xxx.com@DC.SH.XXX.COM : cdrwa
当时重启hbase的时候仍是开着skipACL的,因此leader顺利的在/hbase/tokenauth/keys下面建立了token,集群正常启动,一切正常。
而后咱们关闭了skipACL,彷佛也没有问题,可为何刚好次日就奔溃了?
由于leader去更新token的默认周期刚好是一天,次日它想更新的时候由于没有/hbase/tokenauth/keys的权限而挂掉。
由于咱们加了那两行remove配置,即便这个leader是4048,它也没法访问,道理同问题[1]。
这个证据也很好找。
这是第一次解决时,新写入的token,它的建立时间是24号下午2点半。
[zk: localhost:2181(CONNECTED) 3] stat /hbase/tokenauth/keys/3 cZxid = 0x1900000097 ctime = Mon Aug 24 14:30:48 CST 2015 mZxid = 0x1c000000e8 mtime = Tue Aug 25 15:35:36 CST 2015 pZxid = 0x1900000097 cversion = 0 dataVersion = 1 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 42 numChildren = 0
这是leader挂掉的日志,时间是次日下午2点半,集群奔溃也是在2点半左右,恰好间隔24小时左右。
2015-08-25 14:33:01,515 FATAL org.apache.hadoop.hbase.security.token.ZKSecretWatcher: Unable to synchronize master key 4 to znode /hbase/tokenauth/keys/4 org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /hbase/tokenauth/keys/4 at org.apache.zookeeper.KeeperException.create(KeeperException.java:113) at org.apache.zookeeper.KeeperException.create(KeeperException.java:51) at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:783) at org.apache.hadoop.hbase.zookeeper.RecoverableZooKeeper.createNonSequential(RecoverableZooKeeper.java:421) at org.apache.hadoop.hbase.zookeeper.RecoverableZooKeeper.create(RecoverableZooKeeper.java:403) at org.apache.hadoop.hbase.zookeeper.ZKUtil.createWithParents(ZKUtil.java:1164) at org.apache.hadoop.hbase.zookeeper.ZKUtil.createSetData(ZKUtil.java:868) at org.apache.hadoop.hbase.security.token.ZKSecretWatcher.addKeyToZK(ZKSecretWatcher.java:180) at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager.rollCurrentKey(AuthenticationTokenSecretManager.java:250) at org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager$LeaderElector.run(AuthenticationTokenSecretManager.java:317) 2015-08-25 14:33:01,516 FATAL org.apache.hadoop.hbase.regionserver.HRegionServer: ABORTING region server SVR4048HW2285.hadoop.xxx.com ,60020,1440397852099: Unable to synchronize secret key 4 in zookeeper
日志显示它想写入新的token 4失败而终止,昨天写入的是3。由于新的token的id比旧的大一,因此正好挂在想写入4的时候。
AuthenticationTokenSecretManager:
synchronized void rollCurrentKey() { if (!leaderElector.isMaster()) { LOG.info("Skipping rollCurrentKey() because not running as master."); return; } long now = EnvironmentEdgeManager.currentTimeMillis(); AuthenticationKey prev = currentKey; AuthenticationKey newKey = new AuthenticationKey(++idSeq, //新token的id比上一次大一 Long.MAX_VALUE, // don't allow to expire until it's replaced by a new key generateSecret()); allKeys.put(newKey.getKeyId(), newKey); currentKey = newKey; zkWatcher.addKeyToZK(newKey); //试图向zk写入新token lastKeyUpdate = now; if (prev != null) { // make sure previous key is still stored prev.setExpiration(now + tokenMaxLifetime); allKeys.put(prev.getKeyId(), prev); zkWatcher.updateKeyInZK(prev); } }
修复zk上全部权限有问题的节点(设置权限为anyone),删除过时的token(这些token由于没有权限,没被人删除),关闭skipACL,重启zk。
由于已经添加了remove配置,如今不一样region server访问zookeeper的principal都是同样的,不会再出现权限问题。
后记
为了保证不一样region server访问zookeeper的principal同样,咱们必须在zoo.cfg里添加remove配置,这种作法彷佛不是特别科学。
由于做为hbase,你不能保证zookeeper里会有remove配置。假如zookeeper是另外一个团队维护,他们以为添加了这样的配置对其它app有影响呢?
事实上hbase做为client,zookeeper做为server,咱们彷佛能够给hbase配置统一的client身份?
zk-jaas.conf 相似这样:
Client { com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true keyTab="/path/to/zkcli.keytab" storeKey=true useTicketCache=false principal="zkcli@<YOUR-REALM>"; };
而不是这样:
Client { com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true useTicketCache=false keyTab="/etc/hbase.keytab" principal="hbase/svr4048hw2285.hadoop.xxx.com@DC.SH.XXX.COM"; };