大數據時代,HBase作為一款擴展性極佳的分布式存儲系統,越來越多地受到各種業務的青睞,以求在大數據存儲的前提下實現高效的隨機讀寫操作。對于業務方來講,一方面關注HBase本身服務的讀寫性能,另一方面也需要更多地關注HBase客戶端參數的具體意義。這篇文章就從一個具體的HBase客戶端異常入手,定位異常發生的原因以及相應的客戶端參數優化。
案發現場
最近某業務在使用HBase客戶端讀取數據時出現了大量線程block的情況,業務方保留了當時的線程堆棧信息,如下圖所示:
看到這樣的問題,首先從日志和監控排查了業務表和region server,確認了在很長時間內確實沒有請求進來,除此之外并沒有其他有用的信息,同時也沒有接到該集群上其他用戶的異常反饋,從現象看,這次異常是在特定環境下才會觸發的。
案件分析過程
1.根據上圖圖1所示,所有的請求都block在<0x0000000782a936f0>這把全局鎖上,這里需要關注兩個問題:
-
哪個線程持有了這把全局鎖<0x0000000782a936f0>?
-
這是一把什么樣的全局鎖(對于問題本身并不重要,有興趣可以參考步驟3)?
2.哪個線程持有了這把鎖?
2.1 很容易在jstack日志中通過搜索找到全局鎖<0x0000000782a936f0>被如下線程持有:
定睛一看,該線程持有了這把全局鎖,而且處于TIMED_WAITING狀態,因此這把鎖可能長時間不釋放,導致所有需要這把全局鎖的線程都阻塞等待。好了,那問題就轉化成了:為什么這個線程會處于TIME_WAITING狀態?
2.2 根據上圖提示,查看源碼中RpcRetryingCall.java的115行代碼,可以確定該線程處于TIME_WAITING狀態是因為自己休眠導致,如下圖所示:
RpcRetryingCall函數是Rpc請求重試機制的實現,所以可以有兩點推斷:
-
HBase客戶端請求在那個時間段網絡有異常導致rpc請求失敗,進入重試邏輯
-
根據HBase的重試機制(退避機制),每兩次重試機制之間會休眠一段時間,即上圖115行代碼,這個休眠時間太長導致這個線程一直處于TIME_WAITING狀態。
休眠時間由上圖中expectedSleep = callable.sleep(pause,tries + 1)決定,根據hbase算法(見第三部分),默認最大的expectedSleep為20s,整個重試時間會持續8min,這也就是說全局鎖會被持有8min,可這并不能解釋持續將近幾個小時的阻塞無請求。除非有兩種情況:
-
配置有問題:需要客戶端檢查hbase.client.pause和hbase.client.retries.number兩個參數配置出現異常,比如hbase.client.pause參數如果手抖配成了10000,就有可能出現幾個小時阻塞的情況
-
網絡持續有問題:如果線程1持有全局鎖重試失敗之后退出,線程2競爭到這把鎖,此時網絡依然有問題,線程2會再次進入重試,重試8min之后失敗退出,循環下去,也有可能出現幾個小時阻塞的情況
和業務方確認配置,所有參數基本屬于默認配置,因此猜測一不成立,那最有可能的情況就是猜測二。經過確認,在事發當時(凌晨0點~早上6點)確實存在很多服務因為云網絡升級異常發生抖動的情況出現。然而因為沒有具體的日志信息,所以并不能完全確認猜測是否正確。但是,通過問題的分析可以進一步明白HBase重試機制以及部分客戶端參數優化策略,這也是寫這篇文章的初衷之一。
3. 再來看看這把全局鎖到底是什么鎖,查看源碼可知這把鎖是下圖中紅框中的regionLockObject對象:
參考源碼注釋可知,這把鎖是為了防止同時多線程并發加載meta分區。全局鎖代碼塊首先會從緩存中查找meta分區,如果不存在會執行prefetchRegionCache方法遠程查找并寫入緩存,因此如果第一個線程成功加載meta分區數據并寫入緩存,后來線程可以直接使用。
正常情況下,prefetchRegionCache方法只有在緩存不存在的情況下會執行,如果此時網絡不存在問題,遠程查找meta分區信息會很快完成,持鎖時間也會很短。一旦網絡出現長時間抖動,就有可能出現這把鎖一直被持有,阻塞其他線程。
HBase Rpc重試機制
通過上文分析可知,HBase的重試機制是這次異常發生的關鍵點,有必要對其進行一次解析。HBase執行rpc失敗之后會執行重試操作,重試的最大次數可以通過配置文件配置,對應的參數為hbase.client.retries.number,0.98版本中該參數的默認值為31。同時每兩次重試之間會sleep一段時間,即上文提到的expectedSleep變量,該變量實現具體算法如下:
public static int RETRY_BACKOFF[] = { 1, 2, 3, 5, 10, 20, 40, 100, 100, 100, 100, 200, 200 };long normalPause = pause * HConstants.RETRY_BACKOFF[ntries];long jitter = (long)(normalPause * RANDOM.nextFloat() * 0.01f); // 1% possible jitterreturn normalPause + jitter;
其中RETRY_BACKOFF是一個重試系數表,由小到大遞增表示重試時間會隨著重試次數逐漸遞增。pause變量可以通過配置文件配置,對應的參數為hbase.client.pause,0.98版本中該參數的默認值為100。
暫時忽略jitter這個小隨機變量,默認情況下最大的重試間隔休眠時間 expectedSleep = 100 * 200 = 20s。默認重試次數為31,則每次連接集群重試之間的暫停時間將依次為:
[100,200,300,500,1000,2000,4000,10000,10000,10000,10000,20000,20000,…,20000]
這意味著客戶端將在448s內重試30次,然后放棄連接到集群.
客戶端參數優化實踐
很顯然,根據上面第二部分和第三部分的介紹,一旦在網絡出現抖動的異常情況下,默認最差情況下一個線程會存在8min左右的重試時間,從而會導致其他線程都阻塞在regionLockObject這把全局鎖上。為了構建一個更穩定、低延遲的HBase系統,除過需要對服務器端參數做各種調整外,客戶端參數也需要做相應的調整:
1. hbase.client.pause:默認為100,可以減少為20
2. hbase.client.retries.number:默認為31,可以減少為11
修改后,通過上面算法可以計算出每次連接集群重試之間的暫停時間將依次為:
[20,40,60,100,200,400,800,2000,2000,2000,2000,4000,4000]
客戶端將會在18s內重試10次,然后放棄連接到集群,進而會再將全局鎖交給其他線程,執行其他請求。
總結
這篇文章從一個客戶端異常入手,通過堆棧分析、源碼追蹤、合理推斷,一方面整理分享程序異常的定位方法,一方面介紹HBase Rpc的重試機制以及客戶端參數優化。最后,希望能和大家一起擁抱大數據時代的到來,也希望通過我們的努力能夠讓大家更多地了解HBase!
本文章為作者原創
🈲禁止🈲
其他公眾賬號轉載,若有轉載,請標明出處
文章列表