轉自: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"; };