天天看點

微服務如何保障穩定性?

當一個單體應用改造成多個微服務之後,在請求調用過程中往往會出現更多的問題,通信過程中的每一個環節都可能出現問題。而在出現問題之後,如果不加處理,還會出現鍊式反應導緻服務雪崩。服務治理功能就是用來處理此類問題的。我們将從微服務的三個角色:注冊中心、服務消費者以及服務提供者一一說起。

注冊中心如何保障穩定性

注冊中心主要是負責節點狀态的維護,以及相應的變更探測與通知操作。一方面,注冊中心自身的穩定性是十分重要的。另一方面,我們也不能完全依賴注冊中心,需要時常進行類似注冊中心完全當機後微服務如何正常運作的故障演練。

這一節,我們着重講的并不是注冊中心自身可用性保證,而更多的是與節點狀态相關的部分。

節點資訊的保障

我們說過,當注冊中心完全當機後,微服務架構仍然需要有正常工作的能力。這得益于架構内處理節點狀态的一些機制。

本機記憶體

首先服務消費者會将節點狀态保持在本機記憶體中。一方面由于節點狀态不會變更得那麼頻繁,放在記憶體中可以減少網絡開銷。另一方面,當注冊中心當機後,服務消費者仍能從本機記憶體中找到服務節點清單進而發起調用。

本地快照

我們說,注冊中心當機後,服務消費者仍能從本機記憶體中找到服務節點清單。那麼如果服務消費者重新開機了呢?這時候我們就需要一份本地快照了,即我們儲存一份節點狀态到本地檔案,每次重新開機之後會恢複到本機記憶體中。

服務節點的摘除

現在無論注冊中心工作與否,我們都能順利拿到服務節點了。但是不是所有的服務節點都是正确可用的呢?在實際應用中,這是需要打問号的。如果我們不校驗服務節點的正确性,很有可能就調用到了一個不正常的節點上。是以我們需要進行必要的節點管理。

對于節點管理來說,我們有兩種手段,主要是去摘除不正确的服務節點。

注冊中心摘除機制

一是通過注冊中心來進行摘除節點。服務提供者會與注冊中心保持心跳,而一旦超出一定時間收不到心跳包,注冊中心就認為該節點出現了問題,會把節點從服務清單中摘除,并通知到服務消費者,這樣服務消費者就不會調用到有問題的節點上。

服務消費者摘除機制

二是在服務消費者這邊拆除節點。因為服務消費者自身是最知道節點是否可用的角色,是以在服務消費者這邊做判斷更合理,如果服務消費者調用出現網絡異常,就将該節點從記憶體緩存清單中摘除。當然調用失敗多少次之後才進行摘除,以及摘除恢複的時間等等細節,其實都和用戶端熔斷類似,可以結合起來做。

一般來說,對于大流量應用,服務消費者摘除的敏感度會高于注冊中心摘除,兩者之間也不用刻意做同步判斷,因為過一段時間後注冊中心摘除會自動覆寫服務消費者摘除。

服務節點是可以随便摘除/變更的麼

上一節我們講可以摘除問題節點,進而避免流量調用到該節點上。但節點是可以随便摘除的麼?同時,這也包含"節點是可以随便更新的麼?"疑問。

頻繁變動

當網絡抖動的時候,注冊中心的節點就會不斷變動。這導緻的後果就是變更消息會不斷通知到服務消費者,服務消費者不斷重新整理本地緩存。如果一個服務提供者有100個節點,同時有100個服務消費者,那麼頻繁變動的效果可能就是100*100,引起帶寬打滿。

這時候,我們可以在注冊中心這邊做一些控制,例如經過一段時間間隔後才能進行變更消息通知,或者打開開關後直接屏蔽不進行通知,或者通過一個機率計算來判斷需要向哪些服務消費者通知。

增量更新

同樣是由于頻繁變動可能引起的網絡風暴問題,一個可行的方案是進行增量更新,注冊中心隻會推送那些變化的節點資訊而不是全部,進而在頻繁變動的時候避免網絡風暴。

可用節點過少

當網絡抖動,并進行節點摘除過後,很可能出現可用節點過少的情況。這時候過大的流量配置設定給過少的節點,導緻剩下的節點難堪重負,罷工不幹,引起惡化。而實際上,可能節點大多數是可用的,隻不過由于網絡問題與注冊中心未能及時保持心跳而已。

