作者:京東零售 曹志飛
引言
在現(xiàn)代軟件架構(gòu)中,緩存是提高系統(tǒng)性能和響應(yīng)速度的重要手段。然而,如果不正確地使用緩存,可能會導(dǎo)致嚴重的線上事故,尤其是緩存的大熱key問題更是老生常談。本文將探討一個常見但容易被忽視的問題:緩存大熱key和緩存擊穿問題。我們將從一個真實案例入手,分析其原因,并提供解決方案和預(yù)防措施。
案例描述
某系統(tǒng)在雙十一大促期間,遇到了一個嚴重的線上事故。業(yè)務(wù)人員在創(chuàng)建一個大型活動,該大型活動由于活動條件和活動獎勵比較多,導(dǎo)致生成的緩存內(nèi)容非常大。活動上線后,系統(tǒng)就開始出現(xiàn)各種異常告警,核心UMP監(jiān)控可用率由100%持續(xù)下降到20%,系統(tǒng)訪問Redis的調(diào)用次數(shù)和查詢性能也斷崖式下降,后續(xù)更是產(chǎn)生連鎖反應(yīng)影響了其他多個核心接口的可用率,導(dǎo)致整個系統(tǒng)服務(wù)不可用。
原因分析
在這個系統(tǒng)中,為了提高查詢活動的性能,我們開發(fā)團隊決定使用Redis作為緩存系統(tǒng)。將每個活動信息作為一個key-value存儲在Redis中。由于業(yè)務(wù)需要,有時候業(yè)務(wù)運營人員也會創(chuàng)建一個非常龐大的活動,來支撐雙十一期間的各種玩法。針對這種龐大的活動,我們開發(fā)團隊也提前預(yù)料到了可能會出現(xiàn)的大key和熱key問題,所以在查詢活動緩存之前增加了一層本地jvm緩存,本地jvm緩存5分鐘,緩存失效后再去回源查詢Redis中的活動緩存,本以為會萬無一失,沒想到最后還是出了問題。
查詢方法偽代碼
ActivityCache present = activityLocalCache.getIfPresent(activityDetailCacheKey);
if (present != null) {
ActivityCache activityCache = incentiveActivityPOConvert.copyActivityCache(present);
return activityCache
}
ActivityCache remoteCache = getCacheFromRedis(activityDetailCacheKey);
activityLocalCache.put(activityDetailCacheKey, remoteCache);
return remoteCache;
查詢活動緩存流程如上圖所示,為什么加了本地緩存還是出了問題?
這里其實就存在著第一個緩存陷阱:緩存擊穿問題。首先解釋一下什么是緩存擊穿;緩存擊穿(Cache Miss)是指在高并發(fā)的系統(tǒng)中,如果某個緩存鍵對應(yīng)的值在緩存中不存在(即緩存失效),那么所有請求都會直接訪問后端數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫的負載瞬間增加,可能會引發(fā)數(shù)據(jù)庫宕機或服務(wù)不可用的情況。所以在本次事故里邊,運營人員審批活動上線的一瞬間,活動緩存只是寫入到了Redis緩存中,但是本地緩存還都是空的,所以此時就會有大量請求來同時訪問Redis。
按照以往經(jīng)驗,Redis緩存都是純內(nèi)存操作,查詢性能可以滿足大量請求同時查詢活動緩存,就在此時我們卻陷入了第二個緩存陷阱:網(wǎng)絡(luò)帶寬瓶頸;Redis的高并發(fā)性能毋庸置疑,但是我們卻忽略了一個大key和熱key對網(wǎng)絡(luò)帶寬的影響,本次引發(fā)問題的大熱key大小達到了1.5M,經(jīng)過事后了解京東Redis對單分片的網(wǎng)絡(luò)帶寬也有限流,默認200M,根據(jù)換算,該熱key最多只能支持133次的并發(fā)訪問。所以就在活動上線的同一時刻,加上緩存擊穿的影響,迅速達到了Redis單分片的帶寬限流閾值,導(dǎo)致Redis線程進入阻塞狀態(tài),以至于所有的業(yè)務(wù)服務(wù)器都無法查詢Redis緩存成功,最終引發(fā)了緩存雪崩效應(yīng)。
解決方案
為了解決這個問題,我們開發(fā)團隊采取了以下措施:
- 大key治理:更換緩存對象序列化方法,由原來的JSON序列化調(diào)整為Protostuff序列化方式。治理效果:緩存對象大小由1.5M減少到了0.5M。
- 使用壓縮算法:在存儲緩存對象時,再使用壓縮算法(如gzip)對數(shù)據(jù)進行壓縮,注意設(shè)置壓縮閾值,超過一定閾值后再進行壓縮,以減少占用的內(nèi)存空間和網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)量。壓縮效果:500k壓縮到了17k。
- 緩存回源優(yōu)化:本地緩存miss后回源查詢Redis增加線程鎖,減少回源Redis并發(fā)數(shù)量。
- 監(jiān)控和優(yōu)化Redis配置:定期監(jiān)控Redis網(wǎng)絡(luò)傳輸情況,根據(jù)實際情況調(diào)整Redis的限流配置,以確保Redis的穩(wěn)定運行。
治理后業(yè)務(wù)偽代碼如下:
ActivityCache present = activityLocalCache.get(activityDetailCacheKey, key -> getCacheFromRedis(key));
if (present != null) {
return present;
}
/**
* 查詢二進制緩存
*
* @param activityDetailCacheBinKey
* @return
*/
private ActivityCache getBinCacheFromJimdb(String activityDetailCacheBinKey) {
List activityByteList = slaveCluster.hMget(activityDetailCacheBinKey.getBytes(),"stock".getBytes());
if (activityByteList.get(0) != null && activityByteList.get(0).length > 0) {
byte[] decompress = ByteCompressionUtil.decompress(activityByteList.get(0));
ActivityCache activityCache = ProtostuffUtil.deserialize(decompress, ActivityCache.class);
if (activityCache != null) {
if (activityByteList.get(1) != null && activityByteList.get(1).length > 0) {
activityCache.setAvailableStock(Integer.valueOf(new String(activityByteList.get(1))));
}
return activityCache;
}
}
return null;
[]>
預(yù)防措施
為了避免類似的問題再次發(fā)生,開發(fā)團隊采取了以下預(yù)防措施:
- 設(shè)計階段考慮緩存策略:在系統(tǒng)設(shè)計階段,充分考慮緩存的使用場景和數(shù)據(jù)特性,避免盲目使用大key緩存。
- 進行壓力測試和性能評估:在上線前,進行充分的壓力測試和性能評估,模擬高并發(fā)和大數(shù)據(jù)量的情況,及時發(fā)現(xiàn)和解決潛在問題。
- 定期進行系統(tǒng)優(yōu)化和升級:隨著業(yè)務(wù)的發(fā)展和技術(shù)的進步,定期對系統(tǒng)進行優(yōu)化和升級,引入新的技術(shù)和工具來提高系統(tǒng)的性能和穩(wěn)定性。
結(jié)論
緩存大key和熱key是緩存使用中常見的陷阱,千萬不要心存僥幸,否則會引發(fā)嚴重的線上事故。通過本文的案例分析和解決方案,我們希望能夠幫助讀者更好地理解和應(yīng)對這個問題。記住,合理使用緩存是提高系統(tǒng)性能的關(guān)鍵,而不是簡單地將所有數(shù)據(jù)都存儲在緩存中。
-
軟件
+關(guān)注
關(guān)注
69文章
5009瀏覽量
88061 -
緩存
+關(guān)注
關(guān)注
1文章
241瀏覽量
26754 -
key
+關(guān)注
關(guān)注
0文章
51瀏覽量
12860
發(fā)布評論請先 登錄
相關(guān)推薦
評論