一個大型穩健成熟的分布式系統的背后,往往會設計眾多的支撐組件,將這些支撐系統成為分布式系統的基礎設施。進行系統架構設計所依賴的基礎設施,還包括分布式協作及配置管理組件、分布式緩存組件、持久化存儲組件、分布式消息系統、搜索引擎、以及CDN系統、負載均衡系統、運維自動化系統等,還有實時計算系統、離線計算系統、分布式文件系統、日志收集系統、監控系統、數據倉庫等。此處主要講講緩存系統組件。
緩存組件層
緩存系統帶來的好處:
- 加速讀寫。緩存通常是全內存的,比如Redis、Memcache。對內存的直接讀寫會比傳統的存儲層如MySQL,性能好很多。由於單台機器的內存資源和承載能力有限,並且如果大量使用本地緩存,也會使相同的數據被不同的節點存儲多份,對內存資源造成較大的浪費,因此才催生出了分布式緩存。
- 降低后端的負載。在高並發環境下,大量的讀、寫請求涌向數據庫,磁盤的處理速度與內存顯然不在一個量級,從減輕數據庫的壓力和提供系統響應速度兩個角度來考慮,一般都會在數據庫之前加一層緩存。
緩存系統帶來的成本:
- 數據不一致性:在分布式環境下,數據的讀寫都是並發的,上游有多個應用,通過一個服務的多個部署(為了保證可用性,一定是部署多份的),對同一個數據進行讀寫,在數據庫層面並發的讀寫並不能保證完成順序,也就是說后發出的讀請求很可能先完成(讀出臟數據)
- 代碼維護成本:加入緩存后,需要同時處理緩存層和存儲層的邏輯,增加了開發者維護代碼的成本。
- 運維成本:引入緩存層,比如Redis。為保證高可用,需要做主從,高並發需要做集群。
緩存的更新
緩存的數據一般都是有生命時間的,過了一段時間之后就會失效,再次訪問時需要重新加載。緩存的失效是為了保證與數據源真實的數據保證一致性和緩存空間的有效利用性。常見的緩存更新策略:
- LRU/LFU/FIFO等算法:三種算法都是屬於當緩存不夠用時采用的更新算法。只是選出的淘汰元素的規則不一樣:LRU淘汰最久沒有被訪問過的,LFU淘汰訪問次數最少的,FIFO先進先出。適合內存空間有限,數據長期不變動,基本不存在數據一不致性業務。比如一些一經確定就不允許變更的信息。要清理哪些數據是由具體的算法定的,開發人員只能選擇其中的一種,一致性比較差。
- 超時剔除:給緩存數據手動設置一個過期時間,比如Redis expire命令。當超過時間后,再次訪問時從數據源重新加載並設回緩存。開發維護成本不是很高,很多緩存系統都自帶過期時間API(比如Redis expire)。不能保證實時一致性,適合於能夠容忍一定時間內數據不一致性的業務。
- 主動更新:如果數據源的數據有更新,則主動更新緩存。一致性比較好,只要能確定正確更新,一致性就能有保證。業務數據更新與緩存更新藕合一起,需要處理業務數據更新成功而緩存更新失敗的情景,為了解耦一般用來消息隊列的方式更新。不過為了提高容錯性,一般會結合超時剔除方案,避免緩存更新失敗,緩存得不到更新的場景。一般適用於對於數據的一致性要求很高,比如交易系統,優惠劵的總張數。
緩存系統常見問題及解決
一般情況下,按如下的方式使用緩存功能:當業務系統發起某一個查詢請求時,首先判斷緩存中是否有該數據;如果緩存中存在,則直接返回數據;如果緩存中不存在,則再查詢數據庫,然后返回數據。
- 緩存穿透
- 描述:業務系統要查詢的數據根本就存在,當業務系統發起查詢時,按照上述流程,首先會前往緩存中查詢,由於緩存中不存在,然后再前往數據庫中查詢。由於該數據壓根就不存在,因此數據庫也返回空。這就是緩存穿透。如果在高並發場景中大量的緩存穿透,請求直接落到存儲層數據庫,稍微不慎后端系統就會被壓垮。
- 出現原因:惡意攻擊,故意營造大量不存在的數據請求服務,由於緩存中並不存在這些數據,因此海量請求均落在數據庫中。
- 針對緩存穿透的優化方案
- 緩存空對象:發生緩存穿透,是因為緩存中沒有存儲這些空數據的key,導致這些請求全都到數據庫。那么,可以將數據庫查詢結果為空的key也存儲在緩存中,當后續又出現該key的查詢請求時,緩存直接返回null,而無需查詢數據庫。不過這個方案還是不能應對大量高並發且不相同的緩存穿透,如果在清楚業務有效范圍情況下,一瞬間發起大量不相同的請求,第一次查詢還是會穿透到DB。另外這個方案的一種缺點就是:每一次不同的緩存穿透,緩存一個空對象,導致大量不同的穿透緩存大量空對象,內存被大量白白占用,使真正有效的數據不能被緩存起來。所以對於這種方案需要做到:第一,做好業務過濾。將不屬於業務范圍內的請求過濾掉,系統直接返回,直接不走查詢。第二,給緩存的空對象設置一個較短的過期時間,在內存空間不足時可以被有效快速清除。
- 布隆過濾器:布隆過濾器是一種結合hash函數與bitmap的一種數據結構。它實際上是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否在一個集合中,它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率和刪除困難。當業務系統有查詢請求的時候,首先去BloomFilter中查詢該key是否存在,若不存在,則說明數據庫中也不存在該數據,因此緩存都不用查,直接返回null;若存在,則繼續執行后續的流程,先前往緩存中查詢,緩存中沒有的話再前往數據庫中的查詢。
- 一般情況下,建議對於空數據的key各不相同、key重復請求概率低的場景而言,應該選擇第二種方案;而對於空數據的key數量有限、key重復請求概率較高的場景而言,應該選擇第一種方案。適當情況下,布隆過濾器和緩存空對象也是完全可以結合起來的,布隆過濾器用本地緩存實現,內存占用極低,不命中時再走redis/memcache這種遠程緩存查詢。
- 緩存雪崩
- 描述:當緩存服務器重啟或者大量緩存集中在某一個時間段失效,這樣在失效的時候,也會給后端系統(比如DB)帶來很大壓力,造成數據庫后端故障,從而引起應用服務器雪崩。
- 出現情景:緩存服務器掛了;高峰期緩存局部失效;熱點緩存失效等等原因
- 緩存雪崩優化方案:
- 保證緩存層服務的高可用性,比如一主多從,Redis Sentine機制。
- 依賴隔離組件為后端限流並降級,比如netflix的hystrix。
- 項目資源隔離。避免某個項目的bug,影響了整個系統架構,有問題也局限在項目內部。
- 避免緩存集中失效,緩存數據的過期時間設置隨機,防止同一時間大量數據過期現象發生。
- 如果可以,則設置熱點數據永遠不過期。
- (緩存擊穿) 熱點數據集中失效
- 描述:緩存擊穿是指一個key非常熱點,在不停的扛着大並發,大並發集中對這一個點進行訪問,當這個key在失效的瞬間,持續的大並發就穿破緩存,直接請求數據庫,導致數據庫扛不住高並發量而引起宕機。
- 緩存擊穿優化方案:
- 在現實情況下,很難對數據庫服務器造成壓垮性的壓力,類似緩存擊穿是不常發生的。如果真的存在這種情況,簡單的做法就是讓熱門數據緩存永不過期即可。
- 使用互斥鎖。緩存中沒有數據,獲取鎖並從數據庫去取數據,防止短時間內過多的請求發往數據庫,導致數據庫掛掉。
- 緩存無底洞問題(此問題不常發生,由facebook發現的,此處不記錄,有篇博客寫的很不錯:https://blog.csdn.net/yonggeit/article/details/72862134)
memcache實現分布式緩存系統
memcache是一款開源的高性能的分布式內容對象緩存系統,被許多大型網站所采用,用於在應用中減少對數據庫的訪問,提高應用的訪問速度,並降低數據庫的負載。為了在內存中提供數據的高速查找能力,memcache使用key-value形式存儲和訪問數據,在內存中維護一張巨大的HashTable,使得對數據查詢的時間復雜度降低到O(1),保證了對數據的高性能訪問。內存的空間總是有限的,當內存沒有更多的空間來存儲新的數據時,memcache就會使用LRU(Least Recently Used)算法,將最近不常訪問的數據淘汰掉,以騰出空間來存放新的數據。memcache存儲支持的數據格式也是靈活多樣的,通過對象的序列化機制,可以將更高層的對象轉換成為二進制數據,存儲在緩存服務器中,當前端應用需要時,又可以通過二進制內容反序列化,將數據還原成原有對象。memcache客戶端與服務端通過構建在TCP協議之上的memcache協議來進行通信,協議支持兩種數據的傳遞,這兩種數據分別為文本行和非結構化數據。文本行主要用來承載客戶端的命令及服務端的響應,而非結構化數據則主要用於客戶端和服務端數據的傳遞。由於非結構化數據采用字節流的形式在客戶端和服務端之間進行傳輸和存儲,因此使用方式非常靈活,緩存數據存儲幾乎沒有任何限制,並且服務端也不需要關心存儲的具體內容及字節序。
memcache本身並不是一種分布式的緩存系統,它的分布式是由訪問它的客戶端來實現的。一種比較簡單的實現方式是根據緩存的key來進行Hash,當后端有N台緩存服務器時,訪問的服務器為hash(key)%N,這樣可以將前端的請求均衡地映射到后端的緩存服務器。但這樣也會導致一個問題,一旦后端某台緩存服務器宕機,或者是由於集群壓力過大,需要新增緩存服務器時,大部分的key將會重新分布。對於高並發系統來說,這可能會演變成一場災難,所有的請求將如洪水般瘋狂地涌向后端的數據庫服務器,而數據庫服務器的不可用,將會導致整個應用的不可用,形成所謂的“雪崩效應”。
使用consistent Hash算法能夠在一定程度上改善上述問題。該算法早在1997年就在論文Consistent hashing and random trees中被提出,它能夠在移除/添加一台緩存服務器時,盡可能小地改變已存在的key映射關系,避免大量key的重新映射。consistent Hash的原理是這樣的,它將Hash函數的值域空間組織成一個圓環,假設Hash函數的值域空間為0~(2的32次方-1),也就是Hash值是一個32位的無符號整型,整個空間按照順時針的方向進行組織,然后對相應的服務器節點進行Hash,將他們映射到Hash環上,假設有4台服務器分別為node1,node2,node3,node4,它們在環上的位置如圖所示。
接下來使用相同的Hash函數,計算出對應的key的Hash值在環上對應的位置。根據consistent Hash算法,按照順時針方向,分布在node1與node2之間的key,它們的訪問請求會被定位到node2,而node2與node4之間的key,訪問請求會被定位到node4,以此類推。假設有新的節點node5增加進來時,假設它被Hash到node2與node4之間,那么受影響的只有node2和node5之間的key,它們將被重新映射到node5,而其他key的映射關系將不會發生改變,這樣避免了大量key的重新映射。
當然上面描繪的知識一種理想的情況,各個節點在環上分布得十分均勻。正常情況下,當節點數據較少時,節點的分布可能十分不均勻,從而導致數據訪問的傾斜,大量的key被映射到同一台服務器上。為了避免這種情況的出現,可以引入虛擬節點的機制,對每一個服務器節點都計算多個Hash值,每一個Hash值都對應環上一個節點的位置,該節點稱為虛擬節點,而key的映射方式不變,只是多了一步從虛擬節點再映射到真實節點的過程。這樣,如果虛擬節點的數量足夠多,即使只有很少的實際節點,也能夠使key分布得相對均衡。