背景
內容點贊業務在得物社區中是一個非常高頻的業務場景,功能本身複雜度不高,但是業務場景多、QPS高、而且由於社區的用户體量,整體點讚的數據量非常大。其中最核心、對響應性能要求最高的主要是“用户是否點贊內容”和“內容點贊數”場景。
在得物社區中凡是有內容消費的場景,都會有上面兩個點贊場景的處理,所以整體點贊業務的QPS在社區都是非常高的。當我們在刷各種Feed流時,每一次下滑,都需要對數十篇內容進行登錄用户是否點贊狀態的判斷。作為基礎業務,內容點贊業務的高性能響應,對上游內容場景的消費體驗有極大的影響。
本文對得物社區的點贊業務如何做到高性能響應以及歷史上在緩存使用上關於高性能、穩定性、低成本上的優化探索過程進行講述,希望能給讀者帶來一些收穫。
演進探索
v1.0版本
功能需求
社區各種Feed流及內容詳情頁“登錄用户是否已點贊內容” “內容被贊總數” “內容最新點贊用户列表”幾個場景消費展示。
實現方案
點贊業務整體的高性能是基於Redis+MySQL架構。MySQL做數據存儲和查詢支持,Redis撐起業務的高性能響應。在1.0版本中,服務架構還是單體PHP服務,技術方案上將動態下所有的點贊用户查詢出來放到PHP數組裏,然後序列化為Json字符串以Key/Value的方式存儲到Redis中,當用户瀏覽內容時,取出緩存數據,反序列化Json為PHP數組,然後通過in_array和count方法判斷是否已點贊及內容點贊數。在緩存的維護上,則是每一次有新用户點贊或取消贊則直接清除Redis。
緩存結構如下:
cId => '[uid1,uid2,uid3...]'
流程圖如下:
主要問題
這個版本的方案存在比較多待優化點。
第一、緩存構造時要查詢動態下所有點贊用户數據,數據量大,容易產生慢SQL,對DB和帶寬都可能有比較大的壓力。
第二、緩存存儲數據結構上為Key/Value結構,每次使用時需先從Redis查詢,再反序列化成PHP數組,in_array()和count()方法都有比較大的開銷,尤其是查詢熱門動態時,對服務器的CPU和MEM資源都有一定浪費,對Redis也產生了比較大的網絡帶寬開銷。
第三、緩存維護上,每次新增點贊都直接清除緩存,熱門動態大量點贊操作下會出現緩存擊穿,會造成大量DB回查操作。
v2.0版本
大家都知道一些熱點事件很容易在社區中發酵,得物社區自然也存在這種情況。在某一場熱點事件中,得物社區瞬間出現多篇熱點內容,大量用户進入得物社區瀏覽相關動態並點贊,從v1.0版本的點贊維護流程上可以看出執行缺陷,即每次有新點贊都會清除緩存!當有大量用户瀏覽熱點動態,同時又有大量用户在點贊而導致緩存清除的場景下,緩存被擊穿的風險非常高,這樣會導致大量查詢請求打到DB層,研發側在評估風險之後,連夜進行了緩存改造。
功能需求
1、解決熱點內容緩存擊穿的風險。
2、優化代碼層面對緩存數據序列化和反序列化導致的服務器資源消耗。
實現方案
這次改造,除了優化解決緩存擊穿的風險外,也針對之前緩存本身的一些不足之處,思考了一些更高效的實現。在緩存數據結構上摒棄了之前的Key/Value結構,採用了集合結構。集合的特性保證集合中的用户ID不會出現重複,可以準確維護了動態下的點贊總數,通過查看用户是否在集合中,可以高效判斷用户是否點贊內容。這樣解決每次查詢時需要從Redis中獲取全部數據和每次需要代碼解析Json的過程,Redis集合支持直接通過SISMEMBER和SCARD接口判斷是否贊和計算點贊數,從而提升了整個模塊的響應速度和服務負載。在緩存維護上,每次有新增點贊時,主動向集合中添加用户ID,並更新緩存過期時間。每次查詢時,也同樣會查詢緩存的剩餘過期時間,如果低於三分之一,就會重新更新過期時間,這樣避免了熱門動態有大量新增點贊動作時,出現緩存擊穿的情況。
緩存結構如下:
cid => [uid1,uid2,uid3...]
流程圖如下:
主要問題
在技術方案中,會將動態下全部的點贊記錄全部查出,放入一個集合中,當動態是一個熱門動態時,點贊用户量會非常大,此時集合變成了一個大Key,而大Key的清理對Redis的穩定性有比較大的影響,隨時可能會因為緩存過期,而引起Redis的抖動,進而引起服務的抖動。並且每次查詢出全部的點贊用户,容易產生慢SQL,對網絡帶寬也比較有壓力。
v3.0版本
功能需求
1、解決V2.0版本中緩存大Key風險。
2、優化緩存重建時查詢內容全部點贊用户產生的慢SQL場景。
實現方案
在3.0版本中,對大Key進行了打散處理,對同一個動態下的點贊用户,進行打散分片再維護到緩存,每次操作緩存時先根據用户ID計算分片值,這樣每個分片都具有更小的體積和更快的維護和響應速度。而點贊總數的獲取,此時社區服務已經遷移到Go服務架構,我們也搭建了單獨的計數服務,單獨維護內容的被贊總數,節省了scard接口的消耗。
緩存結構如下:
cid_slice1 => [uid1,uid11,uid111...]
cid_slice2 => [uid2,uid22,uid222...]
cid_slice3 => [uid3,uid33,uid333...]
...
流程圖如下:
主要問題
如果僅僅從技術實現上看v3.0版本,似乎已經暫時達到了一個水平,在一定時間內也能正常支撐社區點贊業務的高性能響應。但是如果從業務角度和全局觀念上去考慮,這個設計方案仍舊存在比較多的優化點。例如:
緩存分片中仍舊維護了被瀏覽動態下全部的點贊用户數據,消耗了非常大的Redis資源,也增加了緩存維護難度。緩存數據的有效使用率很低,推薦流場景下,用户瀏覽過的動態,幾乎不會再次瀏覽到。
當前技術方案針對單篇內容進行設計,在各種Feed流場景中,查詢任務在點贊服務裏其實放大了十數倍。這種放大對服務器、Redis以及DB,都產生一定的資源消耗浪費。
一些點贊量特別多的歷史動態,有人訪問時均會重建緩存,重建成本高,但使用率不高。
緩存集合分片的設計維護了較多無用數數據,也產生了大量的Key,Key在Redis中同樣是佔用內存空間的。
... ...
總結一下,較高的服務器負載、Redis請求量、DB請求量。非常大的Redis資源使用(幾十GB)。
所以我們需要一個更優的方案,解決優化以下現象:
1、Feed流場景下批量查詢內容任務放大導致的服務器負載,Redis請求,DB請求放大現象。
2、緩存更高效的存儲和使用,降低緩存整體的使用量。
3、緩存更高的命中率。
4、區分冷熱數據。
實際Feed場景下的實現邏輯:
批量查詢動態點贊數據
V4.0版本
功能需求
結合實際業務場景,大部分場景上游服務都是批量判斷是否點贊,社區的動態本身也存在一定的新鮮度(冷熱)。對新緩存的要求是:
1、能解決Feed流場景下批量查詢流量放大現象。
2、緩存數據區分冷熱,減少無效存儲(能在內容和點贊用户角度都區分冷熱數據)。
3、緩存結構要簡單易維護,使業務實現要清晰明瞭。
實現方案
設計思路:
1、批量查詢任務之所以放大是因為之前的緩存是以內容為維度進行設計,新方案要以用户為維度進行設計。
2、舊方案中訪問內容點贊數據會重建緩存,有些老舊內容重建緩存性價比低,而且內容下的點贊用户並不是一直活躍和會重新訪問內容,新方案要等區分冷熱數據,冷數據直接訪問DB,不再進行緩存的重建/更新維護。
3、舊方案在維護緩存過期時間和延長過期時間的設計中,每次操作緩存都會進行ttl接口操作,QPS直接x2。新方案要避免ttl操作,但同時又可以維護緩存過期時間。
4、緩存操作和維護要簡單,期望一個Redis接口操作能達到目的。
所以新方案Redis數據結構的選擇中,能判斷是否點贊、是否是冷熱數據、是否需要延長過期時間,之前的集合是不能滿足了,我們選擇Hash表結構。用户ID做Key,contentId做field,考慮到社區內容ID是趨勢遞增的,一定程度上coententID能代表數據的冷熱,在緩存中只維護一定時間和一定數量的contentID,並且增加minCotentnID用於區分冷熱數據,為了減少ttl接口的調用,還增加ttl字段用户判斷緩存有效期和延長緩存過期時間。一舉三得!
緩存結構如下:
{
"userId":{
"ttl":1653532653, //緩存新建或更新時時間戳
"cid1":1, //用户近一段時間點贊過的動態id
"cid2":1, //用户近一段時間點贊過的動態id
"cidn":1, //用户近一段時間點贊過的動態id
"minCid":3540575, //緩存中最小的動態id,用以區分冷熱,
}
}
在實際業務場景流程如下:
通過流程圖,我們可以清晰看到, 上游Feed流,一次批量查詢請求,沒有了循環邏輯,最優情況下,只有一次Redis操作,業務實現也非常簡單明瞭。
優化結果
優化前後Redis查詢量QPS日常峯值下降了20倍。
優化前後接口平均RT下降了10倍。
優化前後DB查詢量QPS日常峯值下降了6倍。
優化前後緩存節省了16G左右存儲空間。
後續
優化不會結束,技術不會停止,技術方案會隨着業務的演進而演進。
總結
本篇文章中對得物社區點贊業務緩存優化的探索演進做了相關歷史背景和技術方案的解析,當前其中還有更多的細節。而這麼多次版本的優化,都是根據實際的業務場景中出現的風險點以及需求不斷摸索出來的,每個版本的方案也都不是完美方案,v4.0也不是最終方案,還需要開發人員也需要進一步思索,探索更優的技術方案。
並且隨着業務的不斷髮展迭代,會涌現出更多的場景和困難,我們一直在優化探索的路上。
*文/慎之
@得物技術公眾號