天天看點

架構與思維:一次緩存雪崩的災難複盤

雲辦公系統使用者實時資訊查詢功能優化釋出之後,系統發生當機事件(系統挂起,頁面無法加載)。

我們IM原有的一個功能,當滑鼠移動到使用者頭像的時候,會顯示出使用者的基本資訊。資訊比較簡單,隻包含簡單的使用者名、昵稱、性别、郵箱、電話等基本資料,

這是一個典型的資料查詢,大概過程如下左側,通路使用者基本資訊的時候會先去Redis中查一下,如果不存在,就把大約2W左右的使用者資料一次性取出來,儲存在Redis中,因為使用者基本資訊在同一張表上,使用者資訊表的資料量也很少,是以一直也沒什麼問題。

過程如下圖左側所示。

架構與思維:一次緩存雪崩的災難複盤

後續對功能做了優化,原有采集的資訊除了使用者的基本資訊之外,還采集了教育經曆、工作經曆、所獲勳章等。

這些資訊存儲在不同的表裡面,是以采集過程是一個複雜的聯表查詢,特别是有些基礎表資料量比較大,執行效率也是比較慢的。

如果把所有使用者全部取出來并存儲在一個Redis節點中,明顯已經不适用,一個是批量查詢導緻資料庫執行效率慢,一個是Redis單節點資料太大。

是以開發同學做了下優化,每次隻取單個使用者的綜合資訊存在Redis中,一個使用者建一個緩存,如上圖右側所示。 

這種做法看着沒啥問題,當晚釋出後,在第二天的上午10點~11點就發生了系統瓶頸卡頓,最後挂起的情況,資料庫的記憶體、CPU全部飙上去了。

第一時間的處理方法是降級,程式復原到之前隻提供基本資訊的階段,其他的前端預設顯示空資訊。接着就是對問題進行分析了,後确認原因是産生了 緩存雪崩了。

新釋出的系統,緩存池是空的,在早上10點高峰期的時候,大量的人員到IM上進行通路,系統開始初次建立每個人的緩存資訊,大量的請求查詢不到緩存,直接透過緩存池投向資料庫,造成瞬時DB請求量井噴。這是典型的緩存雪崩了。 

同時因為,失效時間相近(8小時失效),是以也有潛在的緩存雪崩。

應急處理方案:适當處理緩存的機制,采用布隆過濾器、空初始值、随機緩存失效時間方式來預防緩存擊穿和緩存雪崩的産生。

最終解決方案:改回原來緩存全公司員工資訊的方式,根據執行計劃和SlowLog,優化擷取員工資訊的SQL腳本,去掉不需要的字段和無意義的連接配接。   

緩存雪崩是指大量的key設定了相同的過期時間,導緻在緩存在同一時刻全部失效,造成瞬時DB請求量大、壓力驟增,引起雪崩。

上面的哪個問題,初次通路的資料都是未建立緩存的,跟同時失效的情況一樣,當峰值期到來的時候,會大量的請求查詢不到緩存,直接透過緩存池投向資料庫,造成瞬時DB請求量井噴。

在系統容量設計的時候,應該能夠預見後期會有大量的請求,是以在發生雪崩前對緩存叢集實作高可用,如果是使用 Redis,可以使用 主從+哨兵 ,Redis Cluster 來避免 Redis 全盤崩潰的情況。

同樣的,也需要對資料庫進行高可用保障,因為透過緩存之後,真正考驗的是資料庫的抗壓能力。是以 1主N從 甚至 資料庫叢集 是我們需要重點去考慮的。

可以使用 Hystrix進行限流 + 降級 ,比如像上面那種情況,一下子來了1W個請求,不是目前系統的吞吐能力能夠承受的,假設單秒TPS的能力隻能是 5000個,那麼剩餘的 5000 請求就可以走限流邏輯。

可以設定一些預設值,然後調用我們自己降級邏輯去FallBack,保護最後的 MySQL 不會被大量的請求挂起。 除了Hystrix之外,阿裡的Sentinel 和 Google的RateLimiter 都是不錯的選擇。

Sentinel 漏桶算法

架構與思維:一次緩存雪崩的災難複盤

RateLimiter 令牌桶算法

架構與思維:一次緩存雪崩的災難複盤

另外可以考慮使用用本地緩存來進行緩沖,在 Redis Cluster 不可用的時候,不至于全線崩潰。

可以給緩存設定過期時間時加上一個随機值時間,使得每個key的過期時間分布開來,不會集中在同一時刻失效。

随機值我們團隊的做法是:n * 3/4 + n * random() 。是以,比如你原本計劃對一個緩存建立的過期時間為8小時,那就是6小時 + 0~2小時的随機值。

這樣保證了均勻分布在 6~8小時之間。如圖: 

架構與思維:一次緩存雪崩的災難複盤

類似上面的那個案例,并不是還沒過期,而是新功能釋出,壓根還沒建設過緩存,是以可以在峰值期之前先做好部分緩存,避免瞬時壓力太大。

是以如果10點是峰值期,那麼可以預先在8~10點期間,可以逐漸的把大部分緩存建立起來。如圖:

架構與思維:一次緩存雪崩的災難複盤

緩存穿透是指通路一個不存在的key,緩存不起作用,請求會穿透到DB,流量井噴時會導緻DB挂掉。

比如 我們查詢使用者的資訊,程式會根據使用者的編号去緩存中檢索,如果找不到,再到資料庫中搜尋。如果你給了一個不存在的編号:XXXXXXXX,那麼每次都比對不到,就透過緩存進入資料庫。

這樣風險很大,如果因為某些原因導緻大量不存在的編号被查詢,甚至被惡意僞造編号進行攻擊,那将是災難。

發生穿透的原因是緩存中沒有存儲這些空資料的key,或者壓根這個資料的key是不會存在的,進而導緻每次查詢都進入資料庫中。

我們就可以将這些key的值設定為null,并寫到緩存池中。後面再出現查詢這個key 的請求的時候,直接傳回null,這樣就在緩存池中就被判斷傳回了,壓力在緩存層中,不會轉移到資料庫上。

我們稱作布隆過濾器,BloomFilter 類似于一個hbase set 用來判斷某個元素(key)是否存在于某個集合中。

這種方式在大資料場景應用比較多,比如 Hbase 中使用它去判斷資料是否在磁盤上。還有在爬蟲場景判斷url 是否已經被爬取過。

這種方案可以加在第一種方案中,在緩存之前在加一層 BloomFilter ,把存在的key記錄在BloomFilter中,在查詢的時候先去 BloomFilter 去查詢 key 是否存在,如果不存在就直接傳回,存在再走查緩存 ,投入資料庫去查詢,這樣減輕了資料庫的壓力。

流程圖如下:

架構與思維:一次緩存雪崩的災難複盤

前面說過,可能會存在一些惡意攻擊,僞造出大量不存在的key ,這種情況下如果我們如果采用緩存空值的辦法,就會産生大量不存在key的null資料。顯然是不合适的,這時我們完全可以使用第二種方案進行過濾掉這些key。

是以,判斷的依據是:

針對key非常多、請求重複率比較低的資料,我們就沒有必要進行緩存,使用 BloomFilter 直接過濾掉。

而對于空資料的key有限的,重複率比較高的,我們則可以采用 緩存空值的辦法 進行處理。 

一個存在的key,在緩存過期的一刻,同時有大量的請求,這些請求都會擊穿到DB,造成瞬時DB請求量大、壓力驟增。(注意跟上面兩種的差別)

分布式鎖場景,在通路key之前,采用SETNX(set if not exists)來設定另一個短期key來鎖住目前key的通路,通路結束再删除該短期key。

這種現象是多個線程同時去查詢資料庫的這條資料,那麼我們可以在第一個查詢資料的請求上使用一個 互斥鎖來鎖住它。

其他的線程走到這一步拿不到鎖就等着,等第一個線程查詢到了資料,然後做緩存。後面的線程進來發現已經有緩存了,就直接走緩存。

鎖不好的地方就是在其他線程在拿不到鎖的時候就等待,這個會造成系統整體吞吐量降低,使用者體驗度也不好。

這是一種短暫降級的方式:

如果一個緩存失效的時候,有無數個請求狂奔而來,而第一個請求從進入緩存池,判空,再到資料庫檢索,再查詢出結果并傳回設定緩存的這個過程裡,緩存是不存在的。

這個就很危險,超高并發下這個短暫的過程足已讓千千萬萬請求投向資料庫。更别提這可能是個慢查詢,整個過程可能長達2s以上,那對資料庫是一種非常大的傷害。

業内有一種做法叫做空初始值,短暫的局部降級來保證整個資料庫系統不被擊穿。大概流程如下:

架構與思維:一次緩存雪崩的災難複盤

可以看出,整個過程中我們犧牲了A、B、C、D的請求,他們拿回了一個空值或者預設值,但是這局部的降級卻保證整個資料庫系統不被擁堵的請求擊穿。

這也是我面試中最喜歡問候選人的緩存類問題。

架構與思維:一次緩存雪崩的災難複盤

架構與思維·公衆号:撰稿者為bat、位元組的幾位高階研發/架構。不做廣告、不賣課、不要打賞,隻分享優質技術

碼字不易,歡迎關注,歡迎轉載

作者:翁智華

出處:https://www.cnblogs.com/wzh2010/

本文采用「CC BY 4.0」知識共享協定進行許可,轉載請注明作者及出處。

繼續閱讀