LruCache在美团DSP系统中的应用演进
背景
DSP系統是互聯網廣告需求方平臺,用于承接媒體流量,投放廣告。業務特點是并發度高,平均響應低(百毫秒)。
為了能夠有效提高DSP系統的性能,美團平臺引入了一種帶有清退機制的緩存結構LruCache(Least Recently Used Cache),在目前的DSP系統中,使用LruCache + 鍵值存儲數據庫的機制將遠端數據變為本地緩存數據,不僅能夠降低平均獲取信息的耗時,而且通過一定的清退機制,也可以維持服務內存占用在安全區間。
本文將會結合實際應用場景,闡述引入LruCache的原因,并會在高QPS下的挑戰與解決方案等方面做詳細深入的介紹,希望能對DSP感興趣的同學有所啟發。
LruCache簡介
LruCache采用的緩存算法為LRU(Least Recently Used),即最近最少使用算法。這一算法的核心思想是當緩存數據達到預設上限后,會優先淘汰近期最少使用的緩存對象。
LruCache內部維護一個雙向鏈表和一個映射表。鏈表按照使用順序存儲緩存數據,越早使用的數據越靠近鏈表尾部,越晚使用的數據越靠近鏈表頭部;映射表通過Key-Value結構,提供高效的查找操作,通過鍵值可以判斷某一數據是否緩存,如果緩存直接獲取緩存數據所屬的鏈表節點,進一步獲取緩存數據。LruCache結構圖如下所示,上半部分是雙向鏈表,下半部分是映射表(不一定有序)。雙向鏈表中value_1所處位置為鏈表頭部,value_N所處位置為鏈表尾部。
LruCache讀操作,通過鍵值在映射表中查找緩存數據是否存在。如果數據存在,則將緩存數據所處節點從鏈表中當前位置取出,移動到鏈表頭部;如果不存在,則返回查找失敗,等待新數據寫入。下圖為通過LruCache查找key_2后LruCache結構的變化。
LruCache沒有達到預設上限情況下的寫操作,直接將緩存數據加入到鏈表頭部,同時將緩存數據鍵值與緩存數據所處的雙鏈表節點作為鍵值對插入到映射表中。下圖是LruCache預設上限大于N時,將數據M寫入后的數據結構。
LruCache達到預設上限情況下的寫操作,首先將鏈表尾部的緩存數據在映射表中的鍵值對刪除,并刪除鏈表尾部數據,再將新的數據正常寫入到緩存中。下圖是LruCache預設上限為N時,將數據M寫入后的數據結構。
線程安全的LruCache在讀寫操作中,全部使用鎖做臨界區保護,確保緩存使用是線程安全的。
LruCache在美團DSP系統的應用場景
在美團DSP系統中廣泛應用鍵值存儲數據庫,例如使用Redis存儲廣告信息,服務可以通過廣告ID獲取廣告信息。每次請求都從遠端的鍵值存儲數據庫中獲取廣告信息,請求耗時非常長。隨著業務發展,QPS呈現巨大的增長趨勢,在這種高并發的應用場景下,將廣告信息從遠端鍵值存儲數據庫中遷移到本地以減少查詢耗時是常見解決方案。另外服務本身的內存占用要穩定在一個安全的區間內。面對持續增長的廣告信息,引入LruCache + 鍵值存儲數據庫的機制來達到提高系統性能,維持內存占用安全、穩定的目標。
LruCache + Redis機制的應用演進
在實際應用中,LruCache + Redis機制實踐分別經歷了引入LruCache、LruCache增加時效清退機制、HashLruCache滿足高QPS應用場景以及零拷貝機制四個階段。各階段的測試機器是16核16G機器。
演進一:引入LruCache提高美團DSP系統性能
在較低QPS環境下,直接請求Redis獲取廣告信息,可以滿足場景需求。但是隨著單機QPS的增加,直接請求Redis獲取廣告信息,耗時也會增加,無法滿足業務場景的需求。
引入LruCache,將遠端存放于Redis的信息本地化存儲。LruCache可以預設緩存上限,這個上限可以根據服務所在機器內存與服務本身內存占用來確定,確保增加LruCache后,服務本身內存占用在安全范圍內;同時可以根據查詢操作統計緩存數據在實際使用中的命中率。
下圖是增加LruCache結構前后,且增加LruCache后命中率高于95%的情況下,針對持續增長的QPS得出的數據獲取平均耗時(ms)對比圖:
根據平均耗時圖顯示可以得出結論:
下圖是增加LruCache結構前后,且增加LruCache后命中率高于95%的情況下,針對持續增長的QPS得出的數據獲取Top999耗時(ms)對比圖:
根據Top999耗時圖可以得出以下結論:
引入LruCache結構,在實際使用中,在一定的QPS范圍內,確實可以有效減少數據獲取的耗時。但是QPS超出一定范圍后,平均耗時和Top999耗時都很高。所以LruCache在更高的QPS下性能還需要進一步優化。
演進二:LruCache增加時效清退機制
在業務場景中,Redis中的廣告數據有可能做修改。服務本身作為數據的使用方,無法感知到數據源的變化。當緩存的命中率較高或者部分數據在較長時間內多次命中,可能出現數據失效的情況。即數據源發生了變化,但服務無法及時更新數據。針對這一業務場景,增加了時效清退機制。
時效清退機制的組成部分有三點:設置緩存數據過期時間,緩存數據單元增加時間戳以及查詢中的時效性判斷。緩存數據單元將數據進入LruCache的時間戳與數據一起緩存下來。緩存過期時間表示緩存單元緩存的時間上限。查詢中的時效性判斷表示查詢時的時間戳與緩存時間戳的差值超過緩存過期時間,則強制將此數據清空,重新請求Redis獲取數據做緩存。
在查詢中做時效性判斷可以最低程度的減少時效判斷對服務的中斷。當LruCache預設上限較低時,定期做全量數據清理對于服務本身影響較小。但如果LruCache的預設上限非常高,則一次全量數據清理耗時可能達到秒級甚至分鐘級,將嚴重阻斷服務本身的運行。所以將時效性判斷加入到查詢中,只對單一的緩存單元做時效性判斷,在服務性能和數據有效性之間做了折中,滿足業務需求。
演進三:高QPS下HashLruCache的應用
LruCache引入美團DSP系統后,在一段時間內較好地支持了業務的發展。隨著業務的迭代,單機QPS持續上升。在更高QPS下,LruCache的查詢耗時有了明顯的提高,逐漸無法適應低平響的業務場景。在這種情況下,引入了HashLruCache機制以解決這個問題。
LruCache在高QPS下的耗時增加原因分析:
線程安全的LruCache中有鎖的存在。每次讀寫操作之前都有加鎖操作,完成讀寫操作之后還有解鎖操作。在低QPS下,鎖競爭的耗時基本可以忽略;但是在高QPS下,大量的時間消耗在了等待鎖的操作上,導致耗時增長。
HashLruCache適應高QPS場景:
針對大量的同步等待操作導致耗時增加的情況,解決方案就是盡量減小臨界區。引入Hash機制,對全量數據做分片處理,在原有LruCache的基礎上形成HashLruCache,以降低查詢耗時。
HashLruCache引入某種哈希算法,將緩存數據分散到N個LruCache上。最簡單的哈希算法即使用取模算法,將廣告信息按照其ID取模,分散到N個LruCache上。查詢時也按照相同的哈希算法,先獲取數據可能存在的分片,然后再去對應的分片上查詢數據。這樣可以增加LruCache的讀寫操作的并行度,減小同步等待的耗時。
下圖是使用16分片的HashLruCache結構前后,且命中率高于95%的情況下,針對持續增長的QPS得出的數據獲取平均耗時(ms)對比圖:
根據平均耗時圖可以得出以下結論:
下圖是使用16分片的HashLruCache結構前后,且命中率高于95%的情況下,針對持續增長的QPS得出的數據獲取Top999耗時(ms)對比圖:
根據Top999耗時圖可以得出以下結論:
引入HashLruCache結構后,在實際使用中,平均耗時和Top999耗時都有非常明顯的下降,效果非常顯著。
HashLruCache分片數量確定:
根據以上分析,進一步提高HashLruCache性能的一個方法是確定最合理的分片數量,增加足夠的并行度,減少同步等待消耗。所以分片數量可以與CPU數量一致。由于超線程技術的使用,可以將分片數量進一步提高,增加并行性。
下圖是使用HashLruCache機制后,命中率高于95%,不同分片數量在不同QPS下得出的數據獲取平均耗時(ms)對比圖:
平均耗時圖顯示,在較高的QPS下,平均耗時并沒有隨著分片數量的增加而有明顯的減少,基本維持穩定的狀態。
下圖是使用HashLruCache機制后,命中率高于95%,不同分片數量在不同QPS下得出的數據獲取Top999耗時(ms)對比圖:
Top999耗時圖顯示,QPS為750時,分片數量從8增長到16再增長到24時,Top999耗時有一定的下降,并不顯著;QPS為1000時,分片數量從8增長到16有明顯下降,但是從16增長到24時,基本維持了穩定狀態。明顯與實際使用的機器CPU數量有較強的相關性。
HashLruCache機制在實際使用中,可以根據機器性能并結合實際場景的QPS來調節分片數量,以達到最好的性能。
演進四:零拷貝機制
線程安全的LruCache內部維護一套數據。對外提供數據時,將對應的數據完整拷貝一份提供給調用方使用。如果存放結構簡單的數據,拷貝操作的代價非常小,這一機制不會成為性能瓶頸。但是美團DSP系統的應用場景中,LruCache中存放的數據結構非常復雜,單次的拷貝操作代價很大,導致這一機制變成了性能瓶頸。
理想的情況是LruCache對外僅僅提供數據地址,即數據指針。使用方在業務需要使用的地方通過數據指針獲取數據。這樣可以將復雜的數據拷貝操作變為簡單的地址拷貝,大量減少拷貝操作的性能消耗,即數據的零拷貝機制。直接的零拷貝機制存在安全隱患,即由于LruCache中的時效清退機制,可能會出現某一數據已經過期被刪除,但是使用方仍然通過持有失效的數據指針來獲取該數據。
進一步分析可以確定,以上問題的核心是存放于LruCache的數據生命周期對于使用方不透明。解決這一問題的方案是為LruCache中存放的數據添加原子變量的引用計數。使用原子變量不僅確保了引用計數的線程安全,使得各個線程讀取的引用計數一致,同時保證了并發狀態最小的同步性能開銷。不論是LruCache中還是使用方,每次獲取數據指針時,即將引用計數加1;同理,不再持有數據指針時,引用計數減1。當引用計數為0時,說明數據沒有被任何使用方使用,且數據已經過期從LruCache中被刪除。這時刪除數據的操作是安全的。
下圖是使零拷貝機制后,命中率高于95%,不同QPS下得出的數據獲取平均耗時(ms)對比圖:
平均耗時圖顯示,使用零拷貝機制后,平均耗時下降幅度超過60%,效果非常顯著。
下圖是使零拷貝機制后,命中率高于95%,不同QPS下得出的數據獲取Top999耗時(ms)對比圖:
根據Top999耗時圖可以得出以下結論:
引入零拷貝機制后,通過拷貝指針替換拷貝數據,大量降低了獲取復雜業務數據的耗時,同時將臨界區減小到最小。線程安全的原子變量自增與自減操作,目前在多個基礎庫中都有實現,例如C++11就提供了內置的整型原子變量,實現線程安全的自增與自減操作。
在HashLruCache中引入零拷貝機制,可以進一步有效降低平均耗時和Top999耗時,且在高QPS下對于穩定Top999耗時有非常好的效果。
總結
下圖是一系列優化措施前后,命中率高于95%,不同QPS下得出的數據獲取平均耗時(ms)對比圖:
平均耗時圖顯示,優化后的平均耗時僅為優化前的20%以內,性能提升非常明顯。優化后平均耗時對于QPS的增長敏感度更低,更好的支持了高QPS的業務場景。
下圖是一系列優化措施前后,命中率高于95%,不同QPS下得出的數據獲取Top999耗時(ms)對比圖:
Top999耗時圖顯示,優化后的Top999耗時僅為優化前的20%以內,對于長尾請求的耗時有非常明顯的降低。
LruCache是一個非常常見的數據結構。在美團DSP的高QPS業務場景下,發揮了重要的作用。為了符合業務需要,在原本的清退機制外,補充了時效性強制清退機制。隨著業務的發展,針對更高QPS的業務場景,使用HashLruCache機制,降低緩存的查詢耗時。針對不同的具體場景,在不同的QPS下,不斷嘗試更合理的分片數量,不斷提高HashLruCache的查詢性能。通過引用計數的方案,在HashLruCache中引入零拷貝機制,進一步大幅降低平均耗時和Top999耗時,更好的服務于業務場景的發展。
作者簡介
- 王粲,2018年11月加入美團,任職美團高級工程師,負責美團DSP系統后端基礎架構的研發工作。
- 崔濤,2015年6月加入美團,任職資深廣告技術專家,期間一手指導并從0到1搭建美團DSP投放平臺,具備豐富的大規模計算引擎的開發和性能優化經驗。
- 霜霜,2015年6月加入美團,任職美團高級工程師,美團DSP系統后端基礎架構與機器學習架構負責人,全面負責DSP業務廣告召回和排序服務的架構設計與優化。
招聘
美團在線營銷DSP團隊誠招工程、算法、數據等各方向精英,發送簡歷至cuitao@meituan.com,共同支持百億級流量的高可靠系統研發與優化。
總結
以上是生活随笔為你收集整理的LruCache在美团DSP系统中的应用演进的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 美团集群调度系统HULK技术演进
- 下一篇: Spring Cloud 2020年路线