[toc]
HBase服務(wù)高可用之路的探索
一、背景
這里的高可用并不是指HBase本身的高可用機(jī)制。而是HBase主備雙服務(wù)的高可用,線上業(yè)務(wù)依賴于主備HBase集群來提供數(shù)據(jù)支持,主集群首要的任務(wù)時(shí)負(fù)責(zé)數(shù)據(jù)的讀寫,備集群只是為了容災(zāi)。
對(duì)于HBase主備服務(wù)高可用方案的調(diào)研,團(tuán)隊(duì)內(nèi)部從未停止過探索的步伐。從最初手動(dòng)切換Nginx的域名映射,到統(tǒng)計(jì)異常日志占比,然后進(jìn)行自動(dòng)的域名切換。那時(shí)候我們面臨的狀況是,主集群大量讀寫超時(shí)、甚至服務(wù)不可用,造成業(yè)務(wù)方接口無法為用戶提供正常的線上業(yè)務(wù)時(shí),HBase運(yùn)維小伙伴們才能感知到HBase集群的異常狀態(tài),手動(dòng)切換流量至備集群,從而在服務(wù)恢復(fù)的時(shí)間內(nèi),造成了無法容忍的損失。
針對(duì)舊方案的種種痛點(diǎn),以及受微服務(wù)中熔斷概念的啟發(fā),最終選擇集成了餓了么提供的一個(gè)熔斷框架——doctor,實(shí)現(xiàn)了HBase主集群服務(wù)查詢異常時(shí),查詢流量能夠及時(shí)、自動(dòng)、無感知地進(jìn)行切換到備集群。
二、HBase熔斷API目前已實(shí)現(xiàn)的功能
- 基于happybase封裝的HBase讀寫操作的基本功能API
- 錯(cuò)誤請(qǐng)求比例到達(dá)一定閾值,觸發(fā)熔斷機(jī)制,主備集群自動(dòng)無感知切換
- 主備切換后,熔斷的恢復(fù)機(jī)制,將自動(dòng)感知主集群是否可以正常提供線上服務(wù)
- 服務(wù)異常切換時(shí),精確到單個(gè)接口的微信預(yù)警
三、關(guān)于熔斷
一般在微服架構(gòu)中,有一個(gè)組件角色叫熔斷器。顧名思義,熔斷器起的作用就是在特定的場(chǎng)景下關(guān)掉當(dāng)前的通路,從而起到保護(hù)整個(gè)系統(tǒng)的效果。
在微服務(wù)架構(gòu)中,一般我們的獨(dú)立服務(wù)是比較多的,每個(gè)獨(dú)立服務(wù)之間劃分責(zé)任邊界,并通過約定協(xié)議接口來進(jìn)行通信。當(dāng)我們的調(diào)用鏈路復(fù)雜依賴多時(shí),很可能會(huì)發(fā)生雪崩效應(yīng)。
假設(shè)有這么一個(gè)場(chǎng)景,有A, B, C, D四個(gè)獨(dú)立服務(wù),A會(huì)依賴B,C,D;當(dāng)D發(fā)生負(fù)載過高或網(wǎng)絡(luò)異常等導(dǎo)致響應(yīng)過慢或超時(shí)時(shí),很可能A會(huì)因此堆積過多的等待鏈接,從而導(dǎo)致A的狀態(tài)也轉(zhuǎn)為異常,后面依賴到A的其他服務(wù)跟著發(fā)生鏈?zhǔn)椒磻?yīng),這將會(huì)導(dǎo)致大面積的服務(wù)不可用,即使本來是一些沒有依賴到B,C,D的服務(wù)。如下圖所示:

