'分佈式緩存的25個優秀實踐與線上案例'

腳本語言 設計 數據庫 數據結構 Redis Lua 物理 誰不曾年少輕狂 2019-09-04
"
"
分佈式緩存的25個優秀實踐與線上案例

本文主要介紹使用分佈式緩存的優秀實踐和線上案例。這些案例是筆者在多家互聯網公司裡積累並形成的優秀實踐,能夠幫助大家在生產實踐中避免很多不必要的生產事故。

一、緩存設計的核心要素

我們在應用中決定使用緩存時,通常需要進行詳細的設計,因為設計緩存架構看似簡單,實則不然,裡面蘊含了很多深奧的原理,如果使用不當,則會造成很多生產事故甚至是服務雪崩之類的嚴重問題。

筆者在做設計評審的過程中,總結了所有與緩存設計相關的設計點,這裡列出來供大家參考。

1、容量規劃

  • 緩存內容的大小
  • 緩存內容的數量
  • 淘汰策略
  • 緩存的數據結構
  • 每秒的讀峰值
  • 每秒的寫峰值

2、性能優化

  • 線程模型
  • 預熱方法
  • 緩存分片
  • 冷熱數據的比例

3、高可用

  • 複製模型
  • 失效轉移
  • 持久策略
  • 緩存重建

4、緩存監控

  • 緩存服務監控
  • 緩存容量監控
  • 緩存請求監控
  • 緩存響應時間監控

5、注意事項

  • 是否有可能發生緩存穿透
  • 是否有大對象
  • 是否使用緩存實現分佈式鎖
  • 是否使用緩存支持的腳本(Lua)
  • 是否避免了Race Condition

筆者在這裡把這些設計點提供給讀者,請讀者在做緩存設計時把每一項作為一個思考的起點,思考我們在設計緩存時是否想到了這些點,以避免在設計的過程中因忽略某一項而導致嚴重的線上事故發生。

二、緩存設計的優秀實踐

筆者在做設計評審的過程中,總結了一些開發人員在設計緩存系統時的優秀實踐,如下所述:

優秀實踐1

緩存系統主要消耗的是服務器的內存,因此,在使用緩存時必須先對應用需要緩存的數據大小進行評估,包括緩存的數據結構、緩存大小、緩存數量、緩存的失效時間,然後根據業務情況自行推算在未來一定時間內的容量的使用情況,根據容量評估的結果來申請和分配緩存資源,否則會造成資源浪費或者緩存空間不夠。

優秀實踐2

建議將使用緩存的業務進行分離,核心業務和非核心業務使用不同的緩存實例,從物理上進行隔離,如果有條件,則請對每個業務使用單獨的實例或者集群,以減小應用之間互相影響的可能性。筆者就經常聽說有的公司應用了共享緩存,造成緩存數據被覆蓋以及緩存數據錯亂的線上事故。

優秀實踐3

根據緩存實例提供的內存大小推算應用需要使用的緩存實例數量,一般在公司裡會成立一個緩存管理的運維團隊,這個團隊會將緩存資源虛擬成多個相同內存大小的緩存實例。

例如一個實例有4GB內存,在應用申請時可以按需申請足夠的實例數量來使用,對這樣的應用需要進行分片,詳情請參考《可伸縮服務架構:框架與中間件》中4.4.3的內容。這裡需要注意,如果我們使用了RDB備份機制,每個實例使用4GB內存,則我們的系統需要大於8GB內存,因為RDB備份時使用了 copy-on-write 機制,需要fork出一個子進程,並且複製一份內存,因此需要雙份的內存存儲大小。

優秀實踐4

緩存一般是用來加速數據庫的讀操作的,一般先訪問緩存後訪問數據庫,所以緩存的超時時間的設置是很重要的。筆者曾經在一家互聯網公司遇到過由於運維操作失誤導致緩存超時設置得較長,從而拖垮服務的線程池,最終導致服務雪崩的情況。

優秀實踐5

所有的緩存實例都需要添加監控,這是非常重要的,我們需要對慢查詢、大對象、內存使用情況做可靠的監控。

優秀實踐6

