系統(tǒng)設(shè)計:微服務(wù)重試機制
為什么微服務(wù)重試機制很重要?
當(dāng)我們單體應(yīng)用時,所有的邏輯計算都在單一的進(jìn)程中,除了進(jìn)程斷電外幾乎不可能有處理失敗的情況。然而,當(dāng)我們把單體應(yīng)用拆分為一個個細(xì)分的子服務(wù)后,服務(wù)間的互相調(diào)用無論是RPC還是HTTP,都是依賴于網(wǎng)絡(luò)。
網(wǎng)絡(luò)是脆弱的,不時請求會出現(xiàn)抖動失敗。例如我們的 Server1 調(diào)用 Server2 進(jìn)行下單時,可能網(wǎng)絡(luò)超時了,這個時候 Server1 就需要返回給用戶提示「網(wǎng)絡(luò)錯誤」,這樣我們的服務(wù)質(zhì)量就下降了,可能會收到用戶的投訴吐槽,降低產(chǎn)品競爭力。
這也是為什么很多產(chǎn)品內(nèi)部都建設(shè)接口維度的 SLA 指標(biāo),當(dāng)成功率低于一定程度時需要和負(fù)責(zé)人績效掛鉤以此來推進(jìn)產(chǎn)品的穩(wěn)定性。
對于網(wǎng)絡(luò)抖動這種情況,解決的最簡單辦法之一就是重試。
重試機制
重試機制:同步 、異步模式
常見的重試主要有兩種模式:原地重試、異步重試。
原地重試很好理解,就是程序在調(diào)用下游服務(wù)失敗的時候重新發(fā)起一次;異步重試是將請求信息丟到某個 mq 中,后續(xù)有一個程序消費到這個事件進(jìn)行重試。
總的來說,原地重試實現(xiàn)簡單,能解決大部分網(wǎng)絡(luò)抖動問題,但是如果是服務(wù)追求強一致性,并且希望在下游故障的時候不影響正常服務(wù)計算,這個時候可以考慮用異步重試,上游服務(wù)可快速響應(yīng)用戶請求由異步消費者去完成重試。
重試算法
無論是異步還是同步模式,重試都有固定的幾個算法:
- 線性退避:每次失敗固定等待固定的時間。
- 隨機退避:每次失敗等待隨機的時間重試。
- 指數(shù)退避:連續(xù)重試時,每次等待的時間都是前一次等待時間的倍數(shù)。
- 綜合退避:結(jié)合多種方式,比如線性 + 隨機抖動、指數(shù) + 隨機抖動。加上隨機抖動可以打散眾多服務(wù)失敗時對下游的重試請求,防止雪崩。
為什么需要等待下再重試?
因為網(wǎng)絡(luò)抖動或者下游負(fù)載高,馬上重試成功的概率必然遠(yuǎn)遠(yuǎn)小于稍等一會再重試,相當(dāng)于是讓下游先喘一口氣。
重試風(fēng)暴
在微服務(wù)架構(gòu)中,務(wù)必要注意避免重試風(fēng)暴的產(chǎn)生。那么,什么是重試風(fēng)暴呢?
如圖所示,數(shù)據(jù)庫出現(xiàn)了負(fù)載過高的情況,這個時候 Server 3 對它的請求會失敗。但是因為配置了重試機制,Server 3 最多對數(shù)據(jù)庫發(fā)起了3次請求。然而,這個時候荒唐的事情就出現(xiàn)了,為了避免抖動上游的每個服務(wù)都設(shè)置了超時重試3次的機制,這樣明明是一次業(yè)務(wù)請求,在上述中由于有3個環(huán)節(jié)存在變成了對數(shù)據(jù)庫的 27 (3 ^(n)) 次請求!這對原本就要崩潰的數(shù)據(jù)庫,更是雪上加霜。
微服務(wù)架構(gòu)通常一次請求會經(jīng)過數(shù)個甚至數(shù)百個服務(wù)處理,如果每個都這樣重試,數(shù)據(jù)庫壓力稍微彪高一點本身沒啥問題,但是很可能就因為重試導(dǎo)致雪崩。
如何防止重試風(fēng)暴
單實例限流
首先,我們接受請求的是單個實例(進(jìn)程)中的線程,所以可以以單進(jìn)程的粒度進(jìn)行限流。
關(guān)于限流,我們常用的是令牌桶或者滑動窗口兩種實現(xiàn),這里簡單實用滑動窗口實現(xiàn)。如下圖所示,每秒會產(chǎn)生一個Bucket,我們在Bucket里記錄這一秒內(nèi)對下游某個接口的成功、失敗數(shù)量。進(jìn)而可以統(tǒng)計出每秒的失敗率,結(jié)合失敗率及失敗請求數(shù)判斷是否需要重試,每個 Bucket 在一定時間后過期。
如果下游大面積失敗,這種時候是不適合重試的,我們可以配置一個比如失敗率超過10%不重試的策略,這樣在單機層面就可以避免很多不必要的重試。
規(guī)范重試狀態(tài)碼
鏈路層面防止重試的最好做法是只在最下游重試(我們上面圖的 Server3),Google SRE中指出了Google內(nèi)部使用特殊錯誤碼的方式來實現(xiàn):
- 約定一個特殊的業(yè)務(wù)狀態(tài)碼,它表示失敗了,但是別重試。
- 任何一個環(huán)節(jié)收到下游這個錯誤,不會重試,繼續(xù)透傳給上游。
通過這個模式,如果是數(shù)據(jù)庫抖動情況下,只有最下游的三個重試請求,上游服務(wù)判斷狀態(tài)碼知道不可重試不再重試。除此之外,在一些業(yè)務(wù)異常情況下也可通過狀態(tài)碼區(qū)分出無需重試的狀態(tài)。
這個方法可以有效避免重試風(fēng)暴,但是缺陷是需要業(yè)務(wù)方強耦合上這個狀態(tài)碼的邏輯,一般需要公司層面做框架上的約束。
超時優(yōu)化
在重試中,最頭疼的莫過于超時這種場景。我們知道網(wǎng)絡(luò)超時,有可能請求壓根沒到下游服務(wù)就產(chǎn)生了,也可能是已經(jīng)到達(dá)下游并且被處理了,只是來不及返回,一個典型的兩軍問題。
關(guān)于超時的情況,顯然無法通過錯誤碼識別,例如 A -> B -> C -> D 情況,如果C故障了,B可以獲取到錯誤碼,并返回給 A,但是因為 A 請求 B 超時了,所以是獲取不到錯誤碼的,這個時候 A 又會發(fā)起重試。那么針對超時的情況有沒什么辦法做優(yōu)化,避免無必要的重試呢?
我認(rèn)為有幾個地方是可以做的:
上游重試的請求不重試
超時導(dǎo)致的重試請求,在請求中帶一個 Flag 標(biāo)記。如果下游發(fā)現(xiàn)上游是因為超時而發(fā)起的請求,自己在請求下游時如果再超時出錯,不再重試。例如 A -> B -> C 時,A 請求 B 超時重試,那么重試時會帶上 Flag,B 發(fā)現(xiàn) A 的重試請求中的 Flag,如果這個時候請求 C 失敗,那么也不再重試請求,這樣就避免了重試被放大。
合理設(shè)置各個環(huán)節(jié)超時時間
A -> B -> C,B -> C 加上超時最多是 1s 時間,那么 A -> B 的超時時間要 >= 1秒,否則可能 B 對 C 的重試還沒結(jié)束, A 就發(fā)起重試請求了。這類問題,我們可以通過分析離線數(shù)據(jù)發(fā)現(xiàn)環(huán)節(jié)中存在的不合理配置。
通過上述的優(yōu)化,我們可以在一定程度上規(guī)避超時引發(fā)的重試風(fēng)暴。
降低時延的重試
我們上文主要都在闡述為了保障請求 SLA 的重試以及規(guī)避重試風(fēng)暴的手段,但是其實在實際應(yīng)用過程中有一些低時延的業(yè)務(wù)場景也經(jīng)常使用重試來優(yōu)化,這個優(yōu)化措施就是 backupRequest。
比方說用戶下單接口,我們希望更低的時延,因為延遲變高了用戶可能下單量就減少了,直接影響到公司的盈利。假設(shè)我們的接口時延 p95 是 300ms,也就是95%的用戶能在 300ms 內(nèi)完成下單,雖然看起來很美好,但是可能存在 “長尾效應(yīng)”,這尾部的 5% 對于業(yè)務(wù)來說也是至關(guān)重要的。
對于這種情況,常見的優(yōu)化方案就是 backupRequest,簡單來說策略就是這樣的:
如果正常請求的超時時間是1s,那么當(dāng)超時時間超過x ms(eg. 500ms)不等超時時間直接再發(fā)起一個相同的請求,如果舊的請求超時,新的請求正常落在300ms以內(nèi),那么我們這次請求不會超時且會在超時時間內(nèi)完成。
這個機制對于時延敏感的業(yè)務(wù)非常有效,但是必須要保證請求是可重試的。
總結(jié)
這篇文章到這里就接近尾聲了,如果你堅持讀到這里,恭喜你已經(jīng)掌握了微服務(wù)的重試機制,相信在工作中遇到的問題也都能游刃有余。下面我簡單做下總結(jié):
- 微服務(wù)重試很重要,因為可以避免一些網(wǎng)絡(luò)波動導(dǎo)致的請求失敗,提升服務(wù)穩(wěn)定性。
- 重試機制分為同步、異步兩種模式,各有各的特性,需要結(jié)合業(yè)務(wù)選擇。
- 常見的重試算法有線性退避、指數(shù)退避、隨機退避,以及結(jié)合其中兩種的綜合退避。
- 重試風(fēng)暴,在微服務(wù)中是一大隱患,我們可以通過單機重試限流以及約定重試狀態(tài)碼來規(guī)避。
- 超時場景下的重試優(yōu)化,上游因超時發(fā)起的流量,下游收到不再重復(fù)重試;合理配置鏈路超時時間。
- 針對時延敏感業(yè)務(wù),可使用 backup request 減輕長尾效應(yīng)。