這時候,就需要在服務消費者這邊設定一個開關比例門檻值,當注冊中心通知節點摘除,但緩存清單中剩下的節點數低于一定比例後(與之前一段時間相比),不再進行摘除,進而保證有足夠的節點提供正常服務。

這個值其實可以設定的高一些,例如百分之70,因為正常情況下不會有頻繁的網絡抖動。當然,如果開發者确實需要下線多數節點,可以關閉該開關。

服務消費者如何保障穩定性

一個請求失敗了,最直接影響到的是服務消費者,那麼在服務消費者這邊,有什麼可以做的呢?

逾時

如果調用一個接口,但遲遲沒有傳回響應的時候,我們往往需要設定一個逾時時間,以防自己被遠端調用拖死。逾時時間的設定也是有講究的,設定的太長起的作用就小,自己被拖垮的風險就大,設定的太短又有可能誤判一些正常請求,大幅提升錯誤率。

在實際使用中,我們可以取該應用一段時間内的P999的值,或者取p95的值*2。具體情況需要自行定奪。

在逾時設定的時候,對于同步與異步的接口也是有區分的。對于同步接口,逾時設定的值不僅需要考慮到下遊接口,還需要考慮上遊接口。而對于異步來說,由于接口已經快速傳回,可以不用考慮上遊接口,隻需考慮自身在異步線程裡的阻塞時長,是以逾時時間也放得更寬一些。

容錯機制

請求調用永遠不能保證成功,那麼當請求失敗時候,服務消費者可以如何進行容錯呢?通常容錯機制分為以下這些:

FailTry:失敗重試。就是指最常見的重試機制,當請求失敗後視圖再次發起請求進行重試。這樣從機率上講,失敗率會呈指數下降。對于重試次數來說,也需要選擇一個恰當的值,如果重試次數太多,就有可能引起服務惡化。另外,結合逾時時間來說,對于性能有要求的服務,可以在逾時時間到達前的一段提前量就發起重試,進而在機率上優化請求調用。當然,重試的前提是幂等操作。

FailOver:失敗切換。和上面的政策類似,隻不過FailTry會在目前執行個體上重試。而FailOver會重新在可用節點清單中根據負載均衡算法選擇一個節點進行重試。

FailFast:快速失敗。請求失敗了就直接報一個錯,或者記錄在錯誤日志中,這沒什麼好說的。

另外,還有很多形形色色的容錯機制,大多是基于自己的業務特性定制的,主要是在重試上做文章,例如每次重試等待時間都呈指數增長等。

第三方架構也都會内置預設的容錯機制,例如Ribbon的容錯機制就是由retry以及retry next組成,即重試目前執行個體與重試下一個執行個體。這裡要多說一句,ribbon的重試次數與重試下一個執行個體次數是以笛卡爾乘積的方式提供的噢!

熔斷

上一節将的容錯機制,主要是一些重試機制,對于偶然因素導緻的錯誤比較有效,例如網絡原因。但如果錯誤的原因是服務提供者自身的故障,那麼重試機制反而會引起服務惡化。這時候我們需要引入一種熔斷的機制,即在一定時間内不再發起調用,給予服務提供者一定的恢複時間,等服務提供者恢複正常後再發起調用。這種保護機制大大降低了鍊式異常引起的服務雪崩的可能性。

在實際應用中,熔斷器往往分為三種狀态,打開、半開以及關閉。引用一張martinfowler畫的原理圖:

微服務如何保障穩定性?

在普通情況下,斷路器處于關閉狀态,請求可以正常調用。當請求失敗達到一定門檻值條件時,則打開斷路器,禁止向服務提供者發起調用。當斷路器打開後一段時間,會進入一個半開的狀态,此狀态下的請求如果調用成功了則關閉斷路器,如果沒有成功則重新打開斷路器,等待下一次半開狀态周期。

斷路器的實作中比較重要的一點是失敗門檻值的設定。可以根據業務需求設定失敗的條件為連續失敗的調用次數,也可以是時間視窗内的失敗比率,失敗比率通過一定的滑動視窗算法進行計算。另外,針對斷路器的半開狀态周期也可以做一些花樣,一種常見的計算方法是周期長度随着失敗次數呈指數增長。

具體的實作方式可以根據具體業務指定,也可以選擇第三方架構例如Hystrix。