我們不推薦多個業務共享一個緩存實例,但是由於成本控制的原因,這種情況經常出現,我們需要通過規範來限制各個應用使用的key有唯一的前綴,並進行隔離設計,避免產生緩存互相覆蓋的問題。

優秀實踐7

任何緩存的key都必須設定緩存失效時間,且失效時間不能集中在某一點,否則會導致緩存佔滿內存或者緩存雪崩。

優秀實踐8

低頻訪問的數據不要放在緩存中,如我們前面所說的,我們使用緩存的主要目的是提高讀取性能。

曾經有個小夥伴設計了一套定時的批處理系統,由於批處理系統需要對一個大的數據模型進行計算,所以該小夥伴把這個數據模型保存在每個節點的本地緩存中,並通過消息隊列接收更新的消息來維護本地緩存中模型的實時性,但是這個模型每個月只用了一次,所以這樣使用緩存是很浪費的。

既然是批處理任務,就需要把任務進行分割,進行批量處理,採用分而治之、逐步計算的方法,得出最終的結果即可。

優秀實踐9

緩存的數據不易過大,尤其是Redis,因為Redis使用的是單線程模型,在單個緩存key的數據過大時,會阻塞其他請求的處理。

優秀實踐10

對於存儲較多value的key,儘量不要使用HGETALL等集合操作,該操作會造成請求阻塞,影響其他應用的訪問。

優秀實踐11

緩存一般用於在交易系統中加速查詢的場景,有大量的更新數據時,尤其是批量處理時,請使用批量模式,但是這種場景較少。

優秀實踐12

如果對性能的要求不是非常高,則儘量使用分佈式緩存,而不要使用本地緩存,因為本地緩存在服務的各個節點之間複製,在某一時刻副本之間是不一致的,如果這個緩存代表的是開關,而且分佈式系統中的請求有可能會重複,就會導致重複的請求走到兩個節點,一個節點的開關是開,一個節點的開關是關,如果請求處理沒有做到冪等,就會造成處理重複,在嚴重情況下會造成資金損失。

優秀實踐13

在寫緩存時一定要寫入完全正確的數據,如果緩存數據的一部分有效、一部分無效,則寧可放棄緩存,也不要把部分數據寫入緩存,否則會造成空指針、程序異常等。

優秀實踐14

在通常情況下,讀的順序是先緩存,後數據庫;寫的順序是先數據庫,後緩存。

優秀實踐15

在使用本地緩存(如Ehcache)時,一定要嚴格控制緩存對象的個數及聲明週期。由於JVM的特性,過多的緩存對象會極大影響JVM的性能,甚至導致內存溢出等。

優秀實踐16

在使用緩存時,一定要有降級處理,尤其是對關鍵的業務環節,緩存有問題或者失效時也要能回源到數據庫進行處理。

三、關於常見的緩存問題的線上案例

筆者在多家互聯網公司負責架構方案評審和線上事故覆盤,這裡列舉其中的一些典型案例,供大家參考和借鑑。

案例1

現象:某應用程序的數據庫負載瞬時升高。

原因:在應用程序中對使用的大量緩存key設置了同一個固定的失效時間,當緩存失效時,會造成在一段時間內同時訪問數據庫,造成數據庫的壓力較大。

總結:在使用緩存時需要進行緩存設計,要充分考慮如何避免常見的緩存穿透、緩存雪崩、緩存併發等問題,尤其是對於高併發的緩存使用,需要對key的過期時間進行隨機設置,例如,將過期時間設置為10秒+random(2),也就是將過期時間隨機設置成10~12秒。

案例2

現象:導致遷移前後兩個系統的核心操作重複。

原因:在遷移的過程中,重複的流量進入了不同的節點,由於使用了本地緩存存儲遷移開關,而遷移開關在開關打開的瞬間導致各個節點的開關狀態不一致,有的是開、有的是關,所以對於不同節點的流量的處理重複,一個走了開關開的邏輯,一個走了開關關的邏輯。

總結:避免使用本地緩存來存儲遷移開關,遷移開關應該在有狀態的訂單上標記。

案例3

現象:某模塊設計使用了緩存加速數據庫的讀操作的性能,但發現數據庫負載並沒有明顯下降。

