高性能緩存架構

雖然我們可以通過各種手段來提升存儲系統的性能,但在某些複雜的業務場景下,單純依靠存儲系統的性能提升不夠的,典型的場景有:

• 需要經過複雜運算後得出的數據,存儲系統無能為力

例如,一個論壇需要在首頁展示當前有多少用戶同時在線,如果使用 MySQL 來存儲當前用戶狀態,則每次獲取這個總數都要“count(*)”大量數據,這樣的操

作無論怎麼優化 MySQL,性能都不會太高。如果要實時展示用戶同時在線數,則 MySQL 性能無法支撐。

• 讀多寫少的數據,存儲系統有心無力

能幾千萬人來瀏覽。如果使用 MySQL 來存儲微博,用戶寫微博只有一條 insert 語句,但每個用戶瀏覽時都要 select 一次,即使有索引,幾千萬條 select 語句對 MySQL 數據庫的壓力也會非常大。

緩存就是為了彌補存儲系統在這些複雜業務場景下的不足,其基本原理是將可能重複使用的數據放到內存中,一次生成、多次使用,避免每次使用都去訪問存儲系統。

緩存能夠帶來性能的大幅提升,以 Memcache 為例,單臺 Memcache 服務器簡單的 key-value 查詢能夠達到 TPS 50000 以上,其基本的架構是:

高性能緩存架構

