背景
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)對比圖:
根據平均耗時圖顯示可以得出結論:
- QPS高於250後,直接請求Redis獲取資料的平均耗時達到10ms以上,完全無法滿足使用的需求。
- 增加LruCache結構後,耗時下降一個量級。從平均耗時角度看,QPS不高於500的情況下,耗時低於2ms。
下圖是增加LruCache結構前後,且增加LruCache後命中率高於95%的情況下,針對持續增長的QPS得出的資料獲取Top999耗時(ms)對比圖:
根據Top999耗時圖可以得出以下結論:
- 增加LruCache結構後,Top999耗時比平均耗時增長一個數量級。
- 即使是較低的QPS下,使用LruCache結構的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)對比圖:
根據平均耗時圖可以得出以下結論:
- 使用HashLruCache後,平均耗時減少將近一半,效果比較明顯。
- 對比不使用HashLruCache的平均耗時可以發現,使用HashLruCache的平均耗時對QPS的增長不敏感,沒有明顯增長。
下圖是使用16分片的HashLruCache結構前後,且命中率高於95%的情況下,針對持續增長的QPS得出的資料獲取Top999耗時(ms)對比圖:
根據Top999耗時圖可以得出以下結論:
- 使用HashLruCache後,Top999耗時減少為未使用時的三分之一左右,效果非常明顯。
- 使用HashLruCache的Top999耗時隨QPS增長明顯比不使用的情況慢,相對來說對QPS的增長敏感度更低。
引入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耗時圖可以得出以下結論:
- 使用零拷貝後,Top999耗時降幅將近50%,效果非常明顯。
- 在高QPS下,使用零拷貝機制的Top999耗時隨QPS增長明顯比不使用的情況慢,相對來說對QPS的增長敏感度更低。
引入零拷貝機制後,通過拷貝指標替換拷貝資料,大量降低了獲取複雜業務資料的耗時,同時將臨界區減小到最小。執行緒安全的原子變數自增與自減操作,目前在多個基礎庫中都有實現,例如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,共同支援百億級流量的高可靠系統研發與優化。