本文主要介紹使用分布式緩存的優(yōu)秀實踐和線上案例观蜗。這些案例是筆者在多家互聯(lián)網(wǎng)公司里積累并形成的優(yōu)秀實踐缕坎,能夠幫助大家在生產(chǎn)實踐中避免很多不必要的生產(chǎn)事故。
一教藻、緩存設計的核心要素
我們在應用中決定使用緩存時距帅,通常需要進行詳細的設計,因為設計緩存架構(gòu)看似簡單括堤,實則不然碌秸,里面蘊含了很多深奧的原理,如果使用不當悄窃,則會造成很多生產(chǎn)事故甚至是服務雪崩之類的嚴重問題讥电。
筆者在做設計評審的過程中,總結(jié)了所有與緩存設計相關的設計點轧抗,這里列出來供大家參考恩敌。
1、容量規(guī)劃
- 緩存內(nèi)容的大小
- 緩存內(nèi)容的數(shù)量
- 淘汰策略
- 緩存的數(shù)據(jù)結(jié)構(gòu)
- 每秒的讀峰值
- 每秒的寫峰值
2横媚、性能優(yōu)化
- 線程模型
- 預熱方法
- 緩存分片
- 冷熱數(shù)據(jù)的比例
3潮剪、高可用
- 復制模型
- 失效轉(zhuǎn)移
- 持久策略
- 緩存重建
4、緩存監(jiān)控
- 緩存服務監(jiān)控
- 緩存容量監(jiān)控
- 緩存請求監(jiān)控
- 緩存響應時間監(jiān)控
5分唾、注意事項
- 是否有可能發(fā)生緩存穿透
- 是否有大對象
- 是否使用緩存實現(xiàn)分布式鎖
- 是否使用緩存支持的腳本(Lua)
- 是否避免了Race Condition
筆者在這里把這些設計點提供給讀者抗碰,請讀者在做緩存設計時把每一項作為一個思考的起點,思考我們在設計緩存時是否想到了這些點绽乔,以避免在設計的過程中因忽略某一項而導致嚴重的線上事故發(fā)生弧蝇。
二、緩存設計的優(yōu)秀實踐
筆者在做設計評審的過程中折砸,總結(jié)了一些開發(fā)人員在設計緩存系統(tǒng)時的優(yōu)秀實踐看疗,如下所述:
優(yōu)秀實踐1
緩存系統(tǒng)主要消耗的是服務器的內(nèi)存,因此睦授,在使用緩存時必須先對應用需要緩存的數(shù)據(jù)大小進行評估两芳,包括緩存的數(shù)據(jù)結(jié)構(gòu)、緩存大小去枷、緩存數(shù)量怖辆、緩存的失效時間,然后根據(jù)業(yè)務情況自行推算在未來一定時間內(nèi)的容量的使用情況删顶,根據(jù)容量評估的結(jié)果來申請和分配緩存資源竖螃,否則會造成資源浪費或者緩存空間不夠。
優(yōu)秀實踐2
建議將使用緩存的業(yè)務進行分離逗余,核心業(yè)務和非核心業(yè)務使用不同的緩存實例特咆,從物理上進行隔離,如果有條件录粱,則請對每個業(yè)務使用單獨的實例或者集群腻格,以減小應用之間互相影響的可能性画拾。筆者就經(jīng)常聽說有的公司應用了共享緩存,造成緩存數(shù)據(jù)被覆蓋以及緩存數(shù)據(jù)錯亂的線上事故菜职。
優(yōu)秀實踐3
根據(jù)緩存實例提供的內(nèi)存大小推算應用需要使用的緩存實例數(shù)量青抛,一般在公司里會成立一個緩存管理的運維團隊,這個團隊會將緩存資源虛擬成多個相同內(nèi)存大小的緩存實例些楣。
例如一個實例有4GB內(nèi)存脂凶,在應用申請時可以按需申請足夠的實例數(shù)量來使用,對這樣的應用需要進行分片愁茁,詳情請參考《可伸縮服務架構(gòu):框架與中間件》中4.4.3的內(nèi)容蚕钦。這里需要注意,如果我們使用了RDB備份機制鹅很,每個實例使用4GB內(nèi)存嘶居,則我們的系統(tǒng)需要大于8GB內(nèi)存,因為RDB備份時使用了 copy-on-write 機制促煮,需要fork出一個子進程邮屁,并且復制一份內(nèi)存,因此需要雙份的內(nèi)存存儲大小菠齿。
優(yōu)秀實踐4
緩存一般是用來加速數(shù)據(jù)庫的讀操作的佑吝,一般先訪問緩存后訪問數(shù)據(jù)庫,所以緩存的超時時間的設置是很重要的绳匀。筆者曾經(jīng)在一家互聯(lián)網(wǎng)公司遇到過由于運維操作失誤導致緩存超時設置得較長芋忿,從而拖垮服務的線程池,最終導致服務雪崩的情況疾棵。
優(yōu)秀實踐5
所有的緩存實例都需要添加監(jiān)控戈钢,這是非常重要的,我們需要對慢查詢是尔、大對象殉了、內(nèi)存使用情況做可靠的監(jiān)控。
優(yōu)秀實踐6
我們不推薦多個業(yè)務共享一個緩存實例拟枚,但是由于成本控制的原因薪铜,這種情況經(jīng)常出現(xiàn),我們需要通過規(guī)范來限制各個應用使用的key有唯一的前綴梨州,并進行隔離設計痕囱,避免產(chǎn)生緩存互相覆蓋的問題。
優(yōu)秀實踐7
任何緩存的key都必須設定緩存失效時間暴匠,且失效時間不能集中在某一點,否則會導致緩存占滿內(nèi)存或者緩存雪崩傻粘。
優(yōu)秀實踐8
低頻訪問的數(shù)據(jù)不要放在緩存中每窖,如我們前面所說的帮掉,我們使用緩存的主要目的是提高讀取性能。
曾經(jīng)有個小伙伴設計了一套定時的批處理系統(tǒng)窒典,由于批處理系統(tǒng)需要對一個大的數(shù)據(jù)模型進行計算蟆炊,所以該小伙伴把這個數(shù)據(jù)模型保存在每個節(jié)點的本地緩存中,并通過消息隊列接收更新的消息來維護本地緩存中模型的實時性瀑志,但是這個模型每個月只用了一次涩搓,所以這樣使用緩存是很浪費的。
既然是批處理任務劈猪,就需要把任務進行分割昧甘,進行批量處理,采用分而治之战得、逐步計算的方法充边,得出最終的結(jié)果即可。
優(yōu)秀實踐9
緩存的數(shù)據(jù)不易過大常侦,尤其是Redis浇冰,因為Redis使用的是單線程模型,在單個緩存key的數(shù)據(jù)過大時聋亡,會阻塞其他請求的處理肘习。
優(yōu)秀實踐10
對于存儲較多value的key,盡量不要使用HGETALL等集合操作坡倔,該操作會造成請求阻塞漂佩,影響其他應用的訪問。
優(yōu)秀實踐11
緩存一般用于在交易系統(tǒng)中加速查詢的場景致讥,有大量的更新數(shù)據(jù)時仅仆,尤其是批量處理時,請使用批量模式垢袱,但是這種場景較少墓拜。
優(yōu)秀實踐12
如果對性能的要求不是非常高,則盡量使用分布式緩存请契,而不要使用本地緩存咳榜,因為本地緩存在服務的各個節(jié)點之間復制,在某一時刻副本之間是不一致的爽锥,如果這個緩存代表的是開關涌韩,而且分布式系統(tǒng)中的請求有可能會重復,就會導致重復的請求走到兩個節(jié)點氯夷,一個節(jié)點的開關是開臣樱,一個節(jié)點的開關是關,如果請求處理沒有做到冪等,就會造成處理重復雇毫,在嚴重情況下會造成資金損失玄捕。
優(yōu)秀實踐13
在寫緩存時一定要寫入完全正確的數(shù)據(jù),如果緩存數(shù)據(jù)的一部分有效棚放、一部分無效枚粘,則寧可放棄緩存,也不要把部分數(shù)據(jù)寫入緩存飘蚯,否則會造成空指針馍迄、程序異常等。
優(yōu)秀實踐14
在通常情況下局骤,讀的順序是先緩存攀圈,后數(shù)據(jù)庫;寫的順序是先數(shù)據(jù)庫庄涡,后緩存量承。
優(yōu)秀實踐15
在使用本地緩存(如Ehcache)時,一定要嚴格控制緩存對象的個數(shù)及聲明周期穴店。由于JVM的特性撕捍,過多的緩存對象會極大影響JVM的性能,甚至導致內(nèi)存溢出等泣洞。
優(yōu)秀實踐16
在使用緩存時忧风,一定要有降級處理,尤其是對關鍵的業(yè)務環(huán)節(jié)球凰,緩存有問題或者失效時也要能回源到數(shù)據(jù)庫進行處理狮腿。
三、關于常見的緩存問題的線上案例
筆者在多家互聯(lián)網(wǎng)公司負責架構(gòu)方案評審和線上事故復盤呕诉,這里列舉其中的一些典型案例缘厢,供大家參考和借鑒。
案例1
現(xiàn)象:某應用程序的數(shù)據(jù)庫負載瞬時升高甩挫。
原因:在應用程序中對使用的大量緩存key設置了同一個固定的失效時間贴硫,當緩存失效時,會造成在一段時間內(nèi)同時訪問數(shù)據(jù)庫伊者,造成數(shù)據(jù)庫的壓力較大英遭。
總結(jié):在使用緩存時需要進行緩存設計,要充分考慮如何避免常見的緩存穿透亦渗、緩存雪崩挖诸、緩存并發(fā)等問題,尤其是對于高并發(fā)的緩存使用法精,需要對key的過期時間進行隨機設置多律,例如痴突,將過期時間設置為10秒+random(2),也就是將過期時間隨機設置成10~12秒菱涤。
案例2
現(xiàn)象:導致遷移前后兩個系統(tǒng)的核心操作重復苞也。
原因:在遷移的過程中扛点,重復的流量進入了不同的節(jié)點丈秩,由于使用了本地緩存存儲遷移開關特幔,而遷移開關在開關打開的瞬間導致各個節(jié)點的開關狀態(tài)不一致,有的是開攻走、有的是關,所以對于不同節(jié)點的流量的處理重復此再,一個走了開關開的邏輯昔搂,一個走了開關關的邏輯。
總結(jié):避免使用本地緩存來存儲遷移開關输拇,遷移開關應該在有狀態(tài)的訂單上標記摘符。
案例3
現(xiàn)象:某模塊設計使用了緩存加速數(shù)據(jù)庫的讀操作的性能,但發(fā)現(xiàn)數(shù)據(jù)庫負載并沒有明顯下降策吠。
原因:由于這個模塊的使用方查詢請求的數(shù)據(jù)在數(shù)據(jù)庫中不存在逛裤,是非法的數(shù)據(jù),所以導致緩存沒有命中猴抹,每次都穿透到數(shù)據(jù)庫带族,且量級較大。
總結(jié):在使用緩存時需要進行緩存設計蟀给,要充分考慮如何避免常見的緩存穿透蝙砌、緩存雪崩、緩存并發(fā)等問題跋理,尤其是對高并發(fā)的緩存使用择克,需要對無效的key進行緩存,以抵擋惡意的或者無意的對無效緩存查詢的攻擊或影響前普。
案例4
現(xiàn)象:監(jiān)控系統(tǒng)報警肚邢,Redis中單個哈希鍵占用的空間巨大。
原因:應用系統(tǒng)使用了哈希鍵汁政,哈希鍵本身有過期時間道偷,但是哈希鍵里面的每個鍵值對沒有過期時間。
總結(jié):在設計Redis的過程中记劈,如果有大量的鍵值對要保存勺鸦,則請使用字符串鍵的數(shù)據(jù)庫類型,并對每個鍵都設置過期時間目木,請不要在哈希鍵內(nèi)部存儲一個沒有邊界的集合數(shù)據(jù)换途。實際上懊渡,無論是對緩存、內(nèi)存還是對數(shù)據(jù)庫的設計军拟,如果使用任意一個集合的數(shù)據(jù)結(jié)構(gòu)剃执,則都要考慮為它設置最大限制,避免內(nèi)存用光懈息,最常見的是集合溢出導致的內(nèi)存溢出的問題肾档。
案例5
現(xiàn)象:某業(yè)務項目由于緩存宕機導致業(yè)務邏輯中斷,數(shù)據(jù)不一致辫继。
原因:Redis進行主備切換怒见,導致瞬間內(nèi)應用連接Redis異常,應用并沒有對緩存做降級處理姑宽。
總結(jié):對于核心業(yè)務遣耍,在使用緩存時一定要有降級方案。常見的降級方案是在數(shù)據(jù)庫層次預留足夠的容量炮车,在某一部分緩存出現(xiàn)問題時舵变,可以讓應用暫時回源到數(shù)據(jù)庫繼續(xù)業(yè)務邏輯,而不應該中斷業(yè)務邏輯瘦穆,但是這需要嚴格的容量評估纪隙,請參考《分布式服務架構(gòu):原理設計與實戰(zhàn)》第3章的內(nèi)容。
案例6
現(xiàn)象:某應用系統(tǒng)負載升高难审,響應變慢瘫拣,發(fā)現(xiàn)應用進行頻繁GC,甚至出現(xiàn)OutOfMemroyError: GC overhead limt exceed的錯誤日志告喊。
原因:
因為這個項目是個歷史項目麸拄,使用了Hibernate ORM框架,在Hibernate中開啟了二級緩存黔姜,使用了Ehcache拢切;但是在Ehcache中沒有控制緩存對象的個數(shù),緩存對象增多秆吵,導致內(nèi)存緊張淮椰,所以進行了頻繁的GC操作。
總結(jié):
使用本地緩存(如Ehcache纳寂、OSCache主穗、應用內(nèi)存)時,一定要嚴格控制緩存對象的個數(shù)及聲明周期毙芜。
案例7
現(xiàn)象:某個正常運行的應用突然報警線程數(shù)過高忽媒,之后很快就出現(xiàn)了內(nèi)存溢出。
原因:由于緩存連接數(shù)達到最大限制腋粥,應用無法連接緩存晦雨,并且超時時間設置得較大架曹,導致訪問緩存的服務都在等待緩存操作返回,由于緩存負載較高闹瞧,處理不完所有的請求绑雄,但是這些服務都在等待緩存操作返回,服務這時在等待奥邮,并沒有超時万牺,就不能降級并繼續(xù)訪問數(shù)據(jù)庫。這在BIO模式下線程池就會撐滿漠烧,使用方的線程池也都撐滿杏愤;在NIO模式下一樣會使服務的負載增加,服務響應變慢已脓,甚至使服務被壓垮。
總結(jié):在使用遠程緩存(如Redis通殃、Memcached)時度液,一定要對操作超時時間進行設置,這是非常關鍵的画舌,一般我們設計緩存作為加速數(shù)據(jù)庫讀取的手段堕担,也會對緩存操作做降級處理,因此推薦使用更短的緩存超時時間曲聂,如果一定要給出一個數(shù)字霹购,則希望是100毫秒以內(nèi)。
案例8
現(xiàn)象:某項目使用緩存存儲業(yè)務數(shù)據(jù)朋腋,上線后出現(xiàn)錯誤問題齐疙,開發(fā)人員束手無策。
原因:開發(fā)人員不知道如何發(fā)現(xiàn)旭咽、排查贞奋、定位和解決緩存問題。
總結(jié):在設計緩存時要有降級方案穷绵,在遇到問題時首先使用降級方法轿塔,還要設計完善的監(jiān)控和報警功能,幫助開發(fā)人員快速發(fā)現(xiàn)緩存問題仲墨,進而來定位和解決問題勾缭。
案例9
現(xiàn)象:某項目在使用緩存后,開發(fā)測試通過目养,到生產(chǎn)環(huán)境后俩由,服務卻出現(xiàn)了不可預知的問題。
原因:該應用的緩存key與其他應用緩存 key沖突混稽,導致互相覆蓋采驻,出現(xiàn)邏輯錯誤审胚。
總結(jié):在使用緩存時一定要有隔離的設計,可以通過不同的緩存實例來做物理隔離礼旅,也可以通過各個應用的緩存key使用不同的前綴進行邏輯隔離膳叨。