1. 程式人生 > >提前排雷!分散式快取的25個優秀實踐與線上案例

提前排雷!分散式快取的25個優秀實踐與線上案例

作者介紹
楊彪,螞蟻金服技術專家,《分散式服務架構:原理、設計與實戰》和《可伸縮服務架構:框架與中介軟體》作者。近10年網際網路和遊戲行業工作經驗,曾在酷我音樂盒、人人遊戲和掌趣科技等上市公司擔任核心研發職位,做過日活躍使用者量達千萬的專案,也做過多款月流水千萬以上的遊戲。
本文節選自即將出版的《可伸縮服務架構:框架與中介軟體》一書,作者:李豔鵬、楊彪、李海亮、賈博巖、劉淏

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

一、快取設計的核心要素

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

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

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使用不同的字首進行邏輯隔離。