詳談分散式系統快取的設計細節

大快搜尋DKH發表於2018-10-29


在分散式 Web程式設計中,解決高併發以及內部解耦的關鍵技術離不開快取和佇列,而快取角色類似計算機硬體中CPU的各級快取。如今的業務規模稍大的網際網路專案,即使在最初beta版的開發上,都會進行預留設計。但是在諸多應用場景裡,也帶來了某些高成本的技術問題,需要細緻權衡。

 

服務端資料快取

一種區分

快取基於不同的條件有很多種劃分方式,本地快取 (Local cache)和分散式快取(Distributed cache)是一種常見分類,兩者自身又包含很多細類。

本地並不是指程式所在本地伺服器 (從嚴格概念來說),而是更細粒度的指位於程式自身的內部儲存空間,而分散式更多強調的是儲存在程式之外的一個或者多個伺服器上,彼此互動通訊,在具體軟體專案的設計和應用中,多數時候是混合一體。當然,個人認為對快取本質的理解才是最重要的,至於概念上的分類只是一個不同理解下的劃分而已。

一些技術成本

在具體專案架構設計時,單純使用前者 (本地快取)的開發成本毋庸置疑是極低的,主要考慮的是本機的記憶體負載或者極少量的磁碟I/O影響。而後者的設計初心是為了利於分散式程式之間快取資料的高效共享和管理,除了考慮快取所在伺服器自身的記憶體負載,設計時更需要充分考慮網路I/O、CPU的負載,以及某些場景下的磁碟I/O的代價,同時還在具體設計時儘可能規避和權衡整體穩定性和效率,這些不僅僅只是作為快取伺服器的硬體成本和技術維護。需要謹慎考慮的底層問題包括快取間通訊、網路負載和延遲等各種需要權衡的細節。

其實如果理解了快取本質就該知道,任何儲存介質在適當的場景下都可以充當一個高效的快取角色並進行專案整合和快取間叢集。常見主流的 Memcached和Redis等均是屬於後者範疇,甚至可以包括如基於NoSQL設計的MongoDB這類文件資料庫(但這是從角色角度講,而狹義劃分上這是基於磁碟的儲存庫,需要注意,各有專攻)。

這些第三方快取在進行專案整合和快取間叢集,也需要解決一些問題。甚至專案迭代到了後期階段,往往還需要具備較高專業知識的運維同時參與,並且在開發中的邏輯設計和程式碼實現也會增加一定的工作量。所以有時候在具體專案的設計上,一方面要儘可能預留,一方面還得根據實際情況儘可能精簡。

額外說下其他體會:在個人有限的技術學習和實踐裡,關於節點資料互動,尤其是服務間通訊,是不存在完美的閉環的,理論上也都是在 “當前階段”面向“高一致”的權衡罷了(大概跟生活是一樣的吧)。

 

快取資料庫結構設計細節

由於目前個人工作中大多數情況應用的是 Redis 3.x,以下若有特性關聯,均是以此作為參照說明。

例項 (Instance)

根據業務場景,公共資料和業務耦合資料,一定分別使用不同的例項。如果是單例項,才可以考慮以 DB劃分。當你使用的是Redis,那麼DB在Redis裡是有資料隔離,但沒有嚴格許可權限制,所以劃庫只是一種選擇。在Cluster叢集裡則是保持預設單個庫,不過實際中我會嘗試根據專案大小來調整,至於在哪個開發階段則是作為預留設計。

額外需要注意的是,作為重度依賴伺服器記憶體的快取產品,如果開啟了持久化 (後面會提到),並且在為併發量極大的服務提供支援時,伺服器硬體資源會出現大量搶佔,請結合持久策略配置,考慮例項是否進行分盤儲存。

持久化本質是將記憶體資料同步寫入硬碟 (刷盤),而磁碟I/O實在有限,被迫的寫入阻塞除了造成執行緒阻塞和服務超時,還會導致額外異常甚至波及其他底層依賴服務。當然,我的建議是,如果條件允許,最好是在專案初期設計時就進行規劃並確定。

快取 “表”(Table)

一般快取中並沒有傳統 RDBMS中直觀的表概念(往往以鍵值對“KV”形式存在),但從結構上來講,鍵值對本身就可以組裝為各種表結構。一般我會先生成資料庫表關係圖,然後分析什麼時候儲存字串,什麼時候儲存物件,然後使用快取鍵(KEY)進行表和欄位(列)分割。

