Helios多級緩存實(shí)踐
1.緩存定位
在講述緩存之前,我們首先需要確認(rèn)系統(tǒng)是否真的需要緩存。而引入緩存的理由,總結(jié)起來無外乎以下兩種:
為緩解 CPU 壓力而做緩存:譬如把方法運(yùn)行結(jié)果存儲起來、把原本要實(shí)時計算的內(nèi)容提前算好、把一些公用的數(shù)據(jù)進(jìn)行復(fù)用,這可以節(jié)省 CPU 算力,順帶提升響應(yīng)性能。
- ?為緩解 I/O 壓力而做緩存:譬如把原本對網(wǎng)絡(luò)、磁盤等較慢介質(zhì)的讀寫訪問變?yōu)閷?nèi)存等較快介質(zhì)的訪問,將原本對單點(diǎn)組件(如數(shù)據(jù)庫)的讀寫訪問變?yōu)榈娇蓴U(kuò)縮部件(如緩存中間件)的訪問,順帶提升響應(yīng)性能。
- 緩存是典型以空間換時間來提升性能的手段,一般引入緩存的出發(fā)點(diǎn)都是緩解 CPU 和 I/O 資源在峰值流量下的壓力,進(jìn)而提升系統(tǒng)的響應(yīng)性能。
2.緩存屬性
目前 ,“緩存”其實(shí)已經(jīng)被看作一項(xiàng)技術(shù)基礎(chǔ)設(shè)施,針對該種基礎(chǔ)設(shè)施,除了緩存基本的存儲與讀取能力,通用、高效、可統(tǒng)計、可管理等方面的需求也被重視。通常,我們設(shè)計或者選擇緩存至少會考慮以下四個維度的屬性:
- 吞吐量:緩存的吞吐量使用 OPS 值(每秒操作數(shù),Operations per Second,ops/s)來衡量,反映了對緩存進(jìn)行并發(fā)讀、寫操作的效率,即緩存本身的工作效率高低。
- 命中率:緩存的命中率即成功從緩存中返回結(jié)果次數(shù)與總請求次數(shù)的比值,反映了引入緩存的價值高低,命中率越低,引入緩存的收益越小,價值越低。
- 擴(kuò)展功能:緩存除了基本讀寫功能外,還提供哪些額外的管理功能,譬如最大容量、失效時間、失效事件、命中率統(tǒng)計,等等。
- 分布式支持:緩存可分為“進(jìn)程內(nèi)緩存”和“分布式緩存”兩大類,前者只為節(jié)點(diǎn)本身提供服務(wù),無網(wǎng)絡(luò)訪問操作,速度快但緩存的數(shù)據(jù)不能在各個服務(wù)節(jié)點(diǎn)中共享,后者則相反。
3.本地緩存
下面圍繞幾個主流的本地緩存,HashMap, Guava, Ehcache, Caffeine對上述屬性進(jìn)行簡單介紹。
吞吐量:因?yàn)樯婕暗讲l(fā)讀寫,所以對于吞吐量影響最大的即是并發(fā)訪問方式。最原始的 HashMap緩存,因?yàn)闆]有進(jìn)行并發(fā)訪問控制,其吞吐量最高, 但也決定了其無法在多線程并發(fā)下正確地工作;后續(xù)線程安全版本ConcurrentHashMap 采用分段加鎖的方式進(jìn)行了訪問控制;ConcurrentHashMap的并發(fā)訪問用于解決并發(fā)讀寫時的數(shù)據(jù)丟失,而在其他幾種本地緩存的設(shè)計中,因?yàn)樯婕暗綌?shù)據(jù)淘汰與驅(qū)逐能力,其主要的數(shù)據(jù)競爭源于讀取數(shù)據(jù)的同時,也會伴隨著對數(shù)據(jù)狀態(tài)的寫入操作,寫入數(shù)據(jù)的同時,也會伴隨著數(shù)據(jù)狀態(tài)的讀取操作。針對這種一種是以 Guava Cache 為代表的同步處理機(jī)制,即在訪問數(shù)據(jù)時一并完成緩存淘汰、統(tǒng)計、失效等狀態(tài)變更操作,通過分段加鎖等優(yōu)化手段來盡量減少競爭。另一種是以 Caffeine 為代表的異步日志提交機(jī)制,將對數(shù)據(jù)的讀、寫過程看作是日志(即對數(shù)據(jù)的操作指令)的提交過程,然后通過異步批量處理的方式降低鎖的并發(fā)訪問。下圖是Caffeine官方文檔中壓測得到的吞吐量數(shù)據(jù)。
命中率:主要用于最大化有限物理內(nèi)存的使用價值。優(yōu)秀的緩存需要能夠自動地實(shí)現(xiàn)淘汰低價值數(shù)據(jù),而該能力則會涉及到不同的淘汰策略。目前,最基礎(chǔ)的淘汰策略實(shí)現(xiàn)方案有以下三種:
- FIFO(First In First Out):優(yōu)先淘汰最早進(jìn)入被緩存的數(shù)據(jù)。
- LRU(Least Recent Used):優(yōu)先淘汰最久未被使用訪問過的數(shù)據(jù)。對大多數(shù)的緩存場景來說,LRU 都明顯要比 FIFO 策略合理,尤其適合用來處理短時間內(nèi)頻繁訪問的熱點(diǎn)對象。但相反,它的問題是如果一些熱點(diǎn)數(shù)據(jù)在系統(tǒng)中經(jīng)常被頻繁訪問,但最近一段時間因?yàn)槟撤N原因未被訪問過,此時這些熱點(diǎn)數(shù)據(jù)依然要面臨淘汰的命運(yùn),LRU 依然可能錯誤淘汰價值更高的數(shù)據(jù)。
- LFU(Least Frequently Used):優(yōu)先淘汰最不經(jīng)常使用的數(shù)據(jù)。LFU 可以解決上面 LRU 中熱點(diǎn)數(shù)據(jù)間隔一段時間不訪問就被淘汰的問題,但同時它又引入了兩個新的問題,首先是需要對每個緩存的數(shù)據(jù)專門去維護(hù)一個計數(shù)器,每次訪問都要更新,存在高昂的維護(hù)開銷;另一個問題是無法有效反應(yīng)隨時間變化的熱度變化信息,譬如某個曾經(jīng)頻繁訪問的數(shù)據(jù)現(xiàn)在不需要了,它也很難自動被清理出緩存
在此之上,針對LFU策略最近又衍生出了以下兩種變形:
- TinyLFU(Tiny Least Frequently Used):TinyLFU 是 LFU 的改進(jìn)版本。為了緩解 LFU 每次訪問都要修改計數(shù)器所帶來的性能負(fù)擔(dān),TinyLFU 會首先采用 Sketch 對訪問數(shù)據(jù)進(jìn)行分析,關(guān)于sketch的詳細(xì)原理讀者可自行參考Count–min sketch
- W-TinyLFU(Windows-TinyLFU):W-TinyLFU 又是 TinyLFU 的改進(jìn)版本。TinyLFU 在實(shí)現(xiàn)減少計數(shù)器維護(hù)頻率的同時,也帶來了無法很好地應(yīng)對稀疏突發(fā)訪問的問題,所謂稀疏突發(fā)訪問是指有一些絕對頻率較小,但突發(fā)訪問頻率很高的數(shù)據(jù),譬如某些運(yùn)維性質(zhì)的任務(wù),也許一天、一周只會在特定時間運(yùn)行一次,其余時間都不會用到,此時 TinyLFU 就很難讓這類元素通過 Sketch 的過濾,因?yàn)樗鼈儫o法在運(yùn)行期間積累到足夠高的頻率。應(yīng)對短時間的突發(fā)訪問是 LRU 的強(qiáng)項(xiàng),W-TinyLFU 就結(jié)合了 LRU 和 LFU 兩者的優(yōu)點(diǎn),從整體上看是它是 LFU 策略,從局部實(shí)現(xiàn)上看又是 LRU 策略。具體做法是將新記錄暫時放入一個名為 Window Cache 的前端 LRU 緩存里面,讓這些對象可以在 Window Cache 中累積熱度,如果能通過 TinyLFU 的過濾器,再進(jìn)入名為 Main Cache 的主緩存中存儲,主緩存根據(jù)數(shù)據(jù)的訪問頻繁程度分為不同的段(LFU 策略,實(shí)際上 W-TinyLFU 只分了兩段),但單獨(dú)某一段局部來看又是基于 LRU 策略去實(shí)現(xiàn)的(稱為 Segmented LRU)。每當(dāng)前一段緩存滿了之后,會將低價值數(shù)據(jù)淘汰到后一段中去存儲,直至最后一段也滿了之后,該數(shù)據(jù)就徹底清理出緩存。下圖是W-TinyLFU驅(qū)逐算法的原理圖,詳細(xì)的介紹以及在Caffeine中的應(yīng)用可以參閱caffeine的官方設(shè)計文檔。
擴(kuò)展功能:是基礎(chǔ)數(shù)據(jù)讀寫功能之外的額外功能。主要側(cè)重于監(jiān)控統(tǒng)計能力,過期控制,容量控制,引用方式等。
分布式支持:Caffeine只作為本地進(jìn)程內(nèi)緩存,而Ehcache則演變?yōu)橥瑫r能夠支持分布式部署的模式。另外,Ehcache在3.x也支持了堆外緩存的能力,而該能力在本地緩存在GB以上,且對RT敏感的場景就有了用武之地。反觀Caffeine,則更聚焦于單實(shí)例本地進(jìn)程堆內(nèi)緩存。
4.分布式緩存
在微服務(wù)的背景下,Ehcache、Infinispan 等也演進(jìn)為能夠同時支持分布式部署和進(jìn)程內(nèi)嵌部署的緩存方案。Ehcache類的緩存共享方案是通過RMI或者Jgroup多播方式進(jìn)行廣播緩存通知更新,緩存共享復(fù)雜,維護(hù)不方便;簡單的共享可以,但是涉及到緩存恢復(fù),大數(shù)據(jù)緩存,則不合適。
對分布式緩存來說,處理與網(wǎng)絡(luò)相關(guān)的操作是對吞吐量影響更大的因素,目前Redis已經(jīng)成為分布式緩存技術(shù)的首選,我們暫不對Redis分布式緩存技術(shù)做過多的探討,有興趣的讀者可以參閱相關(guān)官網(wǎng)和技術(shù)書籍。
5.多級緩存
分布式緩存與進(jìn)程內(nèi)的本地緩存各有所長,也有各有局限,它們是互補(bǔ)而非競爭的關(guān)系,如有需要,完全可以同時把進(jìn)程內(nèi)緩存和分布式緩存互相搭配,構(gòu)成透明多級緩存(Transparent Multilevel Cache,TMC)。
典型的多級緩存結(jié)構(gòu)如下圖所示,使用進(jìn)程內(nèi)緩存作為一級緩存,分布式緩存作為二級緩存,DB等其他數(shù)據(jù)源作為三級緩存。應(yīng)用進(jìn)程首先讀取一級緩存,未命中的情況下讀取二級緩存并回填數(shù)據(jù)到一級緩存。如果二級緩存也查詢不到,就發(fā)起對最終源的查詢,將結(jié)果回填到一、二級緩存中去。各級緩存數(shù)據(jù)的讀取命中率依次是: 進(jìn)程內(nèi)緩存 > 分布式緩存 > 數(shù)據(jù)源。
對應(yīng)于上述抽象的多級緩存結(jié)構(gòu),Helios多級緩存的架構(gòu)設(shè)計圖如下所示:
在Helios多級緩存的設(shè)計中,緩存邊界被定義為:
用戶觸發(fā)的數(shù)據(jù)讀取99.9%以上只走本地緩存,極少數(shù)miss到分布式緩存或DB 。
DB只會被分布式調(diào)度任務(wù)訪問,用以將最新的數(shù)據(jù)刷新到分布式緩存。
分布式緩存絕大部分情況只會被本地緩存和reload任務(wù)訪問,用以中轉(zhuǎn)最新的數(shù)據(jù)。
同時采用了如下的分層刷新機(jī)制:
- 分布式調(diào)度從數(shù)據(jù)庫reload最新的數(shù)據(jù)覆蓋分布式緩存;有條件進(jìn)行增量更新的緩存數(shù)據(jù)基于變更事件觸發(fā),分布式調(diào)度全量reload兜底。
- 本地調(diào)度定期從分布式緩存同步數(shù)據(jù)覆蓋本地緩存。
下文將聚焦于在Helios多級緩存建設(shè)中的一些實(shí)踐。
5.1 緩存一致性
緩存意味著副本,就必然存在著各數(shù)據(jù)副本之間的一致性問題。而從多級緩存中,存在著本地進(jìn)程內(nèi)緩存與分布式緩存的一致性問題,分布式緩存與DB等外部數(shù)據(jù)源的一致性問題。
關(guān)于本地進(jìn)程內(nèi)緩存與分布式緩存的一致性問題,因?yàn)楸镜剡M(jìn)程內(nèi)的緩存一般是分布式多實(shí)例的結(jié)點(diǎn),所以一般做法是數(shù)據(jù)發(fā)生變動時,在集群內(nèi)發(fā)送推送通知(簡單點(diǎn)的話可采用 Redis 的 PUB/SUB,或者M(jìn)Q的廣播消息機(jī)制,嚴(yán)謹(jǐn)?shù)脑捯?ZooKeeper 或 Etcd 來處理),讓各個節(jié)點(diǎn)的一級緩存自動失效或者刷新。
關(guān)于分布式緩存和DB等外部數(shù)據(jù)源之間的一致性問題,二者皆有成熟的數(shù)據(jù)訪問接口,無需考慮分布式多實(shí)例之間的數(shù)據(jù)復(fù)制問題。問題的復(fù)雜性在于如何保證并發(fā)讀寫情況下的一致性。更新緩存的的Design Pattern有基礎(chǔ)的有四種:cache aside, Read through, Write through, Write behind caching。其中最經(jīng)常使用,成本最低的 Cache Aside 模式邏輯如下:
- 失效:應(yīng)用程序先從cache取數(shù)據(jù),沒有得到,則從數(shù)據(jù)庫中取數(shù)據(jù),成功后,放到緩存中。
- 命中:應(yīng)用程序從cache中取數(shù)據(jù),取到后返回。
- 更新:先把數(shù)據(jù)存到數(shù)據(jù)庫中,成功后,再讓緩存失效。
關(guān)于緩存更新過程中,使用失效而非刷新主要因?yàn)楸苊舛鄠€更新請求并發(fā)操作導(dǎo)致的臟數(shù)據(jù)問題。
當(dāng)然Cache Aside 模式在極端情況下也會存在臟數(shù)據(jù)問題,針對該一致性問題,要么通過2PC或是Paxos協(xié)議保證強(qiáng)一致性,要么就是拼命的降低并發(fā)時臟數(shù)據(jù)的概率。而Facebook在論文使用了這個降低概率的玩法,因?yàn)?PC太慢,而Paxos太復(fù)雜。CacheAside更新模式是最簡單也是最常用的更新模式,但在實(shí)際應(yīng)用場景下,還需要考慮到業(yè)務(wù)側(cè)對緩存Miss的容忍度,分布式緩存更新請求失敗的兜底和補(bǔ)償策略等等。
Helios的緩存更新策略基于實(shí)際場景稍有不同, 具體更新模式如下:
- 失效:應(yīng)用程序先從cache取數(shù)據(jù),沒有取到數(shù)據(jù), 則直接返回未命中(數(shù)據(jù)不存在)
- 命中:應(yīng)用程序從cache中取數(shù)據(jù),取到后返回。
- 更新:先把數(shù)據(jù)更新存儲到數(shù)據(jù)庫中,成功后,再主動刷新分布式緩存,之后通過MQ觸發(fā)本地進(jìn)程內(nèi)緩存的刷新。
Helios多級緩存在更新策略放棄了可能出現(xiàn)的臟數(shù)據(jù),而選擇避免緩存穿透,相當(dāng)于AP模型,而cacheAside模式則屬于CP模型。在實(shí)際應(yīng)用場景下,一般情況下出現(xiàn)臟數(shù)據(jù)的概率會非常低,但是高并發(fā)和高頻更新的數(shù)據(jù),將放大出現(xiàn)臟的概率。在實(shí)際的使用場景中,為了兜底可能的失敗或者遺漏的更新請求,而增加的全量兜底功能:通過定時任務(wù)將DB等數(shù)據(jù)源的數(shù)據(jù)全量刷新到分布式緩存。而該兜底方案卻帶來了另外一個可能并發(fā)更新分布式緩存的場景。針對可能出現(xiàn)的臟數(shù)據(jù)以及其他管控訴求,Helios提供了KEY級別的手動驅(qū)逐與緩存刷新能力。同時為了降低全量兜底策略對數(shù)據(jù)不一致的影響,全量兜底策略也被設(shè)計成為支持流式更新,業(yè)務(wù)側(cè)可自主選擇更新。
緩存一致性的是無法避免的問題,也沒有絕對合適的一致性方案。未來Helios多級緩存架構(gòu)會提供多種更新模式,供業(yè)務(wù)在不同的業(yè)務(wù)場景下選擇。
5.2 緩存監(jiān)控
對于進(jìn)程內(nèi)緩存,例如Ehcache, Caffeine等都雖然都提供了成熟的訪問統(tǒng)計指標(biāo)監(jiān)控能力。但該統(tǒng)計指標(biāo)均為從運(yùn)行時刻起的累計指標(biāo),無法有效反應(yīng)隨時間變化的監(jiān)控信息。同時對于業(yè)務(wù)側(cè)關(guān)心的大KEY和熱KEY 指標(biāo)均不支持。
時間敏感的統(tǒng)計指標(biāo)監(jiān)控能力需要使用滑動時間窗口的方式進(jìn)行分窗口統(tǒng)計上報, Helios內(nèi)部使用Disruptor異步消費(fèi)監(jiān)控事件,以避免對用戶側(cè)請求的影響。同時在訪問頻率統(tǒng)計上,又借鑒了Sketch對低頻訪問數(shù)據(jù)進(jìn)行過濾,避免大量統(tǒng)計數(shù)據(jù)帶來的穩(wěn)定性風(fēng)險。
需要注意的是,異步設(shè)計往往帶來的副作用就是指標(biāo)延遲,且大流量下固定緩沖隊(duì)列往往會犧牲一部分的數(shù)據(jù)準(zhǔn)確性,這點(diǎn)在Caffeine的異步設(shè)計中也有所體現(xiàn)。
5.3 緩存熱點(diǎn)
熱點(diǎn)緩存經(jīng)常是業(yè)務(wù)需要引入多級緩存的一個重要原因,針對頻繁訪問的熱點(diǎn)數(shù)據(jù),如果每次都要從緩存服務(wù)器獲取,可能導(dǎo)致緩存服務(wù)?負(fù)載過高、或者帶寬過?。而針對熱點(diǎn)數(shù)據(jù)最直接的想法就是將數(shù)據(jù)放到使用最近的地方,也就是本地內(nèi)存。
針對緩存熱點(diǎn)最簡單有效的方式就是手動指定,提前預(yù)熱到本地緩存,比如在大促活動前手動將秒殺的商品信息提前緩存到本地進(jìn)程內(nèi)緩存。但是手動預(yù)熱的方式無法解決突發(fā)或者異常熱點(diǎn)流量。這就需要多級緩存框架能夠透明的支持熱點(diǎn)發(fā)現(xiàn)與同步機(jī)制。上文中提到過Caffeine這種本地進(jìn)程內(nèi)緩存通過優(yōu)秀的淘汰策略W-TinyLFU解決了熱點(diǎn)統(tǒng)計,衰減和稀疏突發(fā)訪問的問題,但是Caffeine本身熱點(diǎn)統(tǒng)計周期,熱度衰減策略可能無法match業(yè)務(wù)的統(tǒng)計周期,而且當(dāng)業(yè)務(wù)變更時,存在一定的時間成本優(yōu)化容量參數(shù)以平衡內(nèi)存使用和緩存命中率。
熱點(diǎn)發(fā)現(xiàn)與緩存機(jī)制整個流程必然: 熱度統(tǒng)計,熱KEY認(rèn)定,熱KEY同步與更新這3個流程。目前業(yè)界成熟的熱點(diǎn)方案主要有有贊的TMC方案以及京東的HotKey方案。二者均定位于實(shí)現(xiàn)全局熱點(diǎn)方案,且整體架構(gòu)和流程類似。以有贊的TMC解決方案為例:
- 本地實(shí)例對KEY的訪問進(jìn)行攔截,監(jiān)控統(tǒng)計,并進(jìn)行秒級別的數(shù)據(jù)上報
- 中央決策結(jié)點(diǎn)使用滑動窗口的方式聚合計算窗口內(nèi)部的熱度計算
- 通過熱點(diǎn)閾值判斷出熱點(diǎn)KEY,然后通過etcd等方式通知各應(yīng)用結(jié)點(diǎn)
- 應(yīng)用結(jié)點(diǎn)獲知熱KEY變化后,變更本地?zé)酜EY列表
全局熱點(diǎn)探測模式相較本地?zé)狳c(diǎn)探測會更加精確:特別是在微服務(wù)背景下,服務(wù)實(shí)例較多的情況下,當(dāng)單個實(shí)例探測到熱KEY時可以快速通知其他結(jié)點(diǎn);而且能夠應(yīng)對流量不均情況下的異常熱點(diǎn)KEY的發(fā)現(xiàn),而代價則是較高的通信成本和相對較高的架構(gòu)復(fù)雜性。當(dāng)前Helios熱點(diǎn)探測放棄了全局探測的模式,轉(zhuǎn)而專注于優(yōu)先探索本地?zé)狳c(diǎn)方案。因?yàn)獒槍δ壳皣?yán)選應(yīng)用環(huán)境 流量幾乎均分到每個實(shí)例,可以使用較小的準(zhǔn)入閾值來提升本地?zé)狳c(diǎn)探測的靈敏度;另外一方面,嚴(yán)選環(huán)境的當(dāng)前痛點(diǎn)是希望通過本地?zé)狳c(diǎn)自管理的方式去緩解本地緩存過熱,以及緩存參數(shù)調(diào)優(yōu)的復(fù)雜性。Helios本地?zé)狳c(diǎn)探測流程如下如所示:
- 本地訪問請求在Miss的情況下,會觸發(fā)熱點(diǎn)KEY統(tǒng)計,這里借鑒sketch的模式進(jìn)行頻率統(tǒng)計,避免頻率統(tǒng)計導(dǎo)致的內(nèi)存占用風(fēng)險
- 熱點(diǎn)準(zhǔn)入模塊會根據(jù)t-1和當(dāng)前周期t的KEY訪問頻率進(jìn)行
- 當(dāng)統(tǒng)計周期輪轉(zhuǎn)時,對內(nèi)存非熱KEY進(jìn)行驅(qū)逐,降低內(nèi)存使用水位
自動化的本地?zé)狳c(diǎn)探測,熱點(diǎn)管理非熱點(diǎn)驅(qū)逐能力降低了本地內(nèi)存的水位占用,避免出現(xiàn)本地緩存過熱的情況,同時熱點(diǎn)統(tǒng)計模塊的熱點(diǎn)KEY調(diào)用分析統(tǒng)計也可以用于指導(dǎo)緩存實(shí)例的參數(shù)調(diào)整和性能優(yōu)化。
6.總結(jié)與展望
緩存分為本地進(jìn)程內(nèi)緩存和分布式緩存,因?yàn)槎ㄎ缓褪褂脠鼍暗牟煌?,二者在技術(shù)選型時候選對象及考察點(diǎn)存在很大不同 ,而且目前二者也已呈現(xiàn)不同的演進(jìn)方向。透明多級緩存結(jié)構(gòu)則是希望結(jié)合二者的長處,通過封裝了本地進(jìn)程內(nèi)緩存和分布式緩存之間常用的使用模式,降低了多級緩存的接入與開發(fā)成本。而副作用是,本地緩存與分布式緩存Redis類接口命令協(xié)議上的差異性導(dǎo)致了多級緩存存在著諸多的限制,同時也帶來緩存一致性等問題。要實(shí)現(xiàn)多級緩存架構(gòu)的透明性仍然存在很大挑戰(zhàn),未來也將繼續(xù)在易用性,一致性,緩存治理等方面與業(yè)務(wù)側(cè)深入探討繼續(xù)前行。