隔離

隔離往往和熔斷結合在一起使用,還是以Hystrix為例,它提供了兩種隔離方式:

信号量隔離:使用信号量來控制隔離線程,你可以為不同的資源設定不同的信号量以控制并發,并互相隔離。當然實際上,使用原子計數器也沒什麼不一樣。

線程池隔離:通過提供互相隔離的線程池的方式來隔離資源,相對來說消耗資源更多,但可以更好地應對突發流量。

降級

降級同樣大多和熔斷結合在一起使用,當服務調用者這方斷路器打開後,無法再對服務提供者發起調用了,這時候可以通過傳回降級資料來避免熔斷造成的影響。

降級往往用于那些錯誤容忍度較高的業務。同時降級的資料如何設定也是一門學問。一種方法是為每個接口預先設定好可接受的降級資料,但這種靜态降級的方法适用性較窄。還有一種方法,是去線上日志系統/流量錄制系統中撈取上一次正确的傳回資料作為本次降級資料,但這種方法的關鍵是提供可供穩定抓取請求的日志系統或者流量采樣錄制系統。

另外,針對降級我們往往還會設定操作開關,對于一些影響不大的采取自動降級,而對于一些影響較大的則需進行人為幹預降級。

服務提供者如何保障穩定性

限流

限流就是限制服務請求流量,服務提供者可以根據自身情況(容量)給請求設定一個門檻值,當超過這個門檻值後就丢棄請求,這樣就保證了自身服務的正常運作。

門檻值的設定可以針對兩個方面考慮,一是QPS即每秒請求數,二是并發線程數。從實踐來看,我們往往會選擇後者,因為QPS高往往是由于處理能力高,并不能反映出系統"不堪重負"。

除此之外,我們還有許多針對限流的算法。例如令牌桶算法以及漏桶算法,主要針對突發流量的狀況做了優化。第三方的實作中例如guava rateLimiter就實作了令牌桶算法。在此就不就細節展開了。

重新開機與復原

限流更多的起到一種保障的作用,但如果服務提供者已經出現問題了,這時候該怎麼辦呢?

這時候就會出現兩種狀況。一是本身代碼有bug,這時候一方面需要服務消費者做好熔斷降級等操作,一方面服務提供者這邊結合DevOps需要有快速復原到上一個正确版本的能力。

更多的時候,我們可能僅僅碰到了與代碼無強關聯的單機故障,一個簡單粗暴的辦法就是自動重新開機。例如觀察到某個接口的平均耗時超出了正常範圍一定程度,就将該執行個體進行自動重新開機。當然自動重新開機需要有很多注意事項,例如重新開機時間是否放在晚上,以及自動重新開機引起的與上述節點摘除一樣的問題,都需要考慮和處理。

在事後複盤的時候,如果當時沒有保護現場,就很難定位到問題原因。是以往往在一鍵復原或者自動重新開機之前,我們往往需要進行現場保護。現場保護可以是自動的,例如一開始就給jvm加上列印gc日志的參數-XX:+PrintGCDetails,或者輸出oom檔案-XX:+HeapDumpOnOutOfMemoryError,也可以配合DevOps自動腳本完成,當然手動也可以。一般來說我們會如下操作:

列印堆棧資訊,jstak -l 'java程序PID'

列印記憶體鏡像,jmap -dump:format=b,file=hprof 'java程序PID'

保留gc日志,保留業務日志

排程流量

除了以上這些措施,通過排程流量來避免調用到問題節點上也是非常常用的手段。

當服務提供者中的一台機器出現問題,而其他機器正常時,我們可以結合負載均衡算法迅速調整該機器的權重至0,避免流量流入,再去機器上進行慢慢排查,而不用着急第一時間重新開機。

如果服務提供者分了不同叢集/分組,當其中一個叢集出現問題時,我們也可以通過路由算法将流量路由到正常的叢集中。這時候一個叢集就是一個微服務分組。

而當機房炸了、光纜被偷了等IDC故障時,我們又部署了多IDC,也可以通過一些方式将流量切換到正常的IDC,以供服務繼續正常運作。切換流量同樣可以通過微服務的路由實作,但這時候一個IDC對應一個微服務分組了。除此之外,使用DNS解析進行流量切換也是可以的,将對外域名的VIP從一個IDC切換到另一個IDC。