本文中我們開始聊一些讓系統更簡單,更容易維護的東西——“易伸縮”,首當其衝的第一篇文章就是“stateless”,也叫“無狀態”。
一、初識“狀態”
我們首先舉個例子。
開發Z哥對運維Y弟喊:“Y弟,現在系統好卡,剛上了一波活動,趕緊幫我加幾臺機器上去頂一下。”
Y弟回覆說:“沒問題,分分鐘搞定”。
然後就發現數據庫的壓力迅速上升,DBA就吼了:“Z哥,你丫的搞什麼呢?數據庫要被你弄垮了”。
然後客服那邊接框也爆炸了,越來越多的用戶說剛登陸後沒多久,操作着就退出了,接着登陸,又退出了,到底還做不做生意了。
這個案例中的問題,產生的根本原因是因爲系統中存在着大量“有狀態”的業務處理過程。
二、“有狀態”和“無狀態”
N.Wirth曾經在它1984年出版的書中將程序的定義經典的概括爲:程序=數據結構+算法。(這個概括也是這本書的書名)
這是一個很有意思的啓發,受它的影響,z哥認爲程序做的事情本質就是“數據的移動和組合”,以此來達到我們所期望的結果。而如何移動、如何組合是由“算法”來定的,所以z哥延伸出一個新的定義:數據+算法=成果。
通過程序處理所得到的“成果”其實和你平時生活中完成的任何事情所得到的“成果”是一樣的。任何一個“成果”都是你通過一系列的“行動”將最開始的“原料”進行加工、轉化,最終得到你所期望的“成果”。
比如,你將常溫的水,通過“倒入水壺”、“通電加熱”等工作後變成了100度的水,就是這樣一個過程。
正如燒水的例子,大多數時候得到一個“成果”往往需要好幾道“行動”才能完成。
這個時候如果想降低這幾道“行動”總的成本(如:時間)該怎麼辦呢?
自然就是提煉出反覆要做的事情,讓其只做一次。而這個事情在程序中,就是將一部分“數據”放到一個“暫存區”(一般就是本地內存),以提供給相關的“行動”共用。
但是如此一來,就導致了需要增加一道關係,以表示每一個“行動”與哪一個“暫存區”關聯。因爲在程序裏,“行動”可能是“多線程”的。
這時,這個“行動”就變成“有狀態”的了。
題外話:共用同一個“暫存區”的多個“行動”所處的環境經常被稱作“上下文”。
我們再來深入聊聊“有狀態”。
“暫存區”裏存的是“數據”,所以可以理解爲“有數據”就等價於“有狀態”。
“數據”在程序中的作用範圍分爲“局部”和“全局”(對應局部變量和全局變量),因此“狀態”其實也可以分爲兩種,一種是局部的“會話狀態”,一種是全局的“資源狀態”。
題外話:因爲有些服務端不單單負責運算,還會提供其自身範圍內的“數據”出去,這些“數據”屬於服務端完整的一部分,被稱作“資源”。所以,理論上資源可以被每個會話來使用,因此是全局的狀態。
本文聊的“有狀態”都指的是“會話狀態”。
與“有狀態”相反的是“無狀態”,“無狀態”意味着每次“加工”的所需的“原料”全部由外界提供,服務端內部不做任何的“暫存區”。並且請求可以提交到服務端的任意副本節點上,處理結果都是完全一樣的。
有一類方法天生是“無狀態”,就是負責表達移動和組合的“算法”。因爲它的本質就是:
- 接收“原料”(入參)
- “加工”並返回“成果”(出參)
爲什麼網上主流的觀點都在說要將方法多做成“無狀態”的呢?
因爲我們更習慣於編寫“有狀態”的代碼,但是“有狀態”不利於系統的易伸縮性和可維護性。
在分佈式系統中,“有狀態”意味着一個用戶的請求必須被提交到保存有其相關狀態信息的服務器上,否則這些請求可能無法被理解,導致服務器端無法對用戶請求進行自由調度(例如雙11的時候臨時加再多的機器都沒用)。
同時也導致了容錯性不好,倘若保有用戶信息的服務器宕機,那麼該用戶最近的所有交互操作將無法被透明地移送至備用服務器上,除非該服務器時刻與主服務器同步全部用戶的狀態信息。
但是如果想獲得更好的伸縮性,就需要儘量將“有狀態”的處理機制改造成“無狀態”的處理機制。
三、“無狀態”化處理
將“有狀態”的處理過程改造成“無狀態”的,思路比較簡單,內容不多。
首先,狀態信息前置,豐富入參,將處理需要的數據儘可能都通過上游的客戶端放到入參中傳過來。
當然,這個方案的弊端也很明顯:網絡數據包的大小會更大一些。
另外,客戶端與服務端的交互中如果涉及到多次交互,則需要來回傳遞後續服務端處理中所需的數據,以避免需要在服務端暫存。
(橙色請求,綠色響應)
這些改造的目的都是爲了儘量少出現類似下面的代碼。
func(){
return i++;
}
而是變成:
func(i){
return i+1;
}
要更好的做好這個“無狀態”化的工作,依賴於你在架構設計或者項目設計中的合理分層。
儘量將會話狀態相關的處理上浮到最前面的層,因爲只有最前面的層才與系統使用者接觸,如此一來,其它的下層就可以將“無狀態”作爲一個普遍性的標準去做。
與此同時,由於會話狀態集中在最前面的層,所以哪怕真的狀態丟失了,重建狀態的成本相對也小很多。
比如三層架構的話,保證BLL和DAL都不要有狀態,代碼的可維護性大大提高。
如果是分佈式系統的話,保證那些被服務化的程序都不要有狀態。除了能提高可維護性,也大大有利於做灰度發佈、A/B測試。
題外話:在這裏,提到做分層的目的是爲了說明,只有將IO密集型程序和CPU密集型程序分離,纔是通往“無狀態”真正的出路。一旦分離後,CPU密集型的程序自然就是“無狀態”了。
如此也能更好的做“彈性擴容”。因爲常見的需要“彈性擴容”的場景一般指的就是CPU負荷過大的時候。
最後,如果前面的都不合適,可以將共享存儲作爲降級預案來運用,如遠程緩存、數據庫等。然後當狀態丟失的時候可以從這些共享存儲中恢復。
所以,最理想的狀態存放點。要麼在最前端,要麼在最底層的存儲層。
四、總結
任何事物都是有兩面性的,正如前面提到的,我們並不是要所有的業務處理都改造成“無狀態”,而只是挑其中的一部分。最終還是看“價值”,看“性價比”。
比如,將一個以“狀態”爲核心的即時聊天工具的所有處理過程都改造成“無狀態”的,就有點得不償失了。