歡迎光臨
每天分享高質量文章

【系統架構】億級Web 系統的容錯性實踐【上】

簡單重試機制

最容易也最簡單被人想到的容錯方式,當然就是“失敗重試”,總而言之,簡單粗暴!簡單是指它的實現通常很簡單,粗暴則是指使用不當,很可能會帶來系統“雪崩”的風險,因為重試意味著對後端服務的雙倍請求。

我們請求一個服務,如果服務請求失敗,則重試一次。假設,這個服務在常規狀態下是99.9%的成功率,因為某一次波動性的異常,成功率下跌到95%,那麼如果有重試機制,那麼成功率大概還能保持在99.75%。而簡單重試的缺陷也很明顯,如果服務真的出問題,很可能帶來雙倍流量,衝擊服務系統,有可能直接將服務衝垮。而在實際的真實業務場景,往往更嚴重,一個功能不可用,往往更容易引起使用者的“反覆點選”,反而製造更大規模的流量衝擊。比起服務的成功率比較低,系統直接被衝擊到“掛掉”的後果明顯更嚴重。

簡單重試,要使用在恰當的場景。或者,主動計算服務成功率,成功率過低,就直接不做重試行為,避免帶來過高的流量衝擊。

主備服務自動切換

既然單一服務的重試,可能會給該帶來雙倍的流量衝擊,而最終導致更嚴重的後果,那麼我們不如將場景變為主備服務的自動重試或者切換。例如,我們搭建了兩套獲取openid的服務,如果服務A獲取失敗,則嘗試從服務B中獲取。因為重試的請求壓力是壓到了服務B上,服務A通常不會因為重試而產生雙倍的流量衝擊。

這種重試的機制,看似比較可用,而實際上也存在一些問題:

(1) 通常會存在“資源浪費”的問題。因為備份服務系統,很可能長期處於閑置狀態,只有在主服務異常的時候,它的資源才會被比較充分地使用。不過,如果對於核心的服務業務(例如核心資料、營收相關)進行類似的部署,雖然會增加一些機器成本和預算,但這個付出通常也是物有所值的。

(2) 觸發重試機制,對於使用者的請求來說,耗時必然增加。主服務請求失敗,然後再到備份服務請求,這個環節的請求耗時就至少翻倍增長,假設主服務出現連線(connect)超時,那麼耗時就更是大幅度增加。一個服務在正常狀態下,獲取資料也許只要50ms,而服務的超時時間通常會設定到500-1000ms,甚至更多,一旦出現超時重試的場景,請求耗時必然大幅度增長,很可能會比較嚴重地影響使用者體驗。

(3) 主備服務一起陷入異常。如果是因為流量過大問題導致主服務異常,那麼備份服務很可能也會承受不住這種級別的流量而掛掉。

重試的容錯機制,在AMS上有使用,但是相對比較少,因為我們認為主備服務,還是不足夠可靠。

動態剔除或恢復異常機器

在AMS裡,我們的後端涉及數以百計的各類服務,來支撐整個運營系統的正常運作。所有後端服務或者儲存,首先是部署為無狀態的方式提供服務(一個服務通常很多臺機器),然後,透過公司內的一個公共的智慧路由服務L5,納入到AMS中。

(1) 所有服務與儲存,無狀態路由。這樣做的目的,主要是為了避免單點風險,就是避免某個服務節點掛了,導致整個服務就癱瘓了。實際上,即使像一些具有主備性質(主機器掛了,支援切換到備份機器)的接入服務,也是不夠可靠的,畢竟只有2臺,它們都掛了的情況,還是可能發生的。我們後端的服務,通常都以一組機器的形式提供服務,彼此之間沒有狀態關係,支撐隨機分配請求。

(2) 支援平行擴容。遇到大流量場景,支援加機器擴容。

(3) 自動剔除異常機器。在我們的路由服務,發現某個服務的機器異常的時候(成功率低於50%),就會自動剔除該機器,後續,會發出試探性的請求,確認等它恢復正常之後,再重新加回到服務機器組。

例如,假如一組服務下擁有服務機器四臺(ABCD),假設A機器的服務因為某種未知原因,完全不可用了,這個時候L5服務會主動將A機器自動從服務組裡剔除,只保留BCD三臺機器對外提供服務。而在後續,假如A機器從異常中恢復了,那麼L5再主動將機器A加回來,最後,又變成ABCD四臺機器對外提供服務。

在過去的3年裡,我們逐步將AMS內的服務,漸漸從寫死IP串列或者主備狀態的服務,全部升級和最佳化為L5樣式的服務,慢慢實現了AMS後端服務的自我容錯能力。至少,我們已經比較少遇到,再因為某一臺機器的軟體或者硬體故障,而不得不人工介入處理的情況。我們也慢慢地從疲於奔命地處理告警的苦難中,被解放出來。

設定超時時間

呼叫任何一個服務或者儲存,一個合理的超時時間(超時時間,就是我們請求一個服務時,等待的最長時間),是非常重要的,而這一點往往比較容易被忽視。通常Web系統和後端服務的通訊方式,是同步等待的樣式。這種樣式,它會帶來的問題比較多。

對於服務端,影響比較大的一個問題,就是它會嚴重影響系統吞吐率。假設,我們一個服務的機器上,啟用了100個處理請求的worker,worker的超時時間設定為5秒,1個worker處理1個任務的平均處理耗時是100ms。那麼1個work在5秒鐘的時間裡,能夠處理50個使用者請求,然而,一旦網路或者服務偶爾異常,響應超時,那麼在本次處理的後續整整5秒裡,它僅僅處理了1個等待超時的失敗任務。一旦比較大機率出現這型別的超時異常,系統的吞吐率就會大面積下降,有可能耗盡所有的worker(資源被佔據,全部在等待狀態,直到5s超時才釋放),最終導致新的請求無worker可用,只能陷入異常狀態。

算上網路通訊和其他環節的耗時,使用者就等待了超過5s時間,最後卻獲得一個異常的結果,使用者的心情通常是崩潰的。

解決這個問題的方式,就是設定一個合理的超時時間。例如,回到上面的的例子,平均處理耗時是100ms,那麼我們不如將超時時間從5s下調到500ms。從直觀上看,它就解決了吞吐率下降和使用者等待過長的問題。然而,這樣做本身又比較容易帶來新的問題,就是會引起服務的成功率下降。因為平均耗時是100ms,但是,部分業務請求本身耗時比較長,耗時超過500ms也比較多。例如,某個請求服務端耗時600ms才處理完畢,然後這個時候,客戶端認為等待超過500ms,已經斷開了連線。處理耗時比較長的這型別業務請求會受到比較明顯的影響。

解決超時時間過短帶來成功率下降問題

超時時間設定過短,會將很多本來處理成功的請求,當做服務超時處理掉,進而引起服務成功率下降。將全部業務服務,以一刀切的方式設定一個超時時間,是比較不可取的。最佳化的方法,我們分為兩個方向。

(1)快慢分離

根據實際的業務維度,區分對待地給各個業務服務配置不同的超時時間,同時,最好也將它們的部署服務也分離出來。例如,天天酷跑的查詢服務耗時通常為100ms,那麼超時時間我們就設定為1s,某新手遊的查詢服務通常耗時為700ms,那麼我們就設定為5s。這樣的話,整體系統的成功率,就不會受到比較大的影響。

(2)解決同步阻塞的

“快慢分離”可以改善系統的同步等待問題,但是,對於某些耗時本來就比較長的服務而言,系統的行程/執行緒資源仍然在同步等待過程中,無法響應其他新的請求,只能阻塞等待,它的資源仍然是被佔據,系統的整體吞吐率仍然被大幅度拉低。

解決的思路,當然是利用I/O多路復用,透過非同步回呼的方式,解決同步等待過程中的資源浪費。AMS的一些核心服務,採用的就是“協程”(又叫“微執行緒”,簡單的說,常規非同步程式程式碼裡巢狀比較多層的函式回呼,編寫複雜。而協程則提供了一種類似寫同步程式碼的方式,來寫非同步回呼程式),以解決同步等待的問題。非同步處理的簡單描述,就是當行程遇到I/O網路阻塞時,就保留現場,立刻切換去處理下一個業務請求,行程不會因為某個網路等待而停止處理業務,進而,系統吞吐率即使遇到網路等待時間過長的場景,通常都能保持在比較高的水平。

值得補充一點的是,非同步處理只是解決系統的吞吐率問題,對於使用者的體驗問題,並不會有改善,使用者需要等待的時間並不會減少。

防重入,防止重覆發貨

前面我們提到,我們設定了一個比較“合理的超時時間”,簡而言之,就是一個比較短的超時時間。而在資料寫入的場景,會引起新的問題,就我們的AMS系統而言,就是發貨場景。如果是發貨請求超時,這個時候,我們需要思考的問題就比較多了。

1、發貨等待超時,發貨服務執行發貨失敗。這種場景,問題不大,後續使用者重新點選領取按鈕,就可以觸發下一次重新發貨。

