
之前在我們講的幂等設計中,為了過濾掉已經處理過的請求,其中需要儲存處理過的狀态,為了把服務做成無狀态的,我們引入了第三方的存儲。而這一篇中,我們将談論一下服務的狀态,隻有清楚地了解了狀态這個事,我們才有可能設計出更好或是更有彈力的系統架構。
所謂“狀态”,就是為了保留程式的一些資料或是上下文。比如之前幂等性設計中所說的需要保留下每一次請求的狀态,或是像使用者登入時的 Session,我們需要這個 Session 來判斷這個請求的合法性,還有一個業務流程中需要讓多個服務組合起來形成一個業務邏輯的運作上下文 Context。這些都是所謂的狀态。
我們的代碼中基本上到處都是這樣的狀态。
無狀态的服務 Stateless
一直以來,無狀态的服務被當成分布式服務設計的最佳實踐和鐵律。因為無狀态的服務對于擴充性和運維實在是太友善了。沒有狀态的服務,可以随意地增加和減少結點,同樣可以随意地搬遷。而且,無狀态的服務可以大幅度降低代碼的複雜度以及 Bug 數,因為沒有狀态,是以也沒有明顯的“副作用”。
基本上來說,無狀态的服務和“函數式程式設計”的思維方式如出一轍。在函數式程式設計中,一個鐵律是,函數是無狀态的。換句話說,函數是 immutable 不變的,所有的函數隻描述其邏輯和算法,根本不儲存資料,也不會修改輸入的資料,而是把計算好的結果傳回出去,哪怕要把輸入的資料重新拷貝一份并隻做少量的修改(關于函數式程式設計可以參看我在 CoolShell 上的文章《函數式程式設計》)。
但是,現實世界是一定會有狀态的。這些狀态可能表現在如下的幾個方面。
程式調用的結果。
服務組合下的上下文。
服務的配置。
為了做出無狀态的服務,我們通常需要把狀态儲存到一個第三方的地方。比如,不太重要的資料可以放到 Redis 中,重要的資料可以放到 MySQL 中,或是像 ZooKeeper/Etcd 這樣的高可用的強一緻性的存儲中,或是分布式檔案系統中。
于是,我們為了做成無狀态的服務,會導緻這些服務需要耦合第三方有狀态的存儲服務。一方面是有依賴,另一方面也增加了網絡開銷,導緻服務的響應時間也會變慢。
是以,第三方的這些存儲服務也必須要做成高可用高擴充的方式。而且,為了減少網絡開銷,還需要在無狀态的服務中增加緩存機制。然而,下次這個使用者的請求并不一定會在同一台機器,是以,這個緩存會在所有的機器上都建立,也算是一種浪費吧。
這種“轉移責任”的玩法也催生出了對分布式存儲的強烈需求。正如之前在《分布式系統架構的本質》系列文章中談到的關鍵技術之一的“狀态 / 資料排程”所說的,因為資料層的 scheme 衆多,是以,很難做出一個放之四海皆準的分布式存儲系統。
這也是為什麼無狀态的服務需要依賴于像 ZooKeeper/Etcd 這樣的高可用的有強一緻的服務,或是依賴于底層的分布式檔案系統(像開源的 Ceph 和 GlusterFS)。而現在分布式資料庫也開始将服務和存儲分離,也是為了讓自己的系統更有彈力。
有狀态的服務 Stateful
在今天看來,有狀态的服務在今天看上去的确比較“反動”,但是,我們也需要比較一下它和無狀态服務的優劣。
正如上面所說的,無狀态服務在程式 Bug 上和水準擴充上有非常優秀的表現,但是其需要把狀态存放在一個第三方存儲上,增加了網絡開銷,而在服務内的緩存需要在所有的服務執行個體上都有(因為每次請求不會都落在同一個服務執行個體上),這是比較浪費資源的。
而有狀态的服務有這些好處。
資料本地化(Data Locality)。一方面狀态和資料是本機儲存,這方面不但有更低的延時,而且對于資料密集型的應用來說,這會更快。
更高的可用性和更強的一緻性。也就是 CAP 原理中的 A 和 C。
為什麼會這樣呢?因為對于有狀态的服務,我們需要對于用戶端傳來的請求,都必需保證其落在同一個執行個體上,這叫 Sticky Session 或是 Sticky Connection。這樣一來,我們完全不需要考慮資料要被加載到不同的結點上去,而且這樣的模型更容易了解和實作。
可見,最重要的差別就是,無狀态的服務需要我們把資料同步到不同的結點上,而有狀态的服務通過 Sticky Session 做資料分片(當然,同步有同步的問題,分片也有分片的問題,這兩者沒有誰比誰好,都有 trade-off)。
這種 Sticky Session 是怎麼實作的呢?
最簡單的實作就是用持久化的長連接配接。就算是 HTTP 協定也要用長連接配接。或是通過一個簡單的哈希(hash)算法,比如,通過 uid 求模的方式,走一緻性哈希的玩法,也可以友善地做水準擴充。
然而,這種方式也會帶來問題,那就是,結點的負載和資料并不會很均勻。尤其是長連接配接的方式,連上了就不斷了。是以,玩長連接配接的玩法一般都會有一種叫“反向壓力 (Back Pressure)”。也就是說,如果服務端成為了熱點,那麼就主動斷連接配接,這種玩法也比較危險,需要用戶端的配合,否則容易出 Bug。
如果要做到負載和資料均勻的話,我們需要有一個中繼資料索引來映射後端服務執行個體和請求的對應關鍵,還需要一個路由結點,這個路由結點會根據中繼資料索引來路由,而這個中繼資料索引表會根據後端服務的壓力來重新組織相關的映射。
當然,我們可以把這個路由結點給去掉,讓有狀态的服務直接路由。要做到這點,一般來說,有兩種方式。一種是直接使用配置,在節點啟動時把其中繼資料讀到記憶體中,但是這樣一來增加或減少結點都需要更新這個配置,會導緻其它結點也一同要重新讀入。
另一種比較好的做法是使用到 Gossip 協定,通過這個協定在各個節點之間互相散播消息來同步中繼資料,這樣新增或減少結點,叢集内部可以很容易重新配置設定(聽起來要實作好真的好複雜)。
在有狀态的服務上做自動化伸縮的是有一些相關的真實案例的。比如,Facebook 的 Scuba,這是一個分布式的記憶體資料庫,它使用了靜态的方式,也就是上面的第一種方式。Uber 的 Ringpop 是一個開源的 Node.js 的根據地理位置分片的路由請求的庫(開源位址為:https://github.com/uber-node/ringpop-node )。
還有微軟的 Orleans,Halo 4 就是基于其開發的,其使用了 Gossip 協定,一緻性哈希和 DHT 技術相結合的方式。使用者通過其 ID 的一緻性雜湊演算法映射到一個節點上,而這個節點儲存了這個使用者對應的 DHT,再通過 DHT 定位到處理使用者請求的位置,這個項目也是開源的(開源位址為: https://github.com/dotnet/orleans )。
關于可擴充的有狀态服務,這裡強烈推薦 Twitter 的美女工程師 Caitie McCaffrey 的演講 Youtube 視訊《Building Scalable Stateful Service》(演講 PPT),其文字版是在 High Scalability 上的這篇文章《Making the Case for Building Scalable Stateful Services in the Modern Era》
服務狀态的容錯設計
在容錯設計中,服務狀态是一件非常複雜的事。尤其對于運維來說,因為你要排程服務就需要排程服務的狀态,遷移服務的狀态就需要遷移服務的資料。在資料量比較大的情況下,這一點就變得更為困難了。
雖然上述有狀态的服務的排程通過 Sticky Session 的方式是一種方式,但我依然覺得理論上來說雖然可以這麼幹,這實際在運維的過程中,這麼幹還是件挺麻煩的事兒,不是很好的玩法。
很多系統的高可用的設計都會采取資料在運作時就複制的方案,比如:ZooKeeper、Kafka、Redis 或是 ElasticSearch 等等。在運作時進行資料複制就需要考慮一緻性的問題,是以,強一緻性的系統一般會使用兩階段送出。
這要求所有的結點都需要有一緻的結果,這是 CAP 裡的 CA 系統。而也有的系統采用的是大多數人一緻就可以了,比如 Paxos 算法,這是 CP 系統。
但我們需要知道,即使是這樣,當一個結點挂掉了以後,在另外一個地方重新恢複這個結點時,這個結點需要把資料同步過來才能提供服務。然而,如果資料量過大,這個過程可能會很漫長,這也會影響我們系統的可用性。
是以,我們需要使用底層的分布式檔案系統,對于有狀态的資料不但在運作時進行多結點間的複制,同時為了避免挂掉,還需要把資料持久化在硬碟上,這個硬碟可以是挂載到本地硬碟的一個外部分布式的檔案卷。
這樣當結點挂掉以後,以另外一個主控端上啟動一個新的服務執行個體時,這個服務可以從遠端把之前的檔案系統挂載過來。然後,在啟動的過程中就裝載好了大多數的資料,進而可以從網絡其它結點上同步少量的資料,因而可以快速地恢複和提供服務。
這一點,對于有狀态的服務來說非常關鍵。是以,使用一個分布式檔案系統是排程有狀态服務的關鍵。
小結
好了,我們來總結一下今天分享的主要内容。首先,我講了無狀态的服務。無狀态的服務就像一個函數一樣,對于給定的輸入,它會給出唯一确定的輸出。它的好處是很容易運維和伸縮,但需要底層有分布式的資料庫支援。
接着,我講了有狀态的服務,它們通過 Sticky Session、一緻性 Hash 和 DHT 等技術實作狀态和請求的關聯,并将資料同步到分布式資料庫中;利用分布式檔案系統,還能在節點挂掉時快速啟動新執行個體。下篇文章中,我們講述補償事務。希望對你有幫助。
也歡迎你分享一下你所實作的分布式服務是無狀态的,還是有狀态的?用到了哪些技術?
文末給出了《分布式系統設計模式》系列文章的目錄,希望你能在這個清單裡找到自己感興趣的内容。
彈力設計篇
認識故障和彈力設計
隔離設計 Bulkheads
異步通訊設計 Asynchronous
幂等性設計 Idempotency
服務的狀态 State
補償事務 Compensating Transaction
重試設計 Retry
熔斷設計 Circuit Breaker
限流設計 Throttle
降級設計 degradation
彈力設計總結
管理設計篇
分布式鎖 Distributed Lock
配置中心 Configuration Management
邊車模式 Sidecar
服務網格 Service Mesh
網關模式 Gateway
部署更新政策
性能設計篇
緩存 Cache
異步處理 Asynchronous
資料庫擴充
秒殺 Flash Sales
邊緣計算 Edge Computing