原因:由於這個模塊的使用方查詢請求的數據在數據庫中不存在,是非法的數據,所以導致緩存沒有命中,每次都穿透到數據庫,且量級較大。

總結:在使用緩存時需要進行緩存設計,要充分考慮如何避免常見的緩存穿透、緩存雪崩、緩存併發等問題,尤其是對高併發的緩存使用,需要對無效的key進行緩存,以抵擋惡意的或者無意的對無效緩存查詢的攻擊或影響。

案例4

現象:監控系統報警,Redis中單個哈希鍵佔用的空間巨大。

原因:應用系統使用了哈希鍵,哈希鍵本身有過期時間,但是哈希鍵裡面的每個鍵值對沒有過期時間。

總結:在設計Redis的過程中,如果有大量的鍵值對要保存,則請使用字符串鍵的數據庫類型,並對每個鍵都設置過期時間,請不要在哈希鍵內部存儲一個沒有邊界的集合數據。實際上,無論是對緩存、內存還是對數據庫的設計,如果使用任意一個集合的數據結構,則都要考慮為它設置最大限制,避免內存用光,最常見的是集合溢出導致的內存溢出的問題。

案例5

現象:某業務項目由於緩存宕機導致業務邏輯中斷,數據不一致。

原因:Redis進行主備切換,導致瞬間內應用連接Redis異常,應用並沒有對緩存做降級處理。

總結:對於核心業務,在使用緩存時一定要有降級方案。常見的降級方案是在數據庫層次預留足夠的容量,在某一部分緩存出現問題時,可以讓應用暫時回源到數據庫繼續業務邏輯,而不應該中斷業務邏輯,但是這需要嚴格的容量評估,請參考《分佈式服務架構:原理設計與實戰》第3章的內容。

案例6

現象:某應用系統負載升高,響應變慢,發現應用進行頻繁GC,甚至出現OutOfMemroyError: GC overhead limt exceed的錯誤日誌。

原因:

因為這個項目是個歷史項目,使用了Hibernate ORM框架,在Hibernate中開啟了二級緩存,使用了Ehcache;但是在Ehcache中沒有控制緩存對象的個數,緩存對象增多,導致內存緊張,所以進行了頻繁的GC操作。

總結:

使用本地緩存(如Ehcache、OSCache、應用內存)時,一定要嚴格控制緩存對象的個數及聲明週期。

案例7

現象:某個正常運行的應用突然報警線程數過高,之後很快就出現了內存溢出。

原因:由於緩存連接數達到最大限制,應用無法連接緩存,並且超時時間設置得較大,導致訪問緩存的服務都在等待緩存操作返回,由於緩存負載較高,處理不完所有的請求,但是這些服務都在等待緩存操作返回,服務這時在等待,並沒有超時,就不能降級並繼續訪問數據庫。這在BIO模式下線程池就會撐滿,使用方的線程池也都撐滿;在NIO模式下一樣會使服務的負載增加,服務響應變慢,甚至使服務被壓垮。

總結:在使用遠程緩存(如Redis、Memcached)時,一定要對操作超時時間進行設置,這是非常關鍵的,一般我們設計緩存作為加速數據庫讀取的手段,也會對緩存操作做降級處理,因此推薦使用更短的緩存超時時間,如果一定要給出一個數字,則希望是100毫秒以內。

案例8

現象:某項目使用緩存存儲業務數據,上線後出現錯誤問題,開發人員束手無策。

原因:開發人員不知道如何發現、排查、定位和解決緩存問題。

總結:在設計緩存時要有降級方案,在遇到問題時首先使用降級方法,還要設計完善的監控和報警功能,幫助開發人員快速發現緩存問題,進而來定位和解決問題。

案例9

現象:某項目在使用緩存後,開發測試通過,到生產環境後,服務卻出現了不可預知的問題。

原因:該應用的緩存key與其他應用緩存 key衝突,導致互相覆蓋,出現邏輯錯誤。

總結:在使用緩存時一定要有隔離的設計,可以通過不同的緩存實例來做物理隔離,也可以通過各個應用的緩存key使用不同的前綴進行邏輯隔離。

"

相關推薦

推薦中...