分散式系統關注點——“無狀態”詳解
本文中我們開始聊一些讓系統更簡單,更容易維護的東西——“易伸縮”,首當其衝的第一篇文章就是“stateless”,也叫“無狀態”。
一、初識“狀態”
我們首先舉個例子。
開發 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 負荷過大的時候。
最後,如果前面的都不合適,可以將共享儲存作為降級預案來運用,如遠端快取、資料庫等。然後當狀態丟失的時候可以從這些共享儲存中恢復。
所以,最理想的狀態存放點。要麼在最前端,要麼在最底層的儲存層。
四、總結
任何事物都是有兩面性的,正如前面提到的,我們並不是要所有的業務處理都改造成“無狀態”,而只是挑其中的一部分。最終還是看“價值”,看“價效比”。
比如,將一個以“狀態”為核心的即時聊天工具的所有處理過程都改造成“無狀態”的,就有點得不償失了。