[譯] 設計一個現代的緩存

本文來自阿里集團客戶體驗事業羣 簡直同窗的投稿,簡直基於工做場景對於緩存作了一些研究,並翻譯了一篇文章供同道中人學習。html


原文:http://highscalability.com/bl...git

緩存是提高性能的通用方法,如今大多數的緩存實現都使用了經典的技術。這篇文章中,咱們會發掘Caffeine中的現代的實現方法。Caffeine是一個開源的Java緩存庫,它能提供高命中率和出色的併發能力。指望讀者們能被這些想法激發,進而將它們應用到任何你喜歡的編程語言中。github

驅逐策略

緩存的驅逐策略是爲了預測哪些數據在短時間內最可能被再次用到,從而提高緩存的命中率。因爲簡潔的實現、高效的運行時表現以及在常規的使用場景下有不錯的命中率,LRU(Least Recently Used)策略或許是最流行的驅逐策略。但LRU經過歷史數據來預測將來是侷限的,它會認爲最後到來的數據是最可能被再次訪問的,從而給與它最高的優先級。web

現代緩存擴展了對歷史數據的使用,結合就近程度(recency)和訪問頻次(frequency)來更好的預測數據。其中一種保留歷史信息的方式是使用popularity sketch(一種壓縮、機率性的數據結構)來從一大堆訪問事件中定位頻繁的訪問者。能夠參考CountMin Sketch算法,它由計數矩陣和多個哈希方法實現。發生一次讀取時,矩陣中每行對應的計數器增長計數,估算頻率時,取數據對應是全部行中計數的最小值。這個方法讓咱們從空間、效率、以及適配矩陣的長寬引發的哈希碰撞的錯誤率上作權衡。算法

CountMinSketch算法.jpg

Window TinyLFU(W-TinyLFU)算法將sketch做爲過濾器,當新來的數據比要驅逐的數據高頻時,這個數據纔會被緩存接納。這個許可窗口給予每一個數據項積累熱度的機會,而不是當即過濾掉。這避免了持續的未命中,特別是在忽然流量暴漲的的場景中,一些短暫的重複流量就不會被長期保留。爲了刷新歷史數據,一個時間衰減進程被週期性或增量的執行,給全部計數器減半。
Window TinyLFU.jpg數據庫

對於長期保留的數據,W-TinyLFU使用了分段LRU(Segmented LRU,縮寫SLRU)策略。起初,一個數據項存儲被存儲在試用段(probationary segment)中,在後續被訪問到時,它會被提高到保護段(protected segment)中(保護段佔總容量的80%)。保護段滿後,有的數據會被淘汰回試用段,這也可能級聯的觸發試用段的淘汰。這套機制確保了訪問間隔小的熱數據被保存下來,而被重複訪問少的冷數據則被回收。編程

緩存測試結果.jpg

如圖中數據庫和搜索場景的結果展現,經過考慮就近程度和頻率能大大提高LRU的表現。一些高級的策略,像ARCLIRSW-TinyLFU都提供了接近最理想的命中率。想看更多的場景測試,請查看相應的論文,也能夠在使用simulator來測試本身的場景。緩存

過時策略

過時的實現裏,每每每一個數據項擁有不一樣的過時時間。由於容量的限制,過時後數據須要被懶淘汰,不然這些已過時的髒數據會污染到整個緩存。通常緩存中會啓用專有的清掃線程週期性的遍歷清理緩存。這個策略相比在每次讀寫操做時按照過時時間排序的優先隊列來清理過時緩存要好,由於後臺線程隱藏了的過時數據清除的時間開銷。數據結構

鑑於大多數場景裏不一樣數據項使用的都是固定的過時時長,Caffien採用了統一過時時間的方式。這個限制讓用O(1)的有序隊列組織數據成爲可能。針對數據的寫後過時,維護了一個寫入順序隊列,針對讀後過時,維護了一個讀取順序隊列。緩存能複用驅逐策略下的隊列以及下面將要介紹的併發機制,讓過時的數據項在緩存的維護階段被拋棄掉。併發

併發

因爲在大多數的緩存策略中,數據的讀取都會伴隨對緩存狀態的寫操做,併發的緩存讀取被視爲一個難點問題。傳統的解決方式是用同步鎖。這能夠經過將緩存的數據劃成多個分區來進行鎖拆分優化。不幸的是熱點數據所持有的鎖會比其餘數據更常的被佔有,在這種場景下鎖拆分的性能提高也就沒那麼好了。當單個鎖的競爭成爲瓶頸後,接下來的經典的優化方式是隻更新單個數據的元數據信息,以及使用隨機採樣基於FIFO的驅逐策略來減小數據操做。這些策略會帶來高性能的讀和低性能的寫,同時在選擇驅逐對象時也比較困難。

另外一種可行方案來自於數據庫理論,經過提交日誌的方式來擴展寫的性能。寫入操做先記入日誌中,隨後異步的批量執行,而不是當即寫入到數據結構中。這種思想能夠應用到緩存中,執行哈希表的操做,將操做記錄到緩衝區,而後在合適的時機執行緩衝區中的內容。這個策略依然須要同步鎖或者tryLock,不一樣的是把對鎖的競爭轉移到對緩衝區的追加寫上。

在Caffeine中,有一組緩衝區被用來記錄讀寫。一次訪問首先會被因線程而異的哈希到stripped ring buffer上,當檢測到競爭時,緩衝區會自動擴容。一個ring buffer容量滿載後,會觸發異步的執行操做,然後續的對該ring buffer的寫入會被丟棄,直到這個ring buffer可被使用。雖然由於ring buffer容量滿而沒法被記錄該訪問,但緩存值依然會返回給調用方。這種策略信息的丟失不會帶來大的影響,由於W-TinyLFU能識別出咱們但願保存的熱點數據。經過使用因線程而異的哈希算法替代在數據項的鍵上作哈希,緩存避免了瞬時的熱點key的競爭問題。

StippedReadBuffer.jpg

寫數據時,採用更傳統的併發隊列,每次變動會引發一次當即的執行。雖然數據的損失是不可接受的,但咱們仍然有不少方法能夠來優化寫緩衝區。全部類型的緩衝區都被多個的線程寫入,但卻經過單個線程來執行。這種多生產者/單個消費者的模式容許了更簡單、高效的算法來實現。

緩衝區和細粒度的寫帶來了單個數據項的操做亂序的競態條件。插入、讀取、更新、刪除均可能被各類順序的重放,若是這個策略控制的不合適,則可能引發懸垂索引。解決方案是經過狀態機來定義單個數據項的生命週期。

StippedReadBuffer.jpg

基準測試中,緩衝區隨着哈希表的增加而增加,它的的使用相對更節省資源。讀的性能隨着CPU的核數線性增加,是哈希表吞吐量的33%。寫入有10%的性能損耗,這是由於更新哈希表時的競爭是最主要的開銷。

併發基準測試.jpg

結論

還有許多實用的話題沒有被覆蓋到。包括最小化內存的技巧,當複雜度上升時保證質量的測試技術以及肯定優化是否值得的性能分析方法。這些都是緩存的實踐者須要關注的點,由於一旦這些被忽視,就很難重拾掌控緩存帶來的複雜度的信心。

Caffeine的設計實現來自於大量的洞見和許多貢獻者的共同努力。它這些年的演化離不開一些人的幫助:Charles Fry, Adam Zell, Gil Einziger, Roy Friedman, Kevin Bourrillion, Bob Lee, Doug Lea, Josh Bloch, Bob Lane, Nitsan Wakart, Thomas Müeller, Dominic Tootell, Louis Wasserman, and Vladimir Blagojevic. Thanks to Nitsan Wakart, Adam Zell, Roy Friedman, and Will Chu for their feedback on drafts of this article.

相關文章
相關標籤/搜索