1. 程式人生 > >分布式系統關註點—“無狀態”詳解

分布式系統關註點—“無狀態”詳解

並且 () 原因 簡單 場景 內存 底層 過大 自然

一、初識“狀態”
我們首先舉個例子。

開發 Z 哥對運維 Y 弟喊:“Y 弟,現在系統好卡,剛上了一波活動,趕緊幫我加幾臺機器上去頂一下。”

Y 弟回復說:“沒問題,分分鐘搞定”。

然後就發現數據庫的壓力迅速上升,DBA 就吼了:“Z 哥,你丫的搞什麽呢?數據庫要被你弄垮了”。

然後客服那邊接框也爆炸了,越來越多的用戶說剛登陸後沒多久,操作著就退出了,接著登陸,又退出了,到底還做不做生意了。

這個案例中的問題,產生的根本原因是因為系統中存在著大量“有狀態”的業務處理過程。

二、“有狀態”和“無狀態”
N.Wirth 曾經在它 1984 年出版的書中將程序的定義經典的概括為:程序 = 數據結構 + 算法。(這個概括也是這本書的書名)

這是一個很有意思的啟發,受它的影響,z 哥認為程序做的事情本質就是“數據的移動和組合”,以此來達到我們所期望的結果。而如何移動、如何組合是由“算法”來定的,所以 z 哥延伸出一個新的定義:數據 + 算法 = 成果。

通過程序處理所得到的“成果”其實和你平時生活中完成的任何事情所得到的“成果”是一樣的。任何一個“成果”都是你通過一系列的“行動”將最開始的“原料”進行加工、轉化,最終得到你所期望的“成果”。

比如,你將常溫的水,通過“倒入水壺”、“通電加熱”等工作後變成了 100 度的水,就是這樣一個過程。

正如燒水的例子,大多數時候得到一個“成果”往往需要好幾道“行動”才能完成。

這個時候如果想降低這幾道“行動”總的成本(如:時間)該怎麽辦呢?

自然就是提煉出反復要做的事情,讓其只做一次。而這個事情在程序中,就是將一部分“數據”放到一個“暫存區”(一般就是本地內存),以提供給相關的“行動”共用。

但是如此一來,就導致了需要增加一道關系,以表示每一個“行動”與哪一個“暫存區”關聯。因為在程序裏,“行動”可能是“多線程”的。

這時,這個“行動”就變成“有狀態”的了。

題外話:共用同一個“暫存區”的多個“行動”所處的環境經常被稱作“上下文”。

我們再來深入聊聊“有狀態”。

“暫存區”裏存的是“數據”,所以可以理解為“有數據”就等價於“有狀態”。

“數據”在程序中的作用範圍分為“局部”和“全局”(對應局部變量和全局變量),因此“狀態”其實也可以分為兩種,一種是局部的“會話狀態”,一種是全局的“資源狀態”。

題外話:因為有些服務端不單單負責運算,還會提供其自身範圍內的“數據”出去,這些“數據”屬於服務端完整的一部分,被稱作“資源”。所以,理論上資源可以被每個會話來使用,因此是全局的狀態。

本文聊的“有狀態”都指的是“會話狀態”。

與“有狀態”相反的是“無狀態”,“無狀態”意味著每次“加工”的所需的“原料”全部由外界提供,服務端內部不做任何的“暫存區”。並且請求可以提交到服務端的任意副本節點上,處理結果都是完全一樣的。

有一類方法天生是“無狀態”,就是負責表達移動和組合的“算法”。因為它的本質就是:

接收“原料”(入參)
“加工”並返回“成果”(出參)

為什麽網上主流的觀點都在說要將方法多做成“無狀態”的呢?

因為我們更習慣於編寫“有狀態”的代碼,但是“有狀態”不利於系統的易伸縮性和可維護性。

在分布式系統中,“有狀態”意味著一個用戶的請求必須被提交到保存有其相關狀態信息的服務器上,否則這些請求可能無法被理解,導致服務器端無法對用戶請求進行自由調度(例如雙 11 的時候臨時加再多的機器都沒用)。

同時也導致了容錯性不好,倘若保有用戶信息的服務器宕機,那麽該用戶最近的所有交互操作將無法被透明地移送至備用服務器上,除非該服務器時刻與主服務器同步全部用戶的狀態信息。

但是如果想獲得更好的伸縮性,就需要盡量將“有狀態”的處理機制改造成“無狀態”的處理機制。
三、“無狀態”化處理
將“有狀態”的處理過程改造成“無狀態”的,思路比較簡單,內容不多。

首先,狀態信息前置,豐富入參,將處理需要的數據盡可能都通過上遊的客戶端放到入參中傳過來。

當然,這個方案的弊端也很明顯:網絡數據包的大小會更大一些。

另外,客戶端與服務端的交互中如果涉及到多次交互,則需要來回傳遞後續服務端處理中所需的數據,以避免需要在服務端暫存。

(橙色請求,綠色響應)

這些改造的目的都是為了盡量少出現類似下面的代碼。

func(){
returni++;
}

而是變成:

func(i){
returni+1;
}

要更好的做好這個“無狀態”化的工作,依賴於你在架構設計或者項目設計中的合理分層。

盡量將會話狀態相關的處理上浮到最前面的層,因為只有最前面的層才與系統使用者接觸,如此一來,其它的下層就可以將“無狀態”作為一個普遍性的標準去做。

與此同時,由於會話狀態集中在最前面的層,所以哪怕真的狀態丟失了,重建狀態的成本相對也小很多。

比如三層架構的話,保證 BLL 和 DAL 都不要有狀態,代碼的可維護性大大提高。

如果是分布式系統的話,保證那些被服務化的程序都不要有狀態。除了能提高可維護性,也大大有利於做灰度發布、A/B 測試。

題外話:在這裏,提到做分層的目的是為了說明,只有將 IO 密集型程序和 CPU 密集型程序分離,才是通往“無狀態”真正的出路。一旦分離後,CPU 密集型的程序自然就是“無狀態”了。

如此也能更好的做“彈性擴容”。因為常見的需要“彈性擴容”的場景一般指的就是 CPU 負荷過大的時候。

最後,如果前面的都不合適,可以將共享存儲作為降級預案來運用,如遠程緩存、數據庫等。然後當狀態丟失的時候可以從這些共享存儲中恢復。

所以,最理想的狀態存放點。要麽在最前端,要麽在最底層的存儲層。

四、總結
任何事物都是有兩面性的,正如前面提到的,我們並不是要所有的業務處理都改造成“無狀態”,而只是挑其中的一部分。最終還是看“價值”,看“性價比”。

比如,將一個以“狀態”為核心的即時聊天工具的所有處理過程都改造成“無狀態”的,就有點得不償失了。

分布式系統關註點—“無狀態”詳解