服務(wù)容錯概述
本文主要參考Netflix Hystrix機(jī)制潮剪。
服務(wù)容錯機(jī)制即防御性編程,Design For Failure,核心思想如下:
- 單一服務(wù)/節(jié)點(diǎn)的故障不會嚴(yán)重破壞用戶的體驗(yàn):降級、隔離、超時等
- 系統(tǒng)具備自動或者半自動恢復(fù)的能力:多活重試擅这、熔斷器、限流等
超時/重試
在分布式服務(wù)調(diào)用的場景中景鼠,主要解決了當(dāng)依賴服務(wù)出現(xiàn)網(wǎng)絡(luò)連接或響應(yīng)延遲時仲翎,當(dāng)前服務(wù)不用無限等待的問題。
可以通過設(shè)置RPC、Redis谭确、DB等調(diào)用的超時時間(一般可以設(shè)置為服務(wù)響應(yīng)的99分位),及時釋放關(guān)鍵資源票渠,避免耗盡系統(tǒng)資源逐哈,導(dǎo)致系統(tǒng)不可用。
超時傳遞:從網(wǎng)關(guān)入口進(jìn)行傳遞鏈路超時问顷,以及請求消耗后的超時昂秃;
- RPC中通過Metadata傳遞
- HTTP接口通過header傳遞,然后通過語言內(nèi)超時機(jī)制保證接口的超時處理杜窄;
重試:一般和超時結(jié)合使用肠骆,主要用于對下游服務(wù)有強(qiáng)依賴的場景,通過重試增加數(shù)據(jù)的可靠性塞耕。同步重試次數(shù)不宜過多蚀腿,避免影響接口性能。
限流
限流主要用于下游服務(wù)容量有限扫外,在面對流量激增(惡意刷子或者節(jié)日大促)時候壓力過大導(dǎo)致拒絕服務(wù)的場景:通過犧牲一部分人莉钙,來保證大部分人的體驗(yàn)(服務(wù)整體可用)。
常見的限流分為:
- 控制并發(fā)數(shù):連接池筛谚、線程池等
- 控制流量:漏洞和令牌桶
漏桶:由于恒定速率處理請求磁玉,對于突發(fā)流量不是很友好。
相當(dāng)于請求先進(jìn)入到桶中(等待隊列)驾讲,當(dāng)桶滿了以后開始丟棄請求蚊伞;
水以恒定的速度從桶中流出(處理)請求。
令牌桶
- 和漏桶一樣吮铭,基于固定速率放入时迫,但是由于令牌桶是允許任意速度取出的,所以可以容忍瞬間的流量激增:但是總的時間窗口內(nèi)的令牌數(shù)是固定的
- 假設(shè)限制X的QPS谓晌,桶的大小為單位時間內(nèi)允許的最大流量别垮,即這里的X(單位時間這里是秒)
- 可以分為初始值為空桶或者滿的桶兩種做法
- 以1/X的速率向桶中放入令牌,桶滿了則無法放入
-
每個請求拿走1個令牌扎谎,所以單位時間內(nèi)最多拿走桶的大小個令牌碳想,達(dá)到限流目的
令牌桶
分布式限流
關(guān)鍵為限流操作需要為原子化,可以使用redis+lua腳本來保證原子性毁靶。
-- 令牌桶限流: 不支持預(yù)消費(fèi), 初始桶是滿的
-- KEYS[1] string 限流的key
-- ARGV[1] int 桶最大容量
-- ARGV[2] int 每次添加令牌數(shù)
-- ARGV[3] int 令牌添加間隔(秒)
-- ARGV[4] int 當(dāng)前時間戳
local bucket_capacity = tonumber(ARGV[1])
local add_token = tonumber(ARGV[2])
local add_interval = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
-- 保存上一次更新桶的時間的key
local LAST_TIME_KEY = KEYS[1].."_time";
-- 獲取當(dāng)前桶中令牌數(shù)
local token_cnt = redis.call("get", KEYS[1])
-- 桶完全恢復(fù)需要的最大時長
local reset_time = math.ceil(bucket_capacity / add_token) * add_interval;
if token_cnt then -- 令牌桶存在
-- 上一次更新桶的時間
local last_time = redis.call('get', LAST_TIME_KEY)
-- 恢復(fù)倍數(shù)
local multiple = math.floor((now - last_time) / add_interval)
-- 恢復(fù)令牌數(shù)
local recovery_cnt = multiple * add_token
-- 確保不超過桶容量
local token_cnt = math.min(bucket_capacity, token_cnt + recovery_cnt) - 1
if token_cnt < 0 then
return -1;
end
-- 重新設(shè)置過期時間, 避免key過期
redis.call('set', KEYS[1], token_cnt, 'EX', reset_time)
redis.call('set', LAST_TIME_KEY, last_time + multiple * add_interval, 'EX', reset_time)
return token_cnt
else -- 令牌桶不存在
token_cnt = bucket_capacity - 1
-- 設(shè)置過期時間避免key一直存在
redis.call('set', KEYS[1], token_cnt, 'EX', reset_time);
redis.call('set', LAST_TIME_KEY, now, 'EX', reset_time + 1);
return token_cnt
end
偽代碼邏輯:
如果令牌桶存在(key A)
- 獲取上次桶放入令牌的時間(key B)和當(dāng)前時間的差距(比如50ms)胧奔,則放入50*1/X個令牌(加上并且判斷不超過容量X)
- 從桶中拿走一個令牌,判斷是否被限流
桶不存在:則設(shè)置key A = X-1(容量-本次使用)预吆,key B=當(dāng)前時間龙填,EX時間均為時間窗口,即1s
熔斷器
在工程實(shí)踐中,由于一些系統(tǒng)異逞乙牛或者網(wǎng)絡(luò)異常導(dǎo)致的調(diào)用失敗扇商,可能需要一段時間才能恢復(fù)。
而這段時間內(nèi)的請求會占用寶貴的系統(tǒng)資源宿礁,并且由于持續(xù)失敗可能將資源消耗殆盡(比如db案铺、redis連接池),從而導(dǎo)致系統(tǒng)不可用梆靖。
所以此時能夠立即返回錯誤而不是等待超時是更好的選擇 => 熔斷器
關(guān)鍵參數(shù)配置
window=“10s” - 即整個滑動窗口的大小
bucket=10 - 即bucket的數(shù)量控汉,用window/bucket可以得到單個bucket的大小為1s;滑動窗口以bucket為單位進(jìn)行滑動返吻;
ratio=0.5 - 即滑動窗口內(nèi)統(tǒng)計的總錯誤率到達(dá)50%時觸發(fā)熔斷閾值
request=100 - 即當(dāng)滑動窗口內(nèi)的請求數(shù)量過小時姑子,暫不觸發(fā)熔斷,最小值100
sleep=100ms - 即熔斷器從打開到半打開的時長
熔斷器工作機(jī)制
- closed -> open:滑動窗口內(nèi)request > 100 && ratio > 0.5
- open -> half open:sleep 100ms后测僵,進(jìn)入半打開狀態(tài)
- half open -> closed/open:半開后會放一個請求去執(zhí)行街佑,如果失敗則繼續(xù)open,如果成功則熔斷器關(guān)閉
船艙隔離
在造船行業(yè)捍靠,往往使用此類模式對船艙進(jìn)行隔離舆乔,利用艙壁將不同的船艙隔離起來,這樣如果一個船艙破了進(jìn)水剂公,只損失一個船艙希俩,其它船艙可以不受影響。
而借鑒造船行業(yè)的經(jīng)驗(yàn)纲辽,在微服務(wù)架構(gòu)中為每個服務(wù)單獨(dú)設(shè)置資源颜武,用盡后不影響其他服務(wù):如db隔離、redis隔離拖吼、容器隔離
Fallback 回退/降級
當(dāng)請求失敗/超時/被熔斷/被限流后鳞上,會進(jìn)入Fallback邏輯:
降級邏輯:返回備用數(shù)據(jù),默認(rèn)數(shù)據(jù)吊档,本地數(shù)據(jù)(客戶端緩存)等
故障沉默 Fail-Silent:直接返回空值篙议,相應(yīng)模塊不展示(比如商品的相關(guān)推薦)
快速失敗 Fail-Fast:對于非強(qiáng)依賴的場景則直接報錯(不影響體驗(yàn)的前提下)