假定需要儲存一個登入伺服器表資料,包含欄位 (列):name、sign、addr,那麼可以考慮將資料結構拆分為以下形式:

  { key : "server:name" , value : "xxxx" }

  { key : "server:sign" , value : "yyyy" }

  { key : "server:addr" , value : "zzzz" }

需要注意的是,往往在分散式快取產品中,例如 Redis,存在多種資料結構(如String、Hash等),還需要根據資料關聯性和列的數量,來選擇對應快取的儲存資料結構,相關儲存空間和時間複雜度是完全不同的,而這個在初期階段是很難感受到的。

同時,就算快取的記憶體設定的足夠大,剩餘也很多,也同樣需要考慮類似 RDBMS中的單表容量問題,控制條目數量不能無限增長(比如預知到儲存條目可以輕鬆達到百萬級),“分庫分表”的設計思路都是相通的。

快取鍵 (Key)

上面提到了基於快取鍵來設計表,這裡再單獨說明一下鍵相關的個人規範。在鍵長度足夠簡短的前提下,如果關聯相同業務模組,則必須設計為以同一個標識 (代號)開頭,目的是方便查詢和統計管理。

如使用者登入伺服器列表:

  { key : "ul:server:a" , value : "xxxx" }

  { key : "ul:server:b" , value : "yyyy" }

另外,每個獨立業務系統可考慮配置一個唯一的通用字首標識。當然,這裡不是必需,若實際工作中,如果使用的是不同庫,則可以忽略。

快取值 (value)

快取中的值 (這裡指單一條目)的大小沒有平均標準,但Size自然是越小越好(若使用的是Redis,一次操作的value較大會直接影響整個Redis的響應時間,不僅僅是指網路I/O)。如果儲存佔用空間直達10M+,建議考慮關聯的業務場景是否可以拆分為熱點和非熱點資料。

持久化 (Permanence)

上面也簡單提了下,一般來說,持久和快取本身是沒有直接關係的,可以粗略想象為一個面向硬碟一個面向記憶體。但如今的 Web專案裡,有些業務場景是高度依賴快取的,持久化可以一方面幫助提高快取服務重啟後的快速恢復,另一方面提供特定場景下的儲存特性。當然,由於持久化必然需要犧牲一些效能,包括CPU的搶佔和硬碟I/O影響。不過大多數時候是利大於弊,建議在應用快取的時候,沒有特別情況的話,儘量搭配持久化,無論是使用自身機制還是第三方來實現。

如果是使用的 Redis,其自身就具備相關持久策略,包含AOF和RDB,我在大多數情況下是兩者同時配置的(當然,最新官方版本本身也提供了混合模式)。如果在一些非高併發的場景下,或者說在一些中小專案的管理模組裡,僅僅只是作為最佳化手段,確定了不需持久,也可以直接設定關閉,節約效能開銷損耗,但建議在程式中將該例項做好標註,確保該例項的公共使用範圍。

淘汰 (Eliminate)

快取如果無限制的增長,即使設定了較短的過期 (Expiration ),在一些時間點上,高併發的一批大資料會在較短時間內就達到了可使用記憶體的峰頂,此時程式中與快取伺服器的互動會出現大量延遲和錯誤,甚至給伺服器自身都帶來了嚴重的不穩定性。所以在生產環境裡儘量給快取配置最大記憶體限制,以及適當的淘汰策略。

如果使用的是 Redis,自身淘汰策略選擇比較靈活。

個人的設計是,在資料呈現類似冪律分佈情況下,總有大量資料訪問較低,我會選擇配置 allkeys-lru、volatile-lru,將最少訪問的資料進行淘汰。再比如快取是作為日誌應用的,那麼我一般是專案前期是配置no-enviction,後期會配置為volatile-ttl。

當然,我也見過一種特殊業務下的設計,快取直接用來作為輕量的持久資料庫使用,而且是終端,開始覺得有些新奇,後來發現是非常符合業務設計的 (比如幾乎沒有任何複雜邏輯和強事務)。所以合情合理,確實不應該禁錮在傳統設計裡,畢竟架構總是基於業務去實時組合和改變的。

 

 

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555587/viewspace-2217925/,如需轉載,請註明出處,否則將追究法律責任。

相關文章