標籤:hbase mapreduce zookeeper acl kerberos
最近公司HBase(CDH-4.6.0)遇到了一個麻煩問題,覺得有必要記錄下整個解決的過程。
問題起因
使用者在跑mapreduce任務,從hdfs讀取檔案想寫入到hbase table的時候失敗了(這是hbase提供的一種mapred能力)。這個問題發現在A環境(一個測試環境),自從啟用了kerberos之後。運行了使用者給的程式和自己寫的sample之後,發現程式最後掛在NullPointerException上。這個NPE指示的是服務端的一個叫currentKey的變數為null。
[email protected],java.io.IOException: java.io.IOException: java.lang.NullPointerExceptionat 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)at org.apache.hadoop.hbase.ipc.HBaseServer$Handler.run(HBaseServer.java:1428)
AuthenticationTokenSecretManager:
@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:
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:
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上找到了如下異常:
org.apache.hadoop.hbase.security.token.AuthenticationTokenSecretManager: Zookeeper initialization failedorg.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /hbase/tokenauth/keysat 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裡查看了下它的許可權是這樣的:
[zk: localhost:2181(CONNECTED) 0] getAcl /hbase/tokenauth‘sasl,‘hbase/[email protected]: cdrwa
但很奇怪的是不管我切換什麼賬戶也無法訪問這個節點,想通過setAcl設定它的許可權為anyone也是失敗的。原因很顯然,因為我不是“hbase/[email protected]”,我沒任何許可權操作。
Authentication is not valid : /hbase/tokenauth
可為什麼4048這台機子也沒能成為leader呢(問題[1])?
第一次解決(day 0)
嘗試各種辦法也無法獲得/hbase/tokenauth的控制權,我們只好暫時通過在zookeeper設定檔zoo.cfg添加參數skipACL=true,重啟zookeeper,這樣不會驗證ACL。
重啟hbase,觸發AuthenticationTokenSecretManager.start,大家開始競爭成為leader,於是有了leader,leader是4048這台機子。
然後再通過zookeeper-client的setAcl命令把這個點的許可權改成anyone,再關閉skipACL,重啟zookeeper。
這些是我同事操作的,操作完之後叢集一切正常,mapreduce也可以跑了。不過還有一個隱患,我注意到了/hbase/tokenauth/keys的許可權也是4048專屬,如果4048掛掉了,別人也無法順利成為leader,但是想想它掛掉的機率比較低,等它掛掉再說吧,於是就沒去理會了。
問題2 (day 1)
今天中午的時候,叢集突然奔潰了,所有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 zookeeperorg.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /hbase/tokenauth/keys/3at 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)
第二次解決(day 1)
添加skipACL後重啟ZK,重啟HBase。就這樣暫時保持skipACL開啟,保證hbase正常運行。
思考(day 2)
我們總不能這樣開著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/[email protected]: cdrwa
前者非常統一的使用hbase這個principal,後者則帶上了hostname。
問題必定出在這裡!
我又對比了hbase的zk-jaas.conf,沒區別。這個設定檔裡配置了訪問zk的principal,它們都是帶hostname的。
Client {com.sun.security.auth.module.Krb5LoginModule requireduseKeyTab=trueuseTicketCache=falsekeyTab="/etc/hbase.keytab"principal="hbase/[email protected]";};
可為什麼B最後的principal卻沒帶hostname,我又對比了zookeeper的設定檔zoo.cfg。
B的有下面兩行設定:
kerberos.removeHostFromPrincipal=truekerberos.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/[email protected]: 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 = 0x1900000097ctime = Mon Aug 24 14:30:48 CST 2015mZxid = 0x1c000000e8mtime = Tue Aug 25 15:35:36 CST 2015pZxid = 0x1900000097cversion = 0dataVersion = 1aclVersion = 0ephemeralOwner = 0x0dataLength = 42numChildren = 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/4org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /hbase/tokenauth/keys/4at 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); } }
第三次解決(day 2)
修複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="[email protected]<YOUR-REALM>";};
而不是這樣:
Client {com.sun.security.auth.module.Krb5LoginModule requireduseKeyTab=trueuseTicketCache=falsekeyTab="/etc/hbase.keytab"principal="hbase/[email protected]";};
這樣就不會帶上hostname了吧?
附錄
Zookeeper Authentication
HBase as a MapReduce Job Data Source and Data Sink
著作權聲明:本文為我原創文章,轉載請註明出處
kerberos下HBase訪問Zookeeper的ACL問題