微服務(wù)如何保障穩(wěn)定性?
點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)
作者:fredalxin
地址:https://fredal.xin/talking-msa-msa-stability
當(dāng)一個(gè)單體應(yīng)用改造成多個(gè)微服務(wù)之后,在請(qǐng)求調(diào)用過(guò)程中往往會(huì)出現(xiàn)更多的問(wèn)題,通信過(guò)程中的每一個(gè)環(huán)節(jié)都可能出現(xiàn)問(wèn)題。而在出現(xiàn)問(wèn)題之后,如果不加處理,還會(huì)出現(xiàn)鏈?zhǔn)椒磻?yīng)導(dǎo)致服務(wù)雪崩。服務(wù)治理功能就是用來(lái)處理此類(lèi)問(wèn)題的。
我們將從微服務(wù)的三個(gè)角色:注冊(cè)中心、服務(wù)消費(fèi)者以及服務(wù)提供者一一說(shuō)起。
注冊(cè)中心如何保障穩(wěn)定性
這一節(jié),我們著重講的并不是注冊(cè)中心自身可用性保證,而更多的是與節(jié)點(diǎn)狀態(tài)相關(guān)的部分。
節(jié)點(diǎn)信息的保障
我們說(shuō)過(guò),當(dāng)注冊(cè)中心完全宕機(jī)后,微服務(wù)框架仍然需要有正常工作的能力。這得益于框架內(nèi)處理節(jié)點(diǎn)狀態(tài)的一些機(jī)制。
本機(jī)內(nèi)存
首先服務(wù)消費(fèi)者會(huì)將節(jié)點(diǎn)狀態(tài)保持在本機(jī)內(nèi)存中。一方面由于節(jié)點(diǎn)狀態(tài)不會(huì)變更得那么頻繁,放在內(nèi)存中可以減少網(wǎng)絡(luò)開(kāi)銷(xiāo)。另一方面,當(dāng)注冊(cè)中心宕機(jī)后,服務(wù)消費(fèi)者仍能從本機(jī)內(nèi)存中找到服務(wù)節(jié)點(diǎn)列表從而發(fā)起調(diào)用。
本地快照
我們說(shuō),注冊(cè)中心宕機(jī)后,服務(wù)消費(fèi)者仍能從本機(jī)內(nèi)存中找到服務(wù)節(jié)點(diǎn)列表。那么如果服務(wù)消費(fèi)者重啟了呢?這時(shí)候我們就需要一份本地快照了,即我們保存一份節(jié)點(diǎn)狀態(tài)到本地文件,每次重啟之后會(huì)恢復(fù)到本機(jī)內(nèi)存中。
服務(wù)節(jié)點(diǎn)的摘除
現(xiàn)在無(wú)論注冊(cè)中心工作與否,我們都能順利拿到服務(wù)節(jié)點(diǎn)了。但是不是所有的服務(wù)節(jié)點(diǎn)都是正確可用的呢?在實(shí)際應(yīng)用中,這是需要打問(wèn)號(hào)的。如果我們不校驗(yàn)服務(wù)節(jié)點(diǎn)的正確性,很有可能就調(diào)用到了一個(gè)不正常的節(jié)點(diǎn)上。所以我們需要進(jìn)行必要的節(jié)點(diǎn)管理。
對(duì)于節(jié)點(diǎn)管理來(lái)說(shuō),我們有兩種手段,主要是去摘除不正確的服務(wù)節(jié)點(diǎn)。
注冊(cè)中心摘除機(jī)制
一是通過(guò)注冊(cè)中心來(lái)進(jìn)行摘除節(jié)點(diǎn)。服務(wù)提供者會(huì)與注冊(cè)中心保持心跳,而一旦超出一定時(shí)間收不到心跳包,注冊(cè)中心就認(rèn)為該節(jié)點(diǎn)出現(xiàn)了問(wèn)題,會(huì)把節(jié)點(diǎn)從服務(wù)列表中摘除,并通知到服務(wù)消費(fèi)者,這樣服務(wù)消費(fèi)者就不會(huì)調(diào)用到有問(wèn)題的節(jié)點(diǎn)上。
服務(wù)消費(fèi)者摘除機(jī)制
二是在服務(wù)消費(fèi)者這邊拆除節(jié)點(diǎn)。因?yàn)榉?wù)消費(fèi)者自身是最知道節(jié)點(diǎn)是否可用的角色,所以在服務(wù)消費(fèi)者這邊做判斷更合理,如果服務(wù)消費(fèi)者調(diào)用出現(xiàn)網(wǎng)絡(luò)異常,就將該節(jié)點(diǎn)從內(nèi)存緩存列表中摘除。當(dāng)然調(diào)用失敗多少次之后才進(jìn)行摘除,以及摘除恢復(fù)的時(shí)間等等細(xì)節(jié),其實(shí)都和客戶(hù)端熔斷類(lèi)似,可以結(jié)合起來(lái)做。
一般來(lái)說(shuō),對(duì)于大流量應(yīng)用,服務(wù)消費(fèi)者摘除的敏感度會(huì)高于注冊(cè)中心摘除,兩者之間也不用刻意做同步判斷,因?yàn)檫^(guò)一段時(shí)間后注冊(cè)中心摘除會(huì)自動(dòng)覆蓋服務(wù)消費(fèi)者摘除。
服務(wù)節(jié)點(diǎn)是可以隨便摘除/變更的么
頻繁變動(dòng)
增量更新
同樣是由于頻繁變動(dòng)可能引起的網(wǎng)絡(luò)風(fēng)暴問(wèn)題,一個(gè)可行的方案是進(jìn)行增量更新,注冊(cè)中心只會(huì)推送那些變化的節(jié)點(diǎn)信息而不是全部,從而在頻繁變動(dòng)的時(shí)候避免網(wǎng)絡(luò)風(fēng)暴。
可用節(jié)點(diǎn)過(guò)少
當(dāng)網(wǎng)絡(luò)抖動(dòng),并進(jìn)行節(jié)點(diǎn)摘除過(guò)后,很可能出現(xiàn)可用節(jié)點(diǎn)過(guò)少的情況。這時(shí)候過(guò)大的流量分配給過(guò)少的節(jié)點(diǎn),導(dǎo)致剩下的節(jié)點(diǎn)難堪重負(fù),罷工不干,引起惡化。而實(shí)際上,可能節(jié)點(diǎn)大多數(shù)是可用的,只不過(guò)由于網(wǎng)絡(luò)問(wèn)題與注冊(cè)中心未能及時(shí)保持心跳而已。
這時(shí)候,就需要在服務(wù)消費(fèi)者這邊設(shè)置一個(gè)開(kāi)關(guān)比例閾值,當(dāng)注冊(cè)中心通知節(jié)點(diǎn)摘除,但緩存列表中剩下的節(jié)點(diǎn)數(shù)低于一定比例后(與之前一段時(shí)間相比),不再進(jìn)行摘除,從而保證有足夠的節(jié)點(diǎn)提供正常服務(wù)。
這個(gè)值其實(shí)可以設(shè)置的高一些,例如百分之70,因?yàn)檎G闆r下不會(huì)有頻繁的網(wǎng)絡(luò)抖動(dòng)。當(dāng)然,如果開(kāi)發(fā)者確實(shí)需要下線多數(shù)節(jié)點(diǎn),可以關(guān)閉該開(kāi)關(guān)。
服務(wù)消費(fèi)者如何保障穩(wěn)定性
一個(gè)請(qǐng)求失敗了,最直接影響到的是服務(wù)消費(fèi)者,那么在服務(wù)消費(fèi)者這邊,有什么可以做的呢?
超時(shí)
如果調(diào)用一個(gè)接口,但遲遲沒(méi)有返回響應(yīng)的時(shí)候,我們往往需要設(shè)置一個(gè)超時(shí)時(shí)間,以防自己被遠(yuǎn)程調(diào)用拖死。超時(shí)時(shí)間的設(shè)置也是有講究的,設(shè)置的太長(zhǎng)起的作用就小,自己被拖垮的風(fēng)險(xiǎn)就大,設(shè)置的太短又有可能誤判一些正常請(qǐng)求,大幅提升錯(cuò)誤率。
在實(shí)際使用中,我們可以取該應(yīng)用一段時(shí)間內(nèi)的P999的值,或者取p95的值*2。具體情況需要自行定奪。
在超時(shí)設(shè)置的時(shí)候,對(duì)于同步與異步的接口也是有區(qū)分的。對(duì)于同步接口,超時(shí)設(shè)置的值不僅需要考慮到下游接口,還需要考慮上游接口。而對(duì)于異步來(lái)說(shuō),由于接口已經(jīng)快速返回,可以不用考慮上游接口,只需考慮自身在異步線程里的阻塞時(shí)長(zhǎng),所以超時(shí)時(shí)間也放得更寬一些。
容錯(cuò)機(jī)制
FailTry:失敗重試。就是指最常見(jiàn)的重試機(jī)制,當(dāng)請(qǐng)求失敗后視圖再次發(fā)起請(qǐng)求進(jìn)行重試。這樣從概率上講,失敗率會(huì)呈指數(shù)下降。對(duì)于重試次數(shù)來(lái)說(shuō),也需要選擇一個(gè)恰當(dāng)?shù)闹?,如果重試次?shù)太多,就有可能引起服務(wù)惡化。另外,結(jié)合超時(shí)時(shí)間來(lái)說(shuō),對(duì)于性能有要求的服務(wù),可以在超時(shí)時(shí)間到達(dá)前的一段提前量就發(fā)起重試,從而在概率上優(yōu)化請(qǐng)求調(diào)用。當(dāng)然,重試的前提是冪等操作。 FailOver:失敗切換。和上面的策略類(lèi)似,只不過(guò)FailTry會(huì)在當(dāng)前實(shí)例上重試。而FailOver會(huì)重新在可用節(jié)點(diǎn)列表中根據(jù)負(fù)載均衡算法選擇一個(gè)節(jié)點(diǎn)進(jìn)行重試。 FailFast:快速失敗。請(qǐng)求失敗了就直接報(bào)一個(gè)錯(cuò),或者記錄在錯(cuò)誤日志中,這沒(méi)什么好說(shuō)的。
另外,還有很多形形色色的容錯(cuò)機(jī)制,大多是基于自己的業(yè)務(wù)特性定制的,主要是在重試上做文章,例如每次重試等待時(shí)間都呈指數(shù)增長(zhǎng)等。
第三方框架也都會(huì)內(nèi)置默認(rèn)的容錯(cuò)機(jī)制,例如Ribbon的容錯(cuò)機(jī)制就是由retry以及retry next組成,即重試當(dāng)前實(shí)例與重試下一個(gè)實(shí)例。這里要多說(shuō)一句,ribbon的重試次數(shù)與重試下一個(gè)實(shí)例次數(shù)是以笛卡爾乘積的方式提供的噢!
Spring Boot 學(xué)習(xí)教程推薦:https://github.com/javastacks/spring-boot-best-practice
熔斷
上一節(jié)將的容錯(cuò)機(jī)制,主要是一些重試機(jī)制,對(duì)于偶然因素導(dǎo)致的錯(cuò)誤比較有效,例如網(wǎng)絡(luò)原因。但如果錯(cuò)誤的原因是服務(wù)提供者自身的故障,那么重試機(jī)制反而會(huì)引起服務(wù)惡化。
這時(shí)候我們需要引入一種熔斷的機(jī)制,即在一定時(shí)間內(nèi)不再發(fā)起調(diào)用,給予服務(wù)提供者一定的恢復(fù)時(shí)間,等服務(wù)提供者恢復(fù)正常后再發(fā)起調(diào)用。這種保護(hù)機(jī)制大大降低了鏈?zhǔn)疆惓R鸬姆?wù)雪崩的可能性。
在實(shí)際應(yīng)用中,熔斷器往往分為三種狀態(tài),打開(kāi)、半開(kāi)以及關(guān)閉。引用一張martinfowler畫(huà)的原理圖:

在普通情況下,斷路器處于關(guān)閉狀態(tài),請(qǐng)求可以正常調(diào)用。當(dāng)請(qǐng)求失敗達(dá)到一定閾值條件時(shí),則打開(kāi)斷路器,禁止向服務(wù)提供者發(fā)起調(diào)用。當(dāng)斷路器打開(kāi)后一段時(shí)間,會(huì)進(jìn)入一個(gè)半開(kāi)的狀態(tài),此狀態(tài)下的請(qǐng)求如果調(diào)用成功了則關(guān)閉斷路器,如果沒(méi)有成功則重新打開(kāi)斷路器,等待下一次半開(kāi)狀態(tài)周期。
斷路器的實(shí)現(xiàn)中比較重要的一點(diǎn)是失敗閾值的設(shè)置??梢愿鶕?jù)業(yè)務(wù)需求設(shè)置失敗的條件為連續(xù)失敗的調(diào)用次數(shù),也可以是時(shí)間窗口內(nèi)的失敗比率,失敗比率通過(guò)一定的滑動(dòng)窗口算法進(jìn)行計(jì)算。另外,針對(duì)斷路器的半開(kāi)狀態(tài)周期也可以做一些花樣,一種常見(jiàn)的計(jì)算方法是周期長(zhǎng)度隨著失敗次數(shù)呈指數(shù)增長(zhǎng)。
具體的實(shí)現(xiàn)方式可以根據(jù)具體業(yè)務(wù)指定,也可以選擇第三方框架例如Hystrix。Hystrix理論+實(shí)戰(zhàn)推薦看下。
隔離
隔離往往和熔斷結(jié)合在一起使用,還是以Hystrix為例,它提供了兩種隔離方式:
信號(hào)量隔離:使用信號(hào)量來(lái)控制隔離線程,你可以為不同的資源設(shè)置不同的信號(hào)量以控制并發(fā),并相互隔離。當(dāng)然實(shí)際上,使用原子計(jì)數(shù)器也沒(méi)什么不一樣。 線程池隔離:通過(guò)提供相互隔離的線程池的方式來(lái)隔離資源,相對(duì)來(lái)說(shuō)消耗資源更多,但可以更好地應(yīng)對(duì)突發(fā)流量。
降級(jí)
降級(jí)同樣大多和熔斷結(jié)合在一起使用,當(dāng)服務(wù)調(diào)用者這方斷路器打開(kāi)后,無(wú)法再對(duì)服務(wù)提供者發(fā)起調(diào)用了,這時(shí)候可以通過(guò)返回降級(jí)數(shù)據(jù)來(lái)避免熔斷造成的影響。
降級(jí)往往用于那些錯(cuò)誤容忍度較高的業(yè)務(wù)。同時(shí)降級(jí)的數(shù)據(jù)如何設(shè)置也是一門(mén)學(xué)問(wèn)。一種方法是為每個(gè)接口預(yù)先設(shè)置好可接受的降級(jí)數(shù)據(jù),但這種靜態(tài)降級(jí)的方法適用性較窄。還有一種方法,是去線上日志系統(tǒng)/流量錄制系統(tǒng)中撈取上一次正確的返回?cái)?shù)據(jù)作為本次降級(jí)數(shù)據(jù),但這種方法的關(guān)鍵是提供可供穩(wěn)定抓取請(qǐng)求的日志系統(tǒng)或者流量采樣錄制系統(tǒng)。
另外,針對(duì)降級(jí)我們往往還會(huì)設(shè)置操作開(kāi)關(guān),對(duì)于一些影響不大的采取自動(dòng)降級(jí),而對(duì)于一些影響較大的則需進(jìn)行人為干預(yù)降級(jí)。
服務(wù)提供者如何保障穩(wěn)定性
限流
限流就是限制服務(wù)請(qǐng)求流量,服務(wù)提供者可以根據(jù)自身情況(容量)給請(qǐng)求設(shè)置一個(gè)閾值,當(dāng)超過(guò)這個(gè)閾值后就丟棄請(qǐng)求,這樣就保證了自身服務(wù)的正常運(yùn)行。
閾值的設(shè)置可以針對(duì)兩個(gè)方面考慮,一是QPS即每秒請(qǐng)求數(shù),二是并發(fā)線程數(shù)。從實(shí)踐來(lái)看,我們往往會(huì)選擇后者,因?yàn)镼PS高往往是由于處理能力高,并不能反映出系統(tǒng)"不堪重負(fù)"。
除此之外,我們還有許多針對(duì)限流的算法。例如令牌桶算法以及漏桶算法,主要針對(duì)突發(fā)流量的狀況做了優(yōu)化。第三方的實(shí)現(xiàn)中例如guava rateLimiter就實(shí)現(xiàn)了令牌桶算法。在此就不就細(xì)節(jié)展開(kāi)了。
重啟與回滾
限流更多的起到一種保障的作用,但如果服務(wù)提供者已經(jīng)出現(xiàn)問(wèn)題了,這時(shí)候該怎么辦呢?
這時(shí)候就會(huì)出現(xiàn)兩種狀況。一是本身代碼有bug,這時(shí)候一方面需要服務(wù)消費(fèi)者做好熔斷降級(jí)等操作,一方面服務(wù)提供者這邊結(jié)合DevOps需要有快速回滾到上一個(gè)正確版本的能力。
更多的時(shí)候,我們可能僅僅碰到了與代碼無(wú)強(qiáng)關(guān)聯(lián)的單機(jī)故障,一個(gè)簡(jiǎn)單粗暴的辦法就是自動(dòng)重啟。例如觀察到某個(gè)接口的平均耗時(shí)超出了正常范圍一定程度,就將該實(shí)例進(jìn)行自動(dòng)重啟。當(dāng)然自動(dòng)重啟需要有很多注意事項(xiàng),例如重啟時(shí)間是否放在晚上,以及自動(dòng)重啟引起的與上述節(jié)點(diǎn)摘除一樣的問(wèn)題,都需要考慮和處理。
在事后復(fù)盤(pán)的時(shí)候,如果當(dāng)時(shí)沒(méi)有保護(hù)現(xiàn)場(chǎng),就很難定位到問(wèn)題原因。所以往往在一鍵回滾或者自動(dòng)重啟之前,我們往往需要進(jìn)行現(xiàn)場(chǎng)保護(hù)?,F(xiàn)場(chǎng)保護(hù)可以是自動(dòng)的,例如一開(kāi)始就給jvm加上打印gc日志的參數(shù)-XX:+PrintGCDetails,或者輸出oom文件-XX:+HeapDumpOnOutOfMemoryError,也可以配合DevOps自動(dòng)腳本完成,當(dāng)然手動(dòng)也可以。一般來(lái)說(shuō)我們會(huì)如下操作:
打印堆棧信息, jstak -l 'java進(jìn)程PID'打印內(nèi)存鏡像, jmap -dump:format=b,file=hprof 'java進(jìn)程PID'保留gc日志,保留業(yè)務(wù)日志
調(diào)度流量
除了以上這些措施,通過(guò)調(diào)度流量來(lái)避免調(diào)用到問(wèn)題節(jié)點(diǎn)上也是非常常用的手段。
當(dāng)服務(wù)提供者中的一臺(tái)機(jī)器出現(xiàn)問(wèn)題,而其他機(jī)器正常時(shí),我們可以結(jié)合負(fù)載均衡算法迅速調(diào)整該機(jī)器的權(quán)重至0,避免流量流入,再去機(jī)器上進(jìn)行慢慢排查,而不用著急第一時(shí)間重啟。
如果服務(wù)提供者分了不同集群/分組,當(dāng)其中一個(gè)集群出現(xiàn)問(wèn)題時(shí),我們也可以通過(guò)路由算法將流量路由到正常的集群中。這時(shí)候一個(gè)集群就是一個(gè)微服務(wù)分組。
而當(dāng)機(jī)房炸了、光纜被偷了等IDC故障時(shí),我們又部署了多IDC,也可以通過(guò)一些方式將流量切換到正常的IDC,以供服務(wù)繼續(xù)正常運(yùn)行。切換流量同樣可以通過(guò)微服務(wù)的路由實(shí)現(xiàn),但這時(shí)候一個(gè)IDC對(duì)應(yīng)一個(gè)微服務(wù)分組了。除此之外,使用DNS解析進(jìn)行流量切換也是可以的,將對(duì)外域名的VIP從一個(gè)IDC切換到另一個(gè)IDC。
最后,關(guān)注公眾號(hào)Java技術(shù)棧,在后臺(tái)回復(fù):面試,可以獲取我整理的 Java、微服務(wù)系列面試題和答案,非常齊全。






關(guān)注Java技術(shù)??锤喔韶?/strong>