(http://pic001.cnblogs.com/img/dudu/200809/2008092816494460.png)

緩存雖然能夠大大減輕存儲系統的壓力,但同時也給架構引入了更多複雜性。架構設計時如果沒有針對緩存的複雜性進行處理,某些場景下甚至會導致整個系統崩潰。今天,我來逐一分析緩存的架構設計要點。

緩存穿透

緩存穿透是指緩存沒有發揮作用,業務系統雖然去緩存查詢數據,但緩存中沒有數據,業務系統需要再次去存儲系統查詢數據。通常情況下有兩種情況:

1. 存儲數據不存在

第一種情況是被訪問的數據確實不存在。一般情況下,如果存儲系統中沒有某個數據,則不會在緩存中存儲相應的數據,這樣就導致用戶查詢的時候,在緩存中找不到對應的數據,每次都要去存儲系統中再查詢一遍,然後返回數據不存在。緩存在這個場景中並沒有起到分擔存儲系統訪問壓力的作用。

通常情況下,業務上讀取不存在的數據的請求量並不會太大,但如果出現一些異常情況,例如被黑客攻擊,故意大量訪問某些讀取不存在數據的業務,有可能會將存儲系統拖垮。這種情況的解決辦法比較簡單,如果查詢存儲系統的數據沒有找到,則直接設置一個默認值(可以是空值,也可以是具體的值)存到緩存中,這樣第二次讀取緩存時就會獲取到默認值,而不會繼續訪問存儲系統。

2. 緩存數據生成耗費大量時間或者資源

第二種情況是存儲系統中存在數據,但生成緩存數據需要耗費較長時間或者耗費大量資源。如果剛好在業務訪問的時候緩存失效了,那麼也會出現緩存沒有發揮作用,訪問壓力全部集中在存儲系統上的情況。

典型的就是電商的商品分頁,假設我們在某個電商平臺上選擇“手機”這個類別查看,由於數據巨大,不能把所有數據都緩存起來,只能按照分頁來進行緩存,由於難以預測用戶到底會訪問哪些分頁,因此業務上最簡單的就是每次點擊分頁的時候按分頁計算和生成緩存。通常情況下這樣實現是基本滿足要求的,但是如果被競爭對手用爬蟲來遍歷的時候,系統性能就可能出現問題。

具體的場景有:

• 分頁緩存的有效期設置為 1 天,因為設置太長時間的話,緩存不能反應真實的數據。

• 通常情況下,用戶不會從第 1 頁到最後 1 頁全部看完,一般用戶訪問集中在前 10 頁,因此第 10 頁以後的緩存過期失效的可能性很大。

• 競爭對手每週來爬取數據,爬蟲會將所有分類的所有數據全部遍歷,從第 1 頁到最後 1 頁全部都會讀取,此時很多分頁緩存可能都失效了。

• 由於很多分頁都沒有緩存數據,從數據庫中生成緩存數據又非常耗費性能(order by limit 操作),因此爬蟲會將整個數據庫全部拖慢。

這種情況並沒有太好的解決方案,因為爬蟲會遍歷所有的數據,而且什麼時候來爬取也是不確定的,可能是每天都來,也可能是每週,也可能是一個月來一次,我們也不可能為了應對爬蟲而將所有數據永久緩存。通常的應對方案要麼就是識別爬蟲然後禁止訪問,但這可能會影響 SEO 和推廣;要麼就是做好監控,發現問題後及時處理,因為爬蟲不是攻擊,不會進行暴力破壞,對系統的影響是逐步的,監控發現問題後有時間進行處理。

緩存雪崩

緩存雪崩是指當緩存失效(過期)後引起系統性能急劇下降的情況。當緩存過期被清除後,業務系統需要重新生成緩存,因此需要再次訪問存儲系統,再次進行運算,這個處理步驟耗時幾十毫秒甚至上百毫秒。而對於一個高併發的業務系統來說,幾百毫秒內可能會接到幾百上千個請求。由於舊的緩存已經被清除,新的緩存還未生成,並且處理這些請求的線程都不知道另外有一個線程正在生成緩存,因此所有的請求都會去重新生成緩存,都會去訪問存儲系統,從而對存儲系統造成巨大的性能壓力。這些壓力又會拖慢整個系統,嚴重的會造成數據庫宕機,從而形成一系列連鎖反應,造成整個系統崩潰。緩存雪崩的常見解決方法有兩種:更新鎖機制和後臺更新機制。

1. 更新鎖

對緩存更新操作進行加鎖保護,保證只有一個線程能夠進行緩存更新,未能獲

取更新鎖的線程要麼等待鎖釋放後重新讀取緩存,要麼就返回空值或者默認值。

對於採用分佈式集群的業務系統,由於存在幾十上百臺服務器,即使單臺服務器只有一個線程更新緩存,但幾十上百臺服務器一起算下來也會有幾十上百個線程同時來更新緩存,同樣存在雪崩的問題。因此分佈式集群的業務系統要實現更新鎖機制,需要用到分佈式鎖,如 ZooKeeper。

2. 後臺更新

由後臺線程來更新緩存,而不是由業務線程來更新緩存,緩存本身的有效期設置為永久,後臺線程定時更新緩存。

後臺定時機制需要考慮一種特殊的場景,當緩存系統內存不夠時,會“踢掉”一些緩存數據,從緩存被“踢掉”到下一次定時更新緩存的這段時間內,業務線程讀取緩存返回空值,而業務線程本身又不會去更新緩存,因此業務上看到的現象就是數據丟了。解決的方式有兩種:

• 後臺線程除了定時更新緩存,還要頻繁地去讀取緩存(例如,1 秒或者

100 毫秒讀取一次),如果發現緩存被“踢了”就立刻更新緩存,這種方式實現簡單,但讀取時間間隔不能設置太長,因為如果緩存被踢了,緩存讀取間隔時間又太長,這段時間內業務訪問都拿不到真正的數據而是一個空的緩存值,用戶體驗一般。

• 業務線程發現緩存失效後,通過消息隊列發送一條消息通知後臺線程更新緩存。可能會出現多個業務線程都發送了緩存更新消息,但其實對後臺線程沒有影響,後臺線程收到消息後更新緩存前可以判斷緩存是否存在,存在就不執行更新操作。這種方式實現依賴消息隊列,複雜度會高一些,但緩存更新更及時,用戶體驗更好。

後臺更新既適應單機多線程的場景,也適合分佈式集群的場景,相比更新鎖機制要簡單一些。

後臺更新機制還適合業務剛上線的時候進行緩存預熱。緩存預熱指系統上線

後,將相關的緩存數據直接加載到緩存系統,而不是等待用戶訪問才來觸發緩存加載。

緩存熱點

雖然緩存系統本身的性能比較高,但對於一些特別熱點的數據,如果大部分甚至所有的業務請求都命中同一份緩存數據,則這份數據所在的緩存服務器的壓力也很大。例如,某明星微博發佈“我們”來宣告戀愛了,短時間內上千萬的用戶都會來圍觀。

緩存熱點的解決方案就是複製多份緩存副本,將請求分散到多個緩存服務器

上,減輕緩存熱點導致的單臺緩存服務器壓力。以微博為例,對於粉絲數超過 100 萬的明星,每條微博都可以生成 100 份緩存,緩存的數據是一樣的,通過在緩存的 key 裡面加上編號進行區分,每次讀緩存時都隨機讀取其中某份緩存。

緩存副本設計有一個細節需要注意,就是不同的緩存副本不要設置統一的過期時間,否則就會出現所有緩存副本同時生成同時失效的情況,從而引發緩存雪崩效應。正確的做法是設定一個過期時間範圍,不同的緩存副本的過期時間是指定範圍內的隨機值。

實現方式

由於緩存的各種訪問策略和存儲的訪問策略是相關的,因此上面的各種緩存設計方案通常情況下都是集成在存儲訪問方案中,可以採用“程序代碼實現”的中間層方式,也可以採用獨立的中間件來實現。

小結

今天我為你講了高性能架構設計中緩存設計需要注意的幾個關鍵點,這些關鍵點本身在技術上都不復雜,但可能對業務產生很大的影響,輕則系統響應變慢,重則全站宕機,架構師在設計架構的時候要特別注意這些細節,希望這些設計關鍵點和技術方案對你有所幫助。

這就是今天的全部內容,留一道思考題給你吧,分享一下你所在的業務發生過哪些因為緩存導致的線上問題?採取了什麼樣的解決方案?效果如何?

歡迎你把答案寫到留言區,和我一起討論。相信經過深度思考的回答,也會讓


分享到:


相關文章: