1. 程式人生 > >後端快取的23個關鍵關注點

後端快取的23個關鍵關注點

文章概要

▌1:極簡快取架構

通過JSR107規範,我們將框架定義為客戶端層、快取提供層、快取管理層、快取儲存層。其中快取儲存層又分為基本儲存層、LRU儲存層和Weak儲存層,如下圖所示。

快取分層圖

其中:

客戶端層:使用者直接通過該層與資料進行互動。

快取提供層:主要對快取管理層的生命週期進行維護,負責快取管理層的建立,儲存、獲取以及銷燬。

快取管理層:主要對快取客戶端的生命週期進行維護,負責快取客戶端的建立,儲存、獲取以及銷燬

快取儲存層:負責資料以什麼樣的形式進行儲存。

基本儲存層:是以普通的ConcurrentHashMap為儲存核心,資料不淘汰。

LRU儲存層:是以最近最少用為原則進行的資料儲存和快取淘汰機制。

Weak儲存層:是以弱引用為原則的資料儲存和快取淘汰機制。

▌2:容量評估

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

▌3:業務分離

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

▌4:監控為王

所有的快取例項都需要新增監控,這是非常重要的,我們需要對慢查詢、大物件、記憶體使用情況做可靠的監控。

▌5:失效時間

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

▌6:大量key同時失效時間的危害

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

筆者曾經見過一個case:在應用程式中對使用的大量快取key設定了同一個固定的失效時間,當快取失效時,會造成在一段時間內同時訪問資料庫,造成資料庫的壓力較大。

▌7:先更新資料庫後更新快取有啥問題?

想象一下,如果兩個執行緒同時執行更新操作,執行緒1更新資料庫後,執行緒2也更新了資料庫,然後開始寫快取,但執行緒2先執行了更新快取的操作,而執行緒1在執行更新快取的時候就把執行緒2更新的資料給覆蓋掉了,這樣就會出現資料不一致。

▌8:先刪快取, 行不行?

“先刪快取,然後執行資料庫事務”也有人討論這種方案,不過這種操作對於如商品這種查詢非常頻繁的業務不適用,因為在你刪快取的同時,已經有另一個系統來讀快取了,此時事務還沒有提交。當然對於如使用者維度的業務是可以考慮的。

▌9:資料庫和快取資料一致性

京東採用了通過canal更新快取原子性的方法,如下圖所示。

最終一致性方案

幾個關注點:

❑ 更新資料時使用更新時間戳或者版本對比。

❑ 使用如canal訂閱資料庫binlog;此處把mysql看成釋出者,binlog是釋出的內容,canal(canal 是阿里巴巴mysql資料庫binlog的增量訂閱&消費元件)看成消費者,canal訂閱binlog然後更新到Redis。

將更新請求按照相應的規則分散到多個佇列,然後每個佇列的進行單執行緒更新,更新時拉取最新的資料儲存;更新之前獲取相關的鎖再進行更新。

▌10.先更新資料庫,再刪除快取的一種實踐

流程如下圖所示:

過程不贅述,只強調一個,資料庫update變更會同步發到訊息,通過訊息去刪除快取。如果刪除失敗,訊息有重試機制保障。另外除了極端情況,快取更新是比較及時的。

▌11:本地快取的挑戰

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

▌12:快取熱點與多級快取

對於分散式快取,我們需要在Nginx+Lua應用中進行應用快取來減少Redis叢集的訪問衝擊;即首先查詢應用本地快取,如果命中則直接快取,如果沒有命中則接著查詢Redis叢集、回源到Tomcat;然後將資料快取到應用本地。如同14-8所示。

此處到應用Nginx的負載機制採用:正常情況採用一致性雜湊,如果某個請求型別訪問量突破了一定的閥值,則自動降級為輪詢機制。另外對於一些秒殺活動之類的熱點我們是可以提前知道的,可以把相關資料預先推送到應用Nginx並將負載均衡機制降級為輪詢。

分散式快取方案

另外可以考慮建立實時熱點發現系統來發現熱點,如下圖所示:

實時熱點發現方案

1)接入Nginx將請求轉發給應用Nginx;

2)應用Nginx首先讀取本地快取;如果命中直接返回,不命中會讀取分散式快取、回源到Tomcat進行處理;

3)應用Nginx會將請求上報給實時熱點發現系統,如使用UDP直接上報請求、或者將請求寫到本地kafka、或者使用flume訂閱本地nginx日誌;上報給實時熱點發現系統後,它將進行統計熱點(可以考慮storm實時計算);

4)根據設定的閥值將熱點資料推送到應用Nginx本地快取。

因為做了本地快取,因此對於資料一致性需要我們去考慮,即何時失效或更新快取:

1)如果可以訂閱資料變更訊息,那麼可以訂閱變更訊息進行快取更新;

2)如果無法訂閱訊息或者訂閱訊息成本比較高,並且對短暫的資料一致性要求不嚴格(比如在商品詳情頁看到的庫存,可以短暫的不一致,只要保證下單時一致即可),那麼可以設定合理的過期時間,過期後再查詢新的資料;

3)如果是秒殺之類的,可以訂閱活動開啟訊息,將相關資料提前推送到前端應用,並將負載均衡機制降級為輪詢;

4)建立實時熱點發現系統來對熱點進行統一推送和更新。

應對快取大熱點:資料複製模式

在Facebook有一招,就是通過多個key_index(key:xxx#N) 來解決資料的熱點讀問題。解決方案是所有熱點key釋出到所有web伺服器;每個伺服器的key有對應別名,可以通過client端的演算法路由到某臺伺服器;做刪除動作時,刪除所有的別名key。可簡單總結為一個通用的group內一致模型。把快取叢集劃分為若干分組(group),在同組內,所有的快取伺服器,都發布熱點key的資料。

對於大量讀操作而言,通過client端路由策略,隨意返回一臺機器即可;而寫操作,有一種解法是通過定時任務來寫入;Facebook採取的是刪除所有別名key的策略。如何保障這一個批量操作都成功?

(1)容忍部分失敗導致的資料版本問題
(2)只要有寫操作,則通過定時任務重新整理快取;如果涉及3臺伺服器,則都操作成功代表該任務表的這條記錄成功完成使命,否則會重試。

▌13:快取失效的連線風暴
引起這個問題的主要原因還是高併發的時候,平時我們設定一個快取的過期時間時,可能有一些會設定1分鐘,5分鐘,併發很高可能會出在某一個時間同時生成了很多的快取,並且過期時間都一樣,這個時候就可能引發過期時間到後,這些快取同時失效,請求全部轉發到DB,DB可能會壓力過重。那如何解決這些問題呢?

其中的一個簡單方案就是將快取失效時間分散開,比如我們可以在原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個快取的過期時間的重複率就會降低,就很難引發集體失效的事件。

如果快取集中在一段時間內失效,DB的壓力凸顯。這個沒有完美解決辦法,但可以分析使用者行為,儘量讓失效時間點均勻分佈。

上述是快取使用過程中經常遇到的併發穿透、併發失效問題。一般情況下,我們解決這些問題的方法是,引入空值、鎖和隨機快取過期時間的機制。

▌14:快取預熱

提前把資料讀入到快取的做法就是資料預熱處理。資料預熱處理要注意一些細節問題:

(1)是否有監控機制確保預熱資料都寫成功了!筆者曾經遇到部分資料成功而影響高峰期業務的案例;

(2)資料預熱配備回滾方案,遇到緊急回滾時便於操作。對於新建cache server叢集,也可以通過資料預熱模式來做一番手腳。如下圖所示,先從冷叢集中獲取key,如果獲取不到,則從熱叢集中獲取。同時把獲取到的key put到冷叢集。如下圖

資料預熱

(3)預熱資料量的考量,要做好容量評估。在容量允許的範圍內預熱全量,否則預熱訪問量高的。

(4)預熱過程中需要注意是否會因為批量資料庫操作或慢sql等引發資料庫效能問題。

▌15:超時時間設計

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

筆者曾經遇到過一個案例:某個正常執行的應用突然報警執行緒數過高,之後很快就出現了記憶體溢位。

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

▌16:不要把快取到儲存

大家都知道一個顛撲不破的真理:在分散式架構下,一切系統都可能fail,無論是快取、儲存包括資料庫還是應用伺服器,而且部分快取本身就未提供持久化機制比如memcached。即使使用持久化機制的cache,也要慎用,如果作為唯一儲存的話。

▌17:快取崩潰解決之道

當我們使用分散式快取時,應該考慮如何應對其中一部分快取例項宕機的情況。接下來部分將介紹分散式快取時的常用演算法。而當快取資料是可丟失的情況時,我們可以選擇一致性雜湊演算法。

取模

對於取模機制如果其中一個例項壞了,如果摘除此例項將導致大量快取不命中,瞬間大流量可能導致後端DB/服務出現問題。對於這種情況可以採用主從機制來避免例項壞了的問題,即其中一個例項壞了可以那從/主頂上來。但是取模機制下如果增加一個節點將導致大量快取不命中,一般是建立另一個叢集,然後把資料遷移到新叢集,然後把流量遷移過去。

一致性雜湊

對於一致性雜湊機制如果其中一個例項壞了,如果摘除此例項將隻影響一致性雜湊環上的部分快取不命中,不會導致瞬間大量回源到後端DB/服務,但是也會產生一些影響。

▌18. 快取崩潰後的快速恢復

如果出現之前說到的一些問題,可以考慮如下方案:

1)主從機制,做好冗餘,即其中一部分不可用,將對等的部分補上去;

2)如果因為快取導致應用可用性已經下降可以考慮:

部分使用者降級,然後慢慢減少降級量;

後臺通過Worker預熱快取資料。

也就是如果整個快取叢集壞了,而且沒有備份,那麼只能去慢慢將快取重建;為了讓部分使用者還是可用的,可以根據系統承受能力,通過降級方案讓一部分使用者先用起來,將這些使用者相關的快取重建;另外通過後臺Worker進行快取資料的預熱。

▌19. 開啟Nginx Proxy Cache效能不升反降
開啟Nginx Proxy Cache後,效能下降,而且過一段記憶體使用率到達98%;解決方案:

1)對於記憶體佔用率高的問題是核心問題,核心使用LRU機制,本身不是問題,不過可以通過修改核心引數:

sysctl -wvm.extra_free_kbytes=6436787

sysctl -wvm.vfs_cache_pressure=10000

2)使用Proxy Cache在機械盤上效能差可以通過tmpfs快取或nginx共享字典快取元資料,或者使用SSD,我們目前使用記憶體檔案系統。

▌20:“網路抖動時,返回502錯誤”緣於timeout
Twemproxy配置的timeout時間太長,之前設定為5s,而且沒有分別針對連線、讀、寫設定超時。後來我們減少超時時間,內網設定在150ms以內,當超時時訪問動態服務。

▌21:應對惡意刷的經驗
商品詳情頁庫存介面2014年被惡意刷,每分鐘超過600w訪問量,tomcat機器只能定時重啟;因為是詳情頁展示的資料,快取幾秒鐘是可以接受的,因此開啟nginxproxy cache來解決該問題,開啟後降到正常水平;後來我們使用Nginx+Lua架構改造服務,資料過濾、URL重寫等在Nginx層完成,通過URL重寫+一致性雜湊負載均衡,不怕隨機URL,一些服務提升了10%+的快取命中率。

▌22:網絡卡打滿了咋辦?

用Redis都有個很頭疼的問題,就是Redis的網絡卡打滿問題,由於Redis的效能很高,在大併發請求下,很容易將網絡卡打滿.通常情況下,1臺伺服器上都會跑幾十個Redis例項 ,一旦網絡卡打滿,很容易干擾到應用層可用性.所以我們基於開源的Contiv netplugin專案,限制了網絡卡的使用, 主要功能是提供基於Policy的網路和儲存管理。Contiv比較“誘人”的一點就是,它的網路管理能力,既有L2(VLAN)、L3(BGP),又有 Overlay(VxLAN),有了它就可以無視底層的網路基礎架構,向上層容器提供一致的虛擬網路了。最主要的一點是,既滿足了業務場景,又相容了以往的網路架構。在轉發效能上,它能接近物理網絡卡的效能,特別在沒有萬兆網路的老機房也能很好的使用。在網路流量監控方面,我們通過使用ovs的sflow來抓取宿主機上所有的網路流量,然後自開發了一個簡單的sflow Collecter, 伺服器收到sflow的資料包進行解析,篩選出關鍵資料,然後進行彙總分析,得到所需要的監控資料。通過這個定製的網路外掛,我們可以隨意的控制某個Redis的流量,流量過大,也不會影響其他的專案,而如果某個伺服器上的Redis流量很低,我們也可以縮小它的配額,提供給本機其他需要大流量的程式使用,這些,通過後臺的監控程式,可以實現完全自動化。

▌23:快取元件的選擇
快取的種類很多,我們實際使用時,需要根據快取位置(系統前後端)、待存資料型別、訪問方式、記憶體效率等情況來選擇最適合的快取元件。本小節接下來將主要探討在應用層後端如何選擇分散式快取元件。

一般業務系統中,大部分資料都是簡單KV資料型別,如前述微博Feed系統中的feed content、feed列表、使用者資訊等。這些簡單型別資料只需要進行set、get、delete操作,不需要在快取端做計算操作,最適合以memcached作為快取元件。

其次對於需要部分獲取、事物型變更、快取端計算的集合類資料,擁有豐富資料結構和訪問介面的Redis 也許會更適合。Redis還支援以主從(master-slave)方式進行資料備份,支援資料的持久化,可以將記憶體中的資料保持在磁碟,重啟時再次載入使用。因磁碟快取(diskstore)方式的效能問題,Redis資料基本只適合儲存在記憶體中,由此帶來的問題是:在某些業務場景,如果待快取的資料量特別大,而資料的訪問量不太大或者有冷熱區分,也必須將所有資料全部放在記憶體中,快取成本(特別是機器成本)會特別高。如果業務遇到這種場景,可以考慮用pika、ssdb等其他快取元件。pika、ssdb都相容Redis協議,同時採用多執行緒方案,支援持久化和複製,單個快取例項可以快取數百G的資料,其中少部分的熱資料存放記憶體,大部分溫熱資料或冷資料都可以放在磁碟,從而很好的降低快取成本。

對前面講到的這些後端常用的快取元件,可以參考下表進行選擇。

快取元件

資料型別

訪問方式

資料容量

(單例項)

同步

記憶體效率

Memcached

簡單KV

GET SET DEL等常規介面

100G以下

Client 多寫

一般

Redis

豐富

更豐富常規介面、事物更新等

30G以下

主從複製

一般

Pika/ssdb

較豐富,部分Redis資料結構不支援

較豐富

數百G以上

主從複製

一般

最後,對於對儲存效率、訪問效能等有更高要求的業務場景,結合業務特性進行快取元件的定製化設計與開發,也是一個很好的選擇。

總之,快取元件的選型要考慮資料模型、訪問方式、快取成本甚至開發人員的知識結構,從而進行因地制宜的取捨,不要盲目引入不熟悉、不活躍、不成熟的快取元件,否則中途頻繁調整快取方案,會給開發進度、運維成本帶來較大的挑戰。

原文連結

https://mp.weixin.qq.com/s?__biz=MzIxMzEzMjM5NQ%3D%3D&mid=2651030776&idx=1&sn=87e55b76d625a0e52b4d9f327fedbaf1&chksm=8c4c51fcbb3bd8ea2708048a15621088d095dad309342368c2d21e810362633a835c43c33db2&mpshare=1&scene=23&srcid=%23rd

服務推薦