提前排雷!分布式緩存的25個(gè)優(yōu)秀實(shí)踐與線上案例
本文主要介紹使用分布式緩存的優(yōu)秀實(shí)踐和線上案例。這些案例是筆者在多家互聯(lián)網(wǎng)公司里積累并形成的優(yōu)秀實(shí)踐,能夠幫助大家在生產(chǎn)實(shí)踐中避免很多不必要的生產(chǎn)事故。
一、緩存設(shè)計(jì)的核心要素
我們?cè)趹?yīng)用中決定使用緩存時(shí),通常需要進(jìn)行詳細(xì)的設(shè)計(jì),因?yàn)樵O(shè)計(jì)緩存架構(gòu)看似簡(jiǎn)單,實(shí)則不然,里面蘊(yùn)含了很多深?yuàn)W的原理,如果使用不當(dāng),則會(huì)造成很多生產(chǎn)事故甚至是服務(wù)雪崩之類的嚴(yán)重問題。
筆者在做設(shè)計(jì)評(píng)審的過程中,總結(jié)了所有與緩存設(shè)計(jì)相關(guān)的設(shè)計(jì)點(diǎn),這里列出來(lái)供大家參考。
1、容量規(guī)劃
緩存內(nèi)容的大小
緩存內(nèi)容的數(shù)量
淘汰策略
緩存的數(shù)據(jù)結(jié)構(gòu)
每秒的讀峰值
每秒的寫峰值
2、性能優(yōu)化
線程模型
預(yù)熱方法
緩存分片
冷熱數(shù)據(jù)的比例
3、高可用
復(fù)制模型
失效轉(zhuǎn)移
持久策略
緩存重建
4、緩存監(jiān)控
緩存服務(wù)監(jiān)控
緩存容量監(jiān)控
緩存請(qǐng)求監(jiān)控
緩存響應(yīng)時(shí)間監(jiān)控
5、注意事項(xiàng)
是否有可能發(fā)生緩存穿透
是否有大對(duì)象
是否使用緩存實(shí)現(xiàn)分布式鎖
是否使用緩存支持的腳本(Lua)
是否避免了Race Condition
筆者在這里把這些設(shè)計(jì)點(diǎn)提供給讀者,請(qǐng)讀者在做緩存設(shè)計(jì)時(shí)把每一項(xiàng)作為一個(gè)思考的起點(diǎn),思考我們?cè)谠O(shè)計(jì)緩存時(shí)是否想到了這些點(diǎn),以避免在設(shè)計(jì)的過程中因忽略某一項(xiàng)而導(dǎo)致嚴(yán)重的線上事故發(fā)生。
二、緩存設(shè)計(jì)的優(yōu)秀實(shí)踐
筆者在做設(shè)計(jì)評(píng)審的過程中,總結(jié)了一些開發(fā)人員在設(shè)計(jì)緩存系統(tǒng)時(shí)的優(yōu)秀實(shí)踐,如下所述:
優(yōu)秀實(shí)踐1
緩存系統(tǒng)主要消耗的是服務(wù)器的內(nèi)存,因此,在使用緩存時(shí)必須先對(duì)應(yīng)用需要緩存的數(shù)據(jù)大小進(jìn)行評(píng)估,包括緩存的數(shù)據(jù)結(jié)構(gòu)、緩存大小、緩存數(shù)量、緩存的失效時(shí)間,然后根據(jù)業(yè)務(wù)情況自行推算在未來(lái)一定時(shí)間內(nèi)的容量的使用情況,根據(jù)容量評(píng)估的結(jié)果來(lái)申請(qǐng)和分配緩存資源,否則會(huì)造成資源浪費(fèi)或者緩存空間不夠。
優(yōu)秀實(shí)踐2
建議將使用緩存的業(yè)務(wù)進(jìn)行分離,核心業(yè)務(wù)和非核心業(yè)務(wù)使用不同的緩存實(shí)例,從物理上進(jìn)行隔離,如果有條件,則請(qǐng)對(duì)每個(gè)業(yè)務(wù)使用單獨(dú)的實(shí)例或者集群,以減小應(yīng)用之間互相影響的可能性。筆者就經(jīng)常聽說(shuō)有的公司應(yīng)用了共享緩存,造成緩存數(shù)據(jù)被覆蓋以及緩存數(shù)據(jù)錯(cuò)亂的線上事故。
優(yōu)秀實(shí)踐3
根據(jù)緩存實(shí)例提供的內(nèi)存大小推算應(yīng)用需要使用的緩存實(shí)例數(shù)量,一般在公司里會(huì)成立一個(gè)緩存管理的運(yùn)維團(tuán)隊(duì),這個(gè)團(tuán)隊(duì)會(huì)將緩存資源虛擬成多個(gè)相同內(nèi)存大小的緩存實(shí)例。
例如一個(gè)實(shí)例有4GB內(nèi)存,在應(yīng)用申請(qǐng)時(shí)可以按需申請(qǐng)足夠的實(shí)例數(shù)量來(lái)使用,對(duì)這樣的應(yīng)用需要進(jìn)行分片,詳情請(qǐng)參考《可伸縮服務(wù)架構(gòu):框架與中間件》中4.4.3的內(nèi)容。這里需要注意,如果我們使用了RDB備份機(jī)制,每個(gè)實(shí)例使用4GB內(nèi)存,則我們的系統(tǒng)需要大于8GB內(nèi)存,因?yàn)镽DB備份時(shí)使用了 copy-on-write 機(jī)制,需要fork出一個(gè)子進(jìn)程,并且復(fù)制一份內(nèi)存,因此需要雙份的內(nèi)存存儲(chǔ)大小。
優(yōu)秀實(shí)踐4
緩存一般是用來(lái)加速數(shù)據(jù)庫(kù)的讀操作的,一般先訪問緩存后訪問數(shù)據(jù)庫(kù),所以緩存的超時(shí)時(shí)間的設(shè)置是很重要的。筆者曾經(jīng)在一家互聯(lián)網(wǎng)公司遇到過由于運(yùn)維操作失誤導(dǎo)致緩存超時(shí)設(shè)置得較長(zhǎng),從而拖垮服務(wù)的線程池,最終導(dǎo)致服務(wù)雪崩的情況。
優(yōu)秀實(shí)踐5
所有的緩存實(shí)例都需要添加監(jiān)控,這是非常重要的,我們需要對(duì)慢查詢、大對(duì)象、內(nèi)存使用情況做可靠的監(jiān)控。
優(yōu)秀實(shí)踐6
我們不推薦多個(gè)業(yè)務(wù)共享一個(gè)緩存實(shí)例,但是由于成本控制的原因,這種情況經(jīng)常出現(xiàn),我們需要通過規(guī)范來(lái)限制各個(gè)應(yīng)用使用的key有唯一的前綴,并進(jìn)行隔離設(shè)計(jì),避免產(chǎn)生緩存互相覆蓋的問題。
優(yōu)秀實(shí)踐7
任何緩存的key都必須設(shè)定緩存失效時(shí)間,且失效時(shí)間不能集中在某一點(diǎn),否則會(huì)導(dǎo)致緩存占滿內(nèi)存或者緩存雪崩。
優(yōu)秀實(shí)踐8
低頻訪問的數(shù)據(jù)不要放在緩存中,如我們前面所說(shuō)的,我們使用緩存的主要目的是提高讀取性能。
曾經(jīng)有個(gè)小伙伴設(shè)計(jì)了一套定時(shí)的批處理系統(tǒng),由于批處理系統(tǒng)需要對(duì)一個(gè)大的數(shù)據(jù)模型進(jìn)行計(jì)算,所以該小伙伴把這個(gè)數(shù)據(jù)模型保存在每個(gè)節(jié)點(diǎn)的本地緩存中,并通過消息隊(duì)列接收更新的消息來(lái)維護(hù)本地緩存中模型的實(shí)時(shí)性,但是這個(gè)模型每個(gè)月只用了一次,所以這樣使用緩存是很浪費(fèi)的。
既然是批處理任務(wù),就需要把任務(wù)進(jìn)行分割,進(jìn)行批量處理,采用分而治之、逐步計(jì)算的方法,得出最終的結(jié)果即可。
優(yōu)秀實(shí)踐9
緩存的數(shù)據(jù)不易過大,尤其是Redis,因?yàn)镽edis使用的是單線程模型,在單個(gè)緩存key的數(shù)據(jù)過大時(shí),會(huì)阻塞其他請(qǐng)求的處理。
優(yōu)秀實(shí)踐10
對(duì)于存儲(chǔ)較多value的key,盡量不要使用HGETALL等集合操作,該操作會(huì)造成請(qǐng)求阻塞,影響其他應(yīng)用的訪問。
優(yōu)秀實(shí)踐11
緩存一般用于在交易系統(tǒng)中加速查詢的場(chǎng)景,有大量的更新數(shù)據(jù)時(shí),尤其是批量處理時(shí),請(qǐng)使用批量模式,但是這種場(chǎng)景較少。
優(yōu)秀實(shí)踐12
如果對(duì)性能的要求不是非常高,則盡量使用分布式緩存,而不要使用本地緩存,因?yàn)楸镜鼐彺嬖诜?wù)的各個(gè)節(jié)點(diǎn)之間復(fù)制,在某一時(shí)刻副本之間是不一致的,如果這個(gè)緩存代表的是開關(guān),而且分布式系統(tǒng)中的請(qǐng)求有可能會(huì)重復(fù),就會(huì)導(dǎo)致重復(fù)的請(qǐng)求走到兩個(gè)節(jié)點(diǎn),一個(gè)節(jié)點(diǎn)的開關(guān)是開,一個(gè)節(jié)點(diǎn)的開關(guān)是關(guān),如果請(qǐng)求處理沒有做到冪等,就會(huì)造成處理重復(fù),在嚴(yán)重情況下會(huì)造成資金損失。
優(yōu)秀實(shí)踐13
在寫緩存時(shí)一定要寫入完全正確的數(shù)據(jù),如果緩存數(shù)據(jù)的一部分有效、一部分無(wú)效,則寧可放棄緩存,也不要把部分?jǐn)?shù)據(jù)寫入緩存,否則會(huì)造成空指針、程序異常等。
優(yōu)秀實(shí)踐14
在通常情況下,讀的順序是先緩存,后數(shù)據(jù)庫(kù);寫的順序是先數(shù)據(jù)庫(kù),后緩存。
優(yōu)秀實(shí)踐15
在使用本地緩存(如Ehcache)時(shí),一定要嚴(yán)格控制緩存對(duì)象的個(gè)數(shù)及聲明周期。由于JVM的特性,過多的緩存對(duì)象會(huì)極大影響JVM的性能,甚至導(dǎo)致內(nèi)存溢出等。
優(yōu)秀實(shí)踐16
在使用緩存時(shí),一定要有降級(jí)處理,尤其是對(duì)關(guān)鍵的業(yè)務(wù)環(huán)節(jié),緩存有問題或者失效時(shí)也要能回源到數(shù)據(jù)庫(kù)進(jìn)行處理。
三、關(guān)于常見的緩存問題的線上案例
筆者在多家互聯(lián)網(wǎng)公司負(fù)責(zé)架構(gòu)方案評(píng)審和線上事故復(fù)盤,這里列舉其中的一些典型案例,供大家參考和借鑒。
案例1
現(xiàn)象:某應(yīng)用程序的數(shù)據(jù)庫(kù)負(fù)載瞬時(shí)升高。
原因:在應(yīng)用程序中對(duì)使用的大量緩存key設(shè)置了同一個(gè)固定的失效時(shí)間,當(dāng)緩存失效時(shí),會(huì)造成在一段時(shí)間內(nèi)同時(shí)訪問數(shù)據(jù)庫(kù),造成數(shù)據(jù)庫(kù)的壓力較大。
總結(jié):在使用緩存時(shí)需要進(jìn)行緩存設(shè)計(jì),要充分考慮如何避免常見的緩存穿透、緩存雪崩、緩存并發(fā)等問題,尤其是對(duì)于高并發(fā)的緩存使用,需要對(duì)key的過期時(shí)間進(jìn)行隨機(jī)設(shè)置,例如,將過期時(shí)間設(shè)置為10秒+random(2),也就是將過期時(shí)間隨機(jī)設(shè)置成10~12秒。
案例2
現(xiàn)象:導(dǎo)致遷移前后兩個(gè)系統(tǒng)的核心操作重復(fù)。
原因:在遷移的過程中,重復(fù)的流量進(jìn)入了不同的節(jié)點(diǎn),由于使用了本地緩存存儲(chǔ)遷移開關(guān),而遷移開關(guān)在開關(guān)打開的瞬間導(dǎo)致各個(gè)節(jié)點(diǎn)的開關(guān)狀態(tài)不一致,有的是開、有的是關(guān),所以對(duì)于不同節(jié)點(diǎn)的流量的處理重復(fù),一個(gè)走了開關(guān)開的邏輯,一個(gè)走了開關(guān)關(guān)的邏輯。
總結(jié):避免使用本地緩存來(lái)存儲(chǔ)遷移開關(guān),遷移開關(guān)應(yīng)該在有狀態(tài)的訂單上標(biāo)記。
案例3
現(xiàn)象:某模塊設(shè)計(jì)使用了緩存加速數(shù)據(jù)庫(kù)的讀操作的性能,但發(fā)現(xiàn)數(shù)據(jù)庫(kù)負(fù)載并沒有明顯下降。
原因:由于這個(gè)模塊的使用方查詢請(qǐng)求的數(shù)據(jù)在數(shù)據(jù)庫(kù)中不存在,是非法的數(shù)據(jù),所以導(dǎo)致緩存沒有命中,每次都穿透到數(shù)據(jù)庫(kù),且量級(jí)較大。
總結(jié):在使用緩存時(shí)需要進(jìn)行緩存設(shè)計(jì),要充分考慮如何避免常見的緩存穿透、緩存雪崩、緩存并發(fā)等問題,尤其是對(duì)高并發(fā)的緩存使用,需要對(duì)無(wú)效的key進(jìn)行緩存,以抵擋惡意的或者無(wú)意的對(duì)無(wú)效緩存查詢的攻擊或影響。
案例4
現(xiàn)象:監(jiān)控系統(tǒng)報(bào)警,Redis中單個(gè)哈希鍵占用的空間巨大。
原因:應(yīng)用系統(tǒng)使用了哈希鍵,哈希鍵本身有過期時(shí)間,但是哈希鍵里面的每個(gè)鍵值對(duì)沒有過期時(shí)間。
總結(jié):在設(shè)計(jì)Redis的過程中,如果有大量的鍵值對(duì)要保存,則請(qǐng)使用字符串鍵的數(shù)據(jù)庫(kù)類型,并對(duì)每個(gè)鍵都設(shè)置過期時(shí)間,請(qǐng)不要在哈希鍵內(nèi)部存儲(chǔ)一個(gè)沒有邊界的集合數(shù)據(jù)。實(shí)際上,無(wú)論是對(duì)緩存、內(nèi)存還是對(duì)數(shù)據(jù)庫(kù)的設(shè)計(jì),如果使用任意一個(gè)集合的數(shù)據(jù)結(jié)構(gòu),則都要考慮為它設(shè)置最大限制,避免內(nèi)存用光,最常見的是集合溢出導(dǎo)致的內(nèi)存溢出的問題。
案例5
現(xiàn)象:某業(yè)務(wù)項(xiàng)目由于緩存宕機(jī)導(dǎo)致業(yè)務(wù)邏輯中斷,數(shù)據(jù)不一致。
原因:Redis進(jìn)行主備切換,導(dǎo)致瞬間內(nèi)應(yīng)用連接Redis異常,應(yīng)用并沒有對(duì)緩存做降級(jí)處理。
總結(jié):對(duì)于核心業(yè)務(wù),在使用緩存時(shí)一定要有降級(jí)方案。常見的降級(jí)方案是在數(shù)據(jù)庫(kù)層次預(yù)留足夠的容量,在某一部分緩存出現(xiàn)問題時(shí),可以讓應(yīng)用暫時(shí)回源到數(shù)據(jù)庫(kù)繼續(xù)業(yè)務(wù)邏輯,而不應(yīng)該中斷業(yè)務(wù)邏輯,但是這需要嚴(yán)格的容量評(píng)估,請(qǐng)參考《分布式服務(wù)架構(gòu):原理設(shè)計(jì)與實(shí)戰(zhàn)》第3章的內(nèi)容。
案例6
現(xiàn)象:某應(yīng)用系統(tǒng)負(fù)載升高,響應(yīng)變慢,發(fā)現(xiàn)應(yīng)用進(jìn)行頻繁GC,甚至出現(xiàn)OutOfMemroyError: GC overhead limt exceed的錯(cuò)誤日志。
原因:
因?yàn)檫@個(gè)項(xiàng)目是個(gè)歷史項(xiàng)目,使用了Hibernate ORM框架,在Hibernate中開啟了二級(jí)緩存,使用了Ehcache;但是在Ehcache中沒有控制緩存對(duì)象的個(gè)數(shù),緩存對(duì)象增多,導(dǎo)致內(nèi)存緊張,所以進(jìn)行了頻繁的GC操作。
總結(jié):
使用本地緩存(如Ehcache、OSCache、應(yīng)用內(nèi)存)時(shí),一定要嚴(yán)格控制緩存對(duì)象的個(gè)數(shù)及聲明周期。
案例7
現(xiàn)象:某個(gè)正常運(yùn)行的應(yīng)用突然報(bào)警線程數(shù)過高,之后很快就出現(xiàn)了內(nèi)存溢出。
原因:由于緩存連接數(shù)達(dá)到最大限制,應(yīng)用無(wú)法連接緩存,并且超時(shí)時(shí)間設(shè)置得較大,導(dǎo)致訪問緩存的服務(wù)都在等待緩存操作返回,由于緩存負(fù)載較高,處理不完所有的請(qǐng)求,但是這些服務(wù)都在等待緩存操作返回,服務(wù)這時(shí)在等待,并沒有超時(shí),就不能降級(jí)并繼續(xù)訪問數(shù)據(jù)庫(kù)。這在BIO模式下線程池就會(huì)撐滿,使用方的線程池也都撐滿;在NIO模式下一樣會(huì)使服務(wù)的負(fù)載增加,服務(wù)響應(yīng)變慢,甚至使服務(wù)被壓垮。
總結(jié):在使用遠(yuǎn)程緩存(如Redis、Memcached)時(shí),一定要對(duì)操作超時(shí)時(shí)間進(jìn)行設(shè)置,這是非常關(guān)鍵的,一般我們?cè)O(shè)計(jì)緩存作為加速數(shù)據(jù)庫(kù)讀取的手段,也會(huì)對(duì)緩存操作做降級(jí)處理,因此推薦使用更短的緩存超時(shí)時(shí)間,如果一定要給出一個(gè)數(shù)字,則希望是100毫秒以內(nèi)。
案例8
現(xiàn)象:某項(xiàng)目使用緩存存儲(chǔ)業(yè)務(wù)數(shù)據(jù),上線后出現(xiàn)錯(cuò)誤問題,開發(fā)人員束手無(wú)策。
原因:開發(fā)人員不知道如何發(fā)現(xiàn)、排查、定位和解決緩存問題。
總結(jié):在設(shè)計(jì)緩存時(shí)要有降級(jí)方案,在遇到問題時(shí)首先使用降級(jí)方法,還要設(shè)計(jì)完善的監(jiān)控和報(bào)警功能,幫助開發(fā)人員快速發(fā)現(xiàn)緩存問題,進(jìn)而來(lái)定位和解決問題。
案例9
現(xiàn)象:某項(xiàng)目在使用緩存后,開發(fā)測(cè)試通過,到生產(chǎn)環(huán)境后,服務(wù)卻出現(xiàn)了不可預(yù)知的問題。
原因:該應(yīng)用的緩存key與其他應(yīng)用緩存 key沖突,導(dǎo)致互相覆蓋,出現(xiàn)邏輯錯(cuò)誤。
總結(jié):在使用緩存時(shí)一定要有隔離的設(shè)計(jì),可以通過不同的緩存實(shí)例來(lái)做物理隔離,也可以通過各個(gè)應(yīng)用的緩存key使用不同的前綴進(jìn)行邏輯隔離。