w-tinyLFU

緩存設計是個基礎架構領域裏的重要話題,本號之前也有談論過相關話題,點擊原文可以看之前的介紹。

 

近日,HighScalability網站刊登了一篇文章,由前Google工程師發明的W-TinyLFU——一種現代的緩存。那麼,什麼緩存設計能夠被稱作是“現代”的呢?

 

當數據的訪問模式不隨時間變化的時候,LFU的策略能夠帶來最佳的緩存命中率。然而LFU有兩個缺點:首先,它需要給每個記錄項維護頻率信息,每次訪問都需要更新,這是個巨大的開銷;其次,如果數據訪問模式隨時間有變,LFU的頻率信息無法隨之變化,因此早先頻繁訪問的記錄可能會佔據緩存,而後期訪問較多的記錄則無法被命中。因此,大多數的緩存設計都是基於LRU或者其變種來進行的,相比之下,LRU並不需要維護昂貴的緩存記錄元信息,同時也能夠反應隨時間變化的數據訪問模式。然而,在許多負載之下,LRU依然需要更多的空間才能做到跟LFU一致的緩存命中率。因此,一個“現代”的緩存,應當能夠綜合兩者的長處。

 

W-TinyLFU就是這樣一個工作,在介紹之前,先來看看TinyLFU,它是W-TinyLFU運轉的基礎。


TinyLFU維護了近期訪問記錄的頻率信息,作爲一個過濾器,當新記錄來時,只有滿足TinyLFU要求的記錄纔可以被插入緩存。如前所述,作爲現代的緩存,它需要解決兩個挑戰:一個是如何避免維護頻率信息的高開銷,另一個是如何反應隨時間變化的訪問模式。首先來看前者,TinyLFU藉助了本號之前介紹過的數據流Sketching技術,Count-Min Sketch顯然是解決這個問題的有效手段,它可以用小得多的空間存放頻率信息,而保證很低的False Positive Rate。但考慮到第二個問題,就要複雜許多了,因爲我們知道,任何Sketching數據結構如果要反應時間變化都是一件困難的事情,在Bloom Filter方面,我們可以有Timing Bloom Filter,但對於CMSketch來說,如何做到Timing CMSketch就不那麼容易了。TinyLFU採用了一種基於滑動窗口的時間衰減設計機制,藉助於一種簡易的reset操作:每次添加一條記錄到Sketch的時候,都會給一個計數器上加1,當計數器達到一個尺寸W的時候,把所有記錄的Sketch數值都除以2,該reset操作可以起到衰減的作用:


可以證明[1],reset操作帶來的頻率估計期望不變。

 

W-TinyLFU主要用來解決一些稀疏的突發訪問元素。在一些數目很少但突發訪問量很大的場景下,TinyLFU將無法保存這類元素,因爲它們無法在給定時間內積累到足夠高的頻率。因此W-TinyLFU就是結合LFU和LRU,前者用來應對大多數場景,而LRU用來處理突發流量。

HighScalability上的另一種視圖:

前端是一個小的LRU,在送到TinyLFU做過濾之後,元素存放到一個大的Segmented LRU緩存裏。前端的小LRU叫做Window LRU,它的容量只佔據1%的總空間,它的目的就是用來存放短期突發訪問記錄。存放主要元素的Segmented LRU(SLRU)是一種LRU的改進,主要把在一個時間窗口內命中至少2次的記錄和命中1次的單獨存放,這樣就可以把短期內較頻繁的緩存元素區分開來。具體做法上,SLRU包含2個固定尺寸的LRU,一個叫Probation段A1,一個叫Protection段A2。新記錄總是插入到A1中,當A1的記錄被再次訪問,就把它移到A2,當A2滿了需要驅逐記錄時,會把驅逐記錄插入到A1中。W-TinyLFU中,SLRU有80%空間被分配給A2段。

 



從實驗上可以看出,相比其他緩存策略,W-TinyLFU的緩存命中率可以達到最優。

 

W-TinyLFU的實現在Caffeine項目裏[2],除了緩存更新策略之外,另一個設計問題是併發更新,這也是本號上一篇講述緩存談論的設計問題。Caffeine採用了類似日誌的方式儘可能避免鎖的操作:寫入操作放到日誌裏然後異步批處理更新。具體而言,Caffeine利用ringbuffer存放寫入數據,待buffer滿之後做批量處理,並且爲每個線程使用獨立的ringbuffer進一步提升性能。

下邊是Java類各緩存實現的併發性能對比,差別還是很顯著的。

 

進一步細節論文介紹在[1],[2]和[3]分別是Java和Golang的對應實現,HighScalability的文章在[4],祝玩得開心~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章