這不是我們希望看到的結(jié)果,所以這個(gè)時(shí)候熔斷器可以派上用場(chǎng)。最簡(jiǎn)單的做法,我們?yōu)槊總€(gè)依賴服務(wù)配置一個(gè)熔斷器開關(guān),正常情況下是關(guān)閉的,也就是可以正常發(fā)起請(qǐng)求;當(dāng)請(qǐng)求失敗(超時(shí)或者其他異常)次數(shù)超過預(yù)設(shè)值時(shí),熔斷器自動(dòng)打開,這時(shí)所有經(jīng)過這個(gè)熔斷器的請(qǐng)求都會(huì)直接返回失敗,并沒有真正到達(dá)所依賴的服務(wù)上。這時(shí)服務(wù)A本身仍然是能正常服務(wù)的。當(dāng)然,我們針對(duì)失敗請(qǐng)求的策略,并沒有這么簡(jiǎn)單粗暴。
四、借鑒HBase熔斷切換在有贊團(tuán)隊(duì)內(nèi)的實(shí)踐
HBase 雖然提供了 HBase Replication 機(jī)制,用來實(shí)現(xiàn)集群間單方向的異步數(shù)據(jù)復(fù)制,線上雖然部署了雙集群,備集群 SSD 分組和主集群 SSD 分組有相同的配置。當(dāng)主集群因?yàn)榇疟P,網(wǎng)絡(luò),或者其他業(yè)務(wù)突發(fā)流量影響導(dǎo)致某些 RegionServer 甚至集群不可用的時(shí)候,就需要提供備集群繼續(xù)提供服務(wù),備集群的數(shù)據(jù)可能會(huì)因?yàn)?HBase Replication 機(jī)制的延遲,相比主集群的數(shù)據(jù)是滯后的,按照我們集群目前的規(guī)模統(tǒng)計(jì),平均延遲在 100ms 以內(nèi)。所以為了達(dá)到高可用,業(yè)務(wù)方只能接受復(fù)制延遲,放棄強(qiáng)一致性,選擇最終一致性和高可用性。
有贊技術(shù)團(tuán)隊(duì)對(duì)于HBase高可用服務(wù)接口的設(shè)計(jì),同樣使用了熔斷的概念,只是其底層的熔斷技術(shù)依賴于java微服務(wù)中的Hystrix框架。其簡(jiǎn)單的客戶端高可用方案原理圖如下所示:

業(yè)務(wù)方是不想感知到后端服務(wù)的狀態(tài),也就是說在客戶端層面,他們只希望一個(gè) Put 或者 Get 請(qǐng)求正常送達(dá)且返回預(yù)期的數(shù)據(jù)即可,那么就需要高可用客戶端封裝一層降級(jí),熔斷處理的邏輯,這里有贊采用 Hystrix 做為底層熔斷處理引擎,在引擎之上封裝了 HBase 的基本 API,用戶只需要配置主備機(jī)房的 ZK 地址即可,所有的降級(jí)熔斷邏輯最終封裝到 ha-hbase-client 中。
以上文字描述摘選自有贊的技術(shù)博客,詳情可以參考鏈接,有贊 HBase 技術(shù)實(shí)踐:讀流程解析與優(yōu)化
五、熔斷在我們的HBase接口服務(wù)中的應(yīng)用
與微服務(wù)中的熔斷概念類比,我們也可以把我們的主備HBase集群看做是兩個(gè)獨(dú)立的服務(wù),而我們的業(yè)務(wù)方則需要依賴這一個(gè)HBase服務(wù),對(duì)外提供自己的服務(wù)。這里稍微有一點(diǎn)不一樣的地方是,我們HBase服務(wù)的角色是由兩個(gè)集群來擔(dān)任,正常情況下,只有一個(gè)集群來承擔(dān)起HBase服務(wù)的功能。HBase熔斷切換的簡(jiǎn)單示例如下:

- 正常狀態(tài)下APP的請(qǐng)求通過熔斷器只會(huì)落在主集群上
- 當(dāng)發(fā)生例如超時(shí)異常時(shí),在指定的窗口期內(nèi),錯(cuò)誤的請(qǐng)求數(shù)達(dá)到一定的閾值,熔斷器就會(huì)認(rèn)為,HBase主集群處于非正常狀態(tài),此時(shí),在服務(wù)的最小恢復(fù)時(shí)間內(nèi),所有的請(qǐng)求通過熔斷器,會(huì)落在備集群中。而熔斷器與主集群的通信鏈路則是被鎖定的。
- 過了指定的服務(wù)的最小恢復(fù)時(shí)間,還未到達(dá)服務(wù)的最大恢復(fù)時(shí)間時(shí),APP的請(qǐng)求會(huì)隨機(jī)落在主備集群,當(dāng)主集群的請(qǐng)求依舊異常時(shí),熔斷器會(huì)繼續(xù)鎖住與主集群的通信鏈路。直至?xí)r間達(dá)到服務(wù)的最大恢復(fù)時(shí)間,熔斷器才會(huì)繼續(xù)嘗試把請(qǐng)求落在主集群上。
六、HBase熔斷工作的流程圖
此處,我們以get請(qǐng)求舉例,用流程圖來演示我們的HBase查詢?nèi)蹟嗯c主備切換機(jī)制。

七、滾動(dòng)計(jì)數(shù)RollingNumber
如果想要更深入地理解主備熔斷切換的設(shè)計(jì)理念,那么,需要優(yōu)先理解一下滾動(dòng)窗口計(jì)數(shù),以及閾值判斷相關(guān)的一些內(nèi)容。doctor熔斷框架的設(shè)計(jì)中,依賴于滑動(dòng)窗口時(shí)間內(nèi)的滾動(dòng)計(jì)數(shù),來進(jìn)行閾值計(jì)算,從而判斷當(dāng)前服務(wù)的健康狀況。
1. 滾動(dòng)計(jì)數(shù)的概念
滾動(dòng)計(jì)數(shù)的行為類似于一個(gè)擁有固定長(zhǎng)度的先進(jìn)先出隊(duì)列,或者時(shí)間戳序列上的滑動(dòng)窗口。一個(gè)滾動(dòng)計(jì)數(shù)的值是隊(duì)列元素的和,時(shí)鐘結(jié)束時(shí),最后一個(gè)元素的值將滾動(dòng)到先前的位置,傳遞了一個(gè)時(shí)間粒度,這個(gè)時(shí)間粒度,默認(rèn)1s。下面將借助一個(gè)小例子,具體來說明這種機(jī)制。
示例中我們使用的滑動(dòng)窗口長(zhǎng)度為4,移位的時(shí)間戳粒度為1s??偟臅r(shí)間周期是20s。
- 初始時(shí)創(chuàng)建一個(gè)填充4個(gè)0元素的列表,這是整個(gè)滾動(dòng)計(jì)數(shù)行為的最開始的狀態(tài)。
[0, 0, 0, 0]
- 在第一個(gè)滑動(dòng)窗口的時(shí)間周期(4s)內(nèi),假如第一秒內(nèi)處理了3個(gè)請(qǐng)求,第二秒內(nèi)處理了2個(gè)請(qǐng)求,第三秒內(nèi)處理了5個(gè)請(qǐng)求,第四秒內(nèi)處理了4個(gè)請(qǐng)求,那么,此時(shí)列表中的狀態(tài)如下,需要計(jì)算的指標(biāo)從上至下依次為,
請(qǐng)求的總數(shù),失敗請(qǐng)求的總數(shù),失敗請(qǐng)求所占的比例
[3, 2, 5, 4]
+--- 14 ---+
+--- 7 ---+
+--- 0.5 ---+
# 則這一個(gè)滑動(dòng)窗口的時(shí)間周期內(nèi)的請(qǐng)求數(shù)總和為14,假如失敗的請(qǐng)求總數(shù)為7,那么,此時(shí)間周期內(nèi)的失敗比例為7 / 14 = 0.5
- 假如在第五秒處理的請(qǐng)求為4,滑動(dòng)窗口需要前移一個(gè)時(shí)間粒度,此時(shí)列表中的狀態(tài)如下:
3, [2, 5, 4, 4]
+--- 15 ---+
+--- 3 ---+
+--- 0.2 ---+
# 則這一個(gè)滑動(dòng)窗口的時(shí)間周期內(nèi)的請(qǐng)求數(shù)總和為15,假如失敗的請(qǐng)求總數(shù)為3,那么,此時(shí)間周期內(nèi)的失敗比例為3 / 15 = 0.2
- 依次類推,第m個(gè)時(shí)間周期內(nèi)
3, 2, 5, 4, 4,..., 6, [8, 2, 4, 4], 5 ... (<= time passing 20s)
+--- 18 ---+
+--- 6 ---+
+--- 0.33 ---+
# 則第m個(gè)滑動(dòng)窗口的時(shí)間周期內(nèi)的請(qǐng)求數(shù)總和為18,假如失敗的請(qǐng)求總數(shù)為6,那么,此時(shí)間周期內(nèi)的失敗比例為6 / 18 = 0.33
八、深入理解熔斷在我們HBase接口服務(wù)中的工作機(jī)制
1. HBase熔斷機(jī)制工作的核心參數(shù)
讀寫閾值判定的配置示例
READ_DOCTOR_CONF = dict(
# Metrics settings.
METRICS_GRANULARITY=1, # sec
METRICS_ROLLINGSIZE=10,
# Health settings.
HEALTH_MIN_RECOVERY_TIME=10, # sec
HEALTH_MAX_RECOVERY_TIME=2 * 10, # sec
HEALTH_THRESHOLD_REQUEST=5 * 1, # per `INTERVAL`
HEALTH_THRESHOLD_TIMEOUT=0.01, # percentage per `INTERVAL`
HEALTH_THRESHOLD_SYS_EXC=0.01, # percentage per `INTERVAL`
HEALTH_THRESHOLD_UNKWN_EXC=0.01, # percentage per `INTERVAL`
)
WRITE_DOCTOR_CONF = dict(
# Metrics settings.
METRICS_GRANULARITY=3, # sec
METRICS_ROLLINGSIZE=20,
# Health settings.
HEALTH_MIN_RECOVERY_TIME=20, # sec
HEALTH_MAX_RECOVERY_TIME=2 * 60, # sec
HEALTH_THRESHOLD_REQUEST=10 * 1, # per `INTERVAL`
HEALTH_THRESHOLD_TIMEOUT=0.5, # percentage per `INTERVAL`
HEALTH_THRESHOLD_SYS_EXC=0.5, # percentage per `INTERVAL`
HEALTH_THRESHOLD_UNKWN_EXC=0.5, # percentage per `INTERVAL`
)
核心參數(shù)解讀
- METRICS_GRANULARITY:滾動(dòng)計(jì)數(shù)中窗口移位的時(shí)間粒度
- METRICS_ROLLINGSIZE:滾動(dòng)計(jì)數(shù)中滑動(dòng)窗口的長(zhǎng)度
- HEALTH_MIN_RECOVERY_TIME:服務(wù)發(fā)生異常時(shí)的最小恢復(fù)時(shí)間
- HEALTH_MAX_RECOVERY_TIME:服務(wù)發(fā)生異常時(shí)的最大恢復(fù)時(shí)間
- HEALTH_THRESHOLD_REQUEST:此參數(shù)用于控制是否需要進(jìn)行錯(cuò)誤閾值計(jì)算
- HEALTH_THRESHOLD_TIMEOUT:發(fā)生超時(shí)異常的請(qǐng)求數(shù)與請(qǐng)求總數(shù)的比值,超過此設(shè)定,將觸發(fā)主備服務(wù)的熔斷切換
- HEALTH_THRESHOLD_SYS_EXC:發(fā)生系統(tǒng)異常的請(qǐng)求數(shù)與請(qǐng)求總數(shù)的比值,超過此設(shè)定,將觸發(fā)主備服務(wù)的熔斷切換
- HEALTH_THRESHOLD_UNKWN_EXC:發(fā)生未知異常的請(qǐng)求數(shù)與請(qǐng)求總數(shù)的比值,超過此設(shè)定,將觸發(fā)主備服務(wù)的熔斷切換
2. 判斷接口是否健康的策略
判斷當(dāng)前接口是否健康的詳細(xì)策略
- 如果當(dāng)前api(例如:getRow的一個(gè)操作)在讀/寫主集群時(shí)嚴(yán)重出錯(cuò),則會(huì)直接去從備集群中獲取結(jié)果,在配置中設(shè)定的服務(wù)最小恢復(fù)時(shí)間內(nèi),進(jìn)一步的請(qǐng)求不會(huì)再操作主集群。
- 如果主集群恢復(fù)健康,但是熔斷器此時(shí)并不知道主集群已經(jīng)恢復(fù)正常了,它的恢復(fù)機(jī)制是:在配置中設(shè)定的服務(wù)恢復(fù)的最小和最大時(shí)間之間,請(qǐng)求通過熔斷器,隨機(jī)去操作主備集群,如果期間訪問主集群的請(qǐng)求,有一次發(fā)生異常,熔斷器就會(huì)鎖住與主集群的通信鏈路,余下所有請(qǐng)求將會(huì)訪問備集群,繼續(xù)去等待最小間隔,然后開啟隨機(jī)訪問模式,直至達(dá)到配置設(shè)定的服務(wù)最大的恢復(fù)時(shí)間,熔斷器認(rèn)為主集群已恢復(fù)上線,之后的請(qǐng)求又會(huì)繼續(xù)操作主集群。
錯(cuò)誤閾值說明
- 接口錯(cuò)誤包括系統(tǒng)錯(cuò)誤與操作的超時(shí)
- 閾值是百分比,錯(cuò)誤請(qǐng)求數(shù)/總的請(qǐng)求數(shù)
- 當(dāng)一個(gè)滑動(dòng)窗口時(shí)間內(nèi)的請(qǐng)求計(jì)數(shù)大于配置設(shè)定的THRESHOLD_REQUEST時(shí),才會(huì)觸發(fā)進(jìn)一步的錯(cuò)誤閾值檢查,然后,如果錯(cuò)誤閾值大于設(shè)定的比例時(shí),才會(huì)觸發(fā)最終的熔斷切換。
3. 健康檢查
- 每一次請(qǐng)求經(jīng)過熔斷器,都會(huì)觸發(fā)健康檢查。
九、總結(jié)
上述便是對(duì)HBase熔斷思想所做的一個(gè)由淺入深的解釋,用于實(shí)現(xiàn)業(yè)務(wù)方訪問HBase時(shí),對(duì)于主備HBase集群的狀態(tài)切換無感知。即使主集群處于異常狀態(tài),我們依舊可以為業(yè)務(wù)方提供正常的HBase服務(wù)。