在高併發環境下Reids做快取踩坑記錄

晦若晨曦發表於2017-12-14

近期隨著公司部分專案遷移,伺服器壓力集中於某些業務上,導致原本穩定的專案出現了頻繁的卡死現象。

通過逐步除錯發現,此時專案的效能瓶頸出現在做快取的Redis上。以最簡單的get指令,一次呼叫耗時可以達到數百毫秒,顯然是不可思議的。

對Redis本身做效能測試,發現Redis效能很正常,遠遠沒有達到極限。

服務卡死時伺服器本身資源也很正常,沒有出現效能瓶頸問題。

基本判定問題出現在伺服器程式碼本身。

伺服器程式結構: SpringMVC+Spring+Mybatis+Jedis

經過數週的排查,最終發現了兩點使用不正確的地方。

Redis呼叫次數過多

在除錯過程中,首先發現的就是此問題。

初步除錯中利用Spring的AOP功能,對Jedis的每次呼叫都做了記錄,發現原有的業務邏輯中,每次介面請求,需要呼叫Redis至少五十次,簡直喪心病狂。

不需要資料支援,僅憑常識就能夠想到如此大量的Redis呼叫會很大程度上影響介面的效能。

根據Redis監控分析,當服務卡死,Redis的連線數遠遠沒有達到極限,僅為6000+,說明此時其實是Jedis連線池不足以支援如此頻繁的請求。

於是第一步就是精簡程式碼,減少Redis請求。

鑑於前人都不是傻子,其實精簡程式碼的成效不是很好。使用hmget替換了迴圈的hget之後,效能略有提升但是仍舊無法滿足需求。最後的解決方案是使用MongoDb再增加一層快取資料庫。

部分修改頻繁的統計、計數性質資料與內容資料一起作為一個文件儲存於MongoDb,再間隔幾分鐘定時從MongoDb再同步到Redis快取中。省去了80%的Redis請求數量。

最後從Redis使用get獲取內容id列表,再通過hmget一次批量獲取資料,兩次請求完成原本數十上百次請求的工作,效能得到極大提升。

在測試環境中測試,Jmeter,300執行緒。3秒超時

原有程式碼僅有不足200的QPS,延遲極高,有很高的失敗率。

新程式碼可以達到800+QPS,而且平均響應時間在50ms左右,最高僅有500ms

Redis快取資料過大

然而程式碼上線不久就再次卡死。只能繼續尋找原因。

Redis的效能強大是經過無數專案實踐證明的。但是Redis也有它適用場景的限制。在原本的程式碼中將大量的Json資料快取在Redis中,是導致Reids效能緩慢的元凶之一。

根據Redis的效能測試,在資料量超過1K時開始出現較為明顯的效能下滑,超過100K時效能下滑已經極為嚴重。

在原本的業務邏輯中,Redis每次讀取資料可達300KB,堪稱海量。此時使用Jmeter進行壓測,在200執行緒下僅僅有250QPS的吞吐量。

在除錯期間,曾經想到過這個問題,並將資料量折半,約為170KB每次,但是因為資料量仍然遠超100KB大關,效能提升並不明顯。

最後通過減少資料量,將資料減少至80KB,發現效能出現較大提升,最後進一步精簡資料達到40KB,得到了700+QPS的吞吐量,提升十分顯著。

減少Redis請求量的另一種思路

在這個專案上線並且平穩執行之後,很容易聯想到公司另一個專案也出現了極為相似的效能問題。

這個專案並不存在資料量過大的問題。每次返回的資料不足10KB,雖然Redis請求量沒有前一個喪心病狂,但是仍然較多而且連讀帶寫。更嚴重的問題則是,由於業務邏輯問題,不能便捷的切換為MongoDb進行資料整合。

第一步將一部分請求用Hmget的方式做了整合,效能提升很不明顯,大概只有5%左右。

Redis請求量大導致速度慢,並不是Redis本身無法處理大量請求,而是大量請求佔用連線池資源,並且有固定的網路請求消耗。Redis中有一個神器pipeline可以解決此問題。

根據初步的測試,通過pipeline的方式對統計資料的Redis請求做了整合之後,在200執行緒下的吞吐量由300QPS提升到了500QPS,提升十分顯著。

補充 :

經過進一步的除錯,通過hmget和pipeline的結合,成功將吞吐量提升至1000+QPS,進一步證明了合理使用Redis,程式的效能都是有很大潛力可以挖掘的。

總結

Redis是效能強大的快取服務。但是也不能無條件的相信Redis的效能。最後錯誤的使用方法將會成為伺服器效能的陷阱,而且會付出大量的精力去修正原有的程式。

快取是高效能服務的保障,但是快取和資料庫資源一樣是需要精細的算計的。節約每一點資源,誰知道哪裡就會成為壓垮伺服器的最後一根稻草呢。

相關文章