Redis的熱key隱患,探測它,干掉他
做一些C端業(yè)務(wù),不可避免的要引入一級緩存來代替數(shù)據(jù)庫的壓力并且減少業(yè)務(wù)響應(yīng)時間,其實(shí)每次引入一個中間件來解決問題的同時,必然會帶來很多新的問題需要注意,比如緩存一致性問題。
那么其實(shí)還會有一些其他問題比如使用Redis作為一級緩存時可能帶來的熱key、大key等問題,本文我們就熱key(hot key)問題來討論,如何合理的解決熱key問題。
背景
熱key是什么問題,如何導(dǎo)致的?
一般來說,我們使用的緩存Redis都是多節(jié)點(diǎn)的集群版,對某個key進(jìn)行讀寫時,會根據(jù)該key的hash計算出對應(yīng)的slot,根據(jù)這個slot就能找到與之對應(yīng)的分片(一個master和多個slave組成的一組redis集群)來存取該K-V。但是在實(shí)際應(yīng)用過程中,對于某些特定業(yè)務(wù)或者一些特定的時段(比如電商業(yè)務(wù)的商品秒殺活動),可能會發(fā)生大量的請求訪問同一個key。
所有的請求(且這類請求讀寫比例非常高)都會落到同一個redis server上,該redis的負(fù)載就會嚴(yán)重加劇,此時整個系統(tǒng)增加新redis實(shí)例也沒有任何用處,因為根據(jù)hash算法,同一個key的請求還是會落到同一臺新機(jī)器上,該機(jī)器依然會成為系統(tǒng)瓶頸2,甚至造成整個集群宕掉,若此熱點(diǎn)key的value 也比較大,也會造成網(wǎng)卡達(dá)到瓶頸,這種問題稱為 “熱key” 問題。
如下圖1、2所示,分別是正常redis cluster集群和使用一層proxy代理的redis 集群key訪問。
如上所說,熱key會給集群中的少部分節(jié)點(diǎn)帶來超高的負(fù)載壓力,如果不正確處理,那么這些節(jié)點(diǎn)宕機(jī)都有可能,從而會影響整個緩存集群的運(yùn)作,因此我們必須及時發(fā)現(xiàn)熱key、解決熱key問題。
1.熱key探測
熱key探測,看到由于redis集群的分散性以及熱點(diǎn)key帶來的一些顯著影響,我們可以通過由粗及細(xì)的思考流程來做熱點(diǎn)key探測的方案。
1.1 集群中每個slot的qps監(jiān)控
熱key最明顯的影響是整個redis集群中的qps并沒有那么大的前提下,流量分布在集群中slot不均的問題,那么我們可以最先想到的就是對于每個slot中的流量做監(jiān)控,上報之后做每個slot的流量對比,就能在熱key出現(xiàn)時發(fā)現(xiàn)影響到的具體slot。雖然這個監(jiān)控最為方便,但是粒度過于粗了,關(guān)注工眾號:碼猿技術(shù)專欄,回復(fù)關(guān)鍵詞:1111 獲取阿里內(nèi)部Java性能調(diào)優(yōu)手冊!僅適用于前期集群監(jiān)控方案,并不適用于精準(zhǔn)探測到熱key的場景。
1.2 proxy的代理機(jī)制作為整個流量入口統(tǒng)計
如果我們使用的是圖2的redis集群proxy代理模式,由于所有的請求都會先到proxy再到具體的slot節(jié)點(diǎn),那么這個熱點(diǎn)key的探測統(tǒng)計就可以放在proxy中做,在proxy中基于時間滑動窗口,對每個key做計數(shù),然后統(tǒng)計出超出對應(yīng)閾值的key。為了防止過多冗余的統(tǒng)計,還可以設(shè)定一些規(guī)則,僅統(tǒng)計對應(yīng)前綴和類型的key。這種方式需要至少有proxy的代理機(jī)制,對于redis架構(gòu)有要求。
1.3 redis基于LFU的熱點(diǎn)key發(fā)現(xiàn)機(jī)制
redis 4.0以上的版本支持了每個節(jié)點(diǎn)上的基于LFU的熱點(diǎn)key發(fā)現(xiàn)機(jī)制,使用redis-cli –hotkeys即可,執(zhí)行redis-cli時加上–hotkeys選項。可以定時在節(jié)點(diǎn)中使用該命令來發(fā)現(xiàn)對應(yīng)熱點(diǎn)key。
如下所示,可以看到redis-cli –hotkeys的執(zhí)行結(jié)果,熱key的統(tǒng)計信息,這個命令的執(zhí)行時間較長,可以設(shè)置定時執(zhí)行來統(tǒng)計。
1.4 基于Redis客戶端做探測
由于redis的命令每次都是從客戶端發(fā)出,基于此我們可以在redis client的一些代碼處進(jìn)行統(tǒng)計計數(shù),每個client做基于時間滑動窗口的統(tǒng)計,超過一定的閾值之后上報至server,然后統(tǒng)一由server下發(fā)至各個client,并且配置對應(yīng)的過期時間。如何使用Redis實(shí)現(xiàn)電商系統(tǒng)的庫存扣減?
這個方式看起來更優(yōu)美,其實(shí)在一些應(yīng)用場景中并不是那么合適,因為在client端這一側(cè)的改造,會給運(yùn)行的進(jìn)程帶來更大的內(nèi)存開銷,更直接的來說,對于Java和goLang這種自動內(nèi)存管理的語言,會更加頻繁的創(chuàng)建對象,從而觸發(fā)gc導(dǎo)致接口響應(yīng)耗時增加的問題,這個反而是不太容易預(yù)料到的事情。
最終可以通過各個公司的基建,做出對應(yīng)的選擇。
2.熱key解決
通過上述幾種方式我們探測到了對應(yīng)熱key或者熱slot,那么我們就要解決對應(yīng)的熱key問題。解決熱key也有好幾種思路可以參考,我們一個一個捋一下。
2.1 對特定key或slot做限流
一種最簡單粗暴的方式,對于特定的slot或者熱key做限流,這個方案明顯對于業(yè)務(wù)來說是有損的,所以建議只用在出現(xiàn)線上問題,需要止損的時候進(jìn)行特定的限流。redis常用命令和數(shù)據(jù)類型
2.2 使用二級(本地)緩存
本地緩存也是一個最常用的解決方案,既然我們的一級緩存扛不住這么大的壓力,就再加一個二級緩存吧。由于每個請求都是由service發(fā)出的,這個二級緩存加在service端是再合適不過了,因此可以在服務(wù)端每次獲取到對應(yīng)熱key時,使用本地緩存存儲一份,等本地緩存過期后再重新請求,降低redis集群壓力。以java為例,guavaCache就是現(xiàn)成的工具。以下示例:
//本地緩存初始化以及構(gòu)造
private static LoadingCache<String, List<Object>> configCache
= CacheBuilder.newBuilder()
.concurrencyLevel(8) //并發(fā)讀寫的級別,建議設(shè)置cpu核數(shù)
.expireAfterWrite(10, TimeUnit.SECONDS) //寫入數(shù)據(jù)后多久過期
.initialCapacity(10) //初始化cache的容器大小
.maximumSize(10)//cache的容器最大
.recordStats()
// build方法中可以指定CacheLoader,在緩存不存在時通過CacheLoader的實(shí)現(xiàn)自動加載緩存
.build(new CacheLoader<String, List<Object>>() {
@Override
public List<Object> load(String hotKey) throws Exception {
}
});
//本地緩存獲取
Object result = configCache.get(key);
本地緩存對于我們的最大的影響就是數(shù)據(jù)不一致的問題,我們設(shè)置多長的緩存過期時間,就會導(dǎo)致最長有多久的線上數(shù)據(jù)不一致問題,這個緩存時間需要衡量自身的集群壓力以及業(yè)務(wù)接受的最大不一致時間。
2.3 拆key
如何既能保證不出現(xiàn)熱key問題,又能盡量的保證數(shù)據(jù)一致性呢?拆key也是一個好的解決方案。
我們在放入緩存時就將對應(yīng)業(yè)務(wù)的緩存key拆分成多個不同的key。如下圖所示,我們首先在更新緩存的一側(cè),將key拆成N份,比如一個key名字叫做"good_100",那我們就可以把它拆成四份,“good_100_copy1”、“good_100_copy2”、“good_100_copy3”、“good_100_copy4”,每次更新和新增時都需要去改動這N個key,這一步就是拆key。
對于service端來講,我們就需要想辦法盡量將自己訪問的流量足夠的均勻,如何給自己即將訪問的熱key上加入后綴。幾種辦法,根據(jù)本機(jī)的ip或mac地址做hash,之后的值與拆key的數(shù)量做取余,最終決定拼接成什么樣的key后綴,從而打到哪臺機(jī)器上;服務(wù)啟動時的一個隨機(jī)數(shù)對拆key的數(shù)量做取余。
2.4 本地緩存的另外一種思路 配置中心
對于熟悉微服務(wù)配置中心的伙伴來講,我們的思路可以向配置中心的一致性轉(zhuǎn)變一下。拿nacos來舉例,它是如何做到分布式的配置一致性的,并且相應(yīng)速度很快?那我們可以將緩存類比配置,這樣去做。
長輪詢+本地化的配置。首先服務(wù)啟動時會初始化全部的配置,然后定時啟動長輪詢?nèi)ゲ樵儺?dāng)前服務(wù)監(jiān)聽的配置有沒有變更,如果有變更,長輪詢的請求便會立刻返回,更新本地配置;如果沒有變更,對于所有的業(yè)務(wù)代碼都是使用本地的內(nèi)存緩存配置。這樣就能保證分布式的緩存配置時效性與一致性。
2.5 其他可以提前做的預(yù)案
上面的每一個方案都相對獨(dú)立的去解決熱key問題,那么如果我們真的在面臨業(yè)務(wù)訴求時,其實(shí)會有很長的時間來考慮整體的方案設(shè)計。一些極端的秒殺場景帶來的熱key問題,如果我們預(yù)算充足,可以直接做服務(wù)的業(yè)務(wù)隔離、redis緩存集群的隔離,避免影響到正常業(yè)務(wù)的同時,也會可以臨時采取更好的容災(zāi)、限流措施。細(xì)說 redis 十種數(shù)據(jù)類型及底層原理
一些整合的方案
目前市面上已經(jīng)有了不少關(guān)于hotKey相對完整的應(yīng)用級解決方案,其中京東在這方面有開源的hotkey工具,原理就是在client端做洞察,然后上報對應(yīng)hotkey,server端檢測到后,將對應(yīng)hotkey下發(fā)到對應(yīng)服務(wù)端做本地緩存,并且這個本地緩存在遠(yuǎn)程對應(yīng)的key更新后,會同步更新,已經(jīng)是目前較為成熟的自動探測熱key、分布式一致性緩存解決方案
總結(jié)
以上就是筆者大概了解或?qū)嵺`過的的如何應(yīng)對熱key的一些方案,從發(fā)現(xiàn)熱key到解決熱key的兩個關(guān)鍵問題的應(yīng)對。每一個方案都有優(yōu)缺點(diǎn),比如會帶來業(yè)務(wù)的不一致性,實(shí)施起來較為困難等等,可以根據(jù)目前自身業(yè)務(wù)的特點(diǎn)、以及目前公司的基建去做對應(yīng)的調(diào)整和改變。