2、發貨等待超時,發貨服務實際在更晚的時候執行發貨成功,我們稱之為“超時成功”。比較麻煩的場景,則是每次都是發貨超時,而實際上都發貨成功,如果系統設計不當,有可能導致用戶可以無限領取禮包,最終造成活動運營事故。

第二種場景,給我們帶來了比較麻煩的問題,如果處理不當,使用者再次點選,就觸發第多次“額外”發貨。

例如,我們假設某個發貨服務超時時間設定為6s,使用者點選按鈕,我們的AMS收到請求後,請求發貨服務發貨,等待6s後,無響應,我們給使用者提示“領取失敗”,而實際上發貨服務卻在第8秒執行發貨成功,禮包到了使用者的賬戶上。而使用者看見“領取失敗”,則又再次點選按鈕,最終導致“額外”多發一個禮包給到這個使用者。

例子的時序和流程圖大致如下:

這裡就提到了防重入,簡單的說,就是如何確認不管使用者點選多少次這個領取按鈕,我們都確保結果只有一種預期結果,就是隻會給使用者發一次禮包,而不引起重覆發貨。我們的AMS活動運營平臺一年上線的活動超過4000個,涉及數以萬計的各種型別、不同業務系統的禮包發貨,業務通訊場景比較複雜。針對不同的業務場景,我們做了不同的解決方案:

1、業務層面限制,設定禮包單使用者限量。在發貨伺服器的源頭,設定好一個使用者僅能最多獲得1個禮包,直接避免重覆發放。但是,這種業務限制,並非每個業務場景都通用的,只限於內部具備該限制能力的業務發貨系統,並且,有一些禮包本身就可以多次領取的,就不適用了。

2、訂單號機制。使用者的每一次符合資格的發貨請求,都生成一個訂單號與之對應,透過它來確保1個訂單號,只發貨1次。這個方案雖然比較完善,但是,它是依賴於發貨服務方配合做“訂單號發貨狀態更新“的,而我們的發貨業務方眾多,並非每一個都能支援”訂單號更新“的場景。

3、 自動重試的非同步發貨樣式。使用者點選領取禮包按鈕後,Web端直接傳回成功,並且提示禮包在30分鐘內到賬。對於後臺,則將該發貨錄入到發貨佇列或者儲存中,等待發貨服務非同步發貨。因為是非同步處理,可以多次執行發貨重試操作,直到發貨成功為止。同時,非同步發貨是可以設定一個比較長的超時等待時間,通常不會出現“超時成功”的場景,並且對於前端響應來說,不需要等待後臺發貨狀態的傳回。但是,這種樣式,會給使用者帶來比較不好的體驗,就是沒有實時反饋,無法立刻告訴使用者,禮包是否到賬。

非訂單號的特殊防刷機制

某些特殊的合作場景,我們無法使用雙方約定訂單號方式,例如一個完全隔離獨立的外部發貨介面,不能和我們做訂單號的約定。基於這種場景,我們AMS專門做了一種防刷的機制,就是透過限制read超時的次數。但是,這種方案並非完美解決重覆發貨問題,只是能起到夠盡可能減少避免被刷的作用。一次網路通訊,通常包含:建立連線(connect),寫入資料發包(write),等待並且讀取回包(read),斷開連線(close)。

通常一個發貨服務如果出現異常,大多數情況,在connect步驟就是失敗或者超時,而如果一個請求走到等待回包(read)時超時,那麼發貨服務另外一邊就有可能發生了“超時但發貨成功”的場景。這個時候,我們將read超時的發生次數記錄起來,然後提供了一個配置限制次數的能力。假如設定為2次,那麼當一個使用者第一次領取禮包,遇到read超時,我們就允許它重試,當還遇到第二次read超時,就達到我們之前設定的閥值2,我們就認為它可能發貨成功,拒絕使用者的第三次領取請求。

這種做法,假設發貨服務真的出現很多超時成功,那麼使用者也最多隻能刷到2次禮包(次數可配置),而避免發生禮包無限制被刷的場景。但是,這種方案並不完全可靠,謹慎使用。

在發貨場景,還會涉及分散式場景下的CAP(一致性、可用性、分割槽容錯性)問題,不過,我們的系統並非是一個電商服務,大部分的發貨並沒有強烈的一致性要求。因此,總體而言,我們是弱化了一致性問題(核心服務,透過非同步重試的方式,達到最終一致性),以追求可用性和分割槽容錯性的保證。

對於web系統容錯機制,還有服務降級、服務解耦等一系列方法將在下篇文章總結。

    已同步到看一看
    贊(0)

    分享創造快樂