在完成事件接入的需求時(shí)蔑担,我們需要記錄上一個(gè)批次拉取的事件扶关,并與當(dāng)前拉取到的事件做出比對(duì)缔逛,從而進(jìn)行差分。我們目前的做法是使用redis來進(jìn)行緩存:將上一個(gè)批次拉取到的事件緩存到一個(gè)list中冰悠。但是當(dāng)事件數(shù)量過多時(shí)堡妒,value的大小會(huì)超過1M的限制,直接拋出異常溉卓。這其實(shí)是Tair出于性能的考慮而做出的限制皮迟,本文將談?wù)勎覀€(gè)人對(duì)于bigKey的理解。
1.什么是BigKey桑寨?
顧名思義伏尼,bigKey指一個(gè)key對(duì)應(yīng)的value占據(jù)的內(nèi)存空間相對(duì)比較大,bigKey通常會(huì)有兩種表現(xiàn)形式:
- 字符串類型的:通常表現(xiàn)為value大于10k的String類型key尉尾。
- 非字符串類型/集合類型:通常表現(xiàn)為存儲(chǔ)了過多元素的List爆阶、Hash、Set沙咏、ZSet類型key辨图。
bigKey一旦產(chǎn)生,將會(huì)對(duì)tair的性能以及穩(wěn)定性造成較大的影響肢藐,下面我將詳細(xì)介紹一下bigKey的危害故河。
2.BigKey有什么危害?
bigKey給tair帶來的危害是多方面的窖壕,性能下降只是其中的一方面忧勿,極端情況下杉女,bigKey甚至?xí)?dǎo)致緩存服務(wù)崩潰瞻讽。下面我將從幾個(gè)角度進(jìn)行分析。
2.1 性能影響
2.1.1 線程阻塞
由于redis采用的是單線程模型熏挎,對(duì)于key的增刪改查都是在主線程中完成速勇。此時(shí),對(duì)于bigKey的操作將會(huì)阻塞主線程坎拐,成為一個(gè)明顯的性能瓶頸烦磁,以對(duì)bigKey的刪除耗時(shí)為例:我們可以看到:
- 當(dāng)集合類型key中的元素?cái)?shù)量從10萬增加到100萬時(shí)养匈,其刪除的耗時(shí)也成倍的增長。
- 當(dāng)集合類型key中單個(gè)元素的大小增加時(shí)都伪,其刪除的耗時(shí)也相應(yīng)的增長呕乎。
另外,在Redis執(zhí)行異步重寫操作時(shí)(bgrewriteaof)陨晶,主線程會(huì)fork出一個(gè)子進(jìn)程來執(zhí)行重寫命令猬仁,這個(gè)子進(jìn)程會(huì)與主線程共享內(nèi)存依痊。當(dāng)主線程收到了新增或者修改一個(gè)key的命令伐坏,主線程會(huì)申請(qǐng)一塊額外的內(nèi)存空間來保存數(shù)據(jù)滤馍。但如果這個(gè)key是一個(gè)bigKey時(shí)采幌,主線程會(huì)去申請(qǐng)一塊更大空間书斜,同樣會(huì)阻塞主線程(與JVM分配內(nèi)存一樣沪停,涉及鎖和同步)唠倦。如果申請(qǐng)不到足夠的空間厂镇,會(huì)導(dǎo)致Swap甚至?xí)蠴OM的風(fēng)險(xiǎn)铃芦,這同樣會(huì)降低Redis的性能和穩(wěn)定性雅镊。
2.1.2 網(wǎng)絡(luò)阻塞
Tair中一個(gè)key最大為1M,我們就以1M舉例刃滓,當(dāng)訪問這個(gè)key的QPS為1000時(shí)漓穿,每秒將會(huì)有1GB左右的流量,對(duì)于帶寬來說將是一個(gè)較大壓力注盈。如果這個(gè)bigKey是一個(gè)熱點(diǎn)key時(shí)晃危,后果將不堪設(shè)想。
2.1.3 數(shù)據(jù)遷移阻塞
如果主從同步的 client-output-buffer-limit 設(shè)置過小老客,并且 master 存在大量bigKey(數(shù)據(jù)量很大)僚饭,主從全量同步時(shí)可能會(huì)導(dǎo)致 buffer 溢出,溢出后主從全量同步就會(huì)失敗胧砰。如果主從集群配置了哨兵鳍鸵,那么哨兵會(huì)讓 slave 繼續(xù)向 master 發(fā)起全量同步請(qǐng)求,然后 buffer 又溢出同步失敗尉间,如此反復(fù)偿乖,會(huì)形成復(fù)制風(fēng)暴,這會(huì)浪費(fèi) master 大量的 CPU哲嘲、內(nèi)存贪薪、帶寬資源,也會(huì)讓 master 產(chǎn)生阻塞的風(fēng)險(xiǎn)眠副。 另外画切,當(dāng)我們使用Redis Cluster時(shí),由于Redis Cluster采用了同步遷移的方式囱怕,bigKey同樣會(huì)阻塞主線程霍弹。這里提一下Codis毫别,Codis在遷移bigKey時(shí),使用了異步遷移 + 指令拆分的方式典格,對(duì)于bigKey (集合類型) 中每個(gè)元素岛宦,用一條指令進(jìn)行遷移,而不是把整個(gè) bigKey 進(jìn)行序列化后再整體傳輸耍缴。這種化整為零的方式恋博,就避免了 bigKey 遷移時(shí),因?yàn)橐蛄谢罅繑?shù)據(jù)而阻塞的問題私恬。
2.2 穩(wěn)定性影響
眾所周知债沮,Redis 是典型的 client-server 架構(gòu),所有的操作命令都需要通過客戶端發(fā)送給服務(wù)器端本鸣。為了避免客戶端和服務(wù)器端的請(qǐng)求發(fā)送和處理速度不匹配疫衩,服務(wù)器為每個(gè)客戶端都分配了輸入緩沖區(qū)和輸出緩沖區(qū)(默認(rèn)大小為1GB),用于緩存客戶端發(fā)送的命令和服務(wù)端返回的數(shù)據(jù)荣德。當(dāng)我們寫入或者讀取大量bigKey的時(shí)候闷煤,很有可能導(dǎo)致輸入/輸出緩沖區(qū)溢出。如果客戶端占用的內(nèi)存總量超過了服務(wù)器設(shè)置的maxmemory時(shí)(默認(rèn)4GB)涮瞻,將會(huì)直接觸發(fā)服務(wù)器的內(nèi)存淘汰策略鲤拿,如果有數(shù)據(jù)被淘汰,再要獲取這些數(shù)據(jù)就需要到后端回源署咽,間接降低了緩存系統(tǒng)的性能近顷。同時(shí),淘汰的如果是bigKey也同樣會(huì)阻塞主線程宁否。另外窒升,在極端情況下,多個(gè)客戶端占用了過多的內(nèi)存將導(dǎo)致OOM慕匠,進(jìn)而使得整個(gè)redis進(jìn)程崩潰饱须。
2.3 數(shù)據(jù)傾斜
使用切片集群的時(shí)候,我們通常會(huì)將不同的key存放在不同的實(shí)例上台谊,如果存在bigKey的話蓉媳,會(huì)導(dǎo)致相應(yīng)實(shí)例的數(shù)據(jù)量增大,內(nèi)存壓力也相應(yīng)增大锅铅。
3.怎樣發(fā)現(xiàn)BigKey酪呻?
常用的做法是通過./redis-cli --bigkeys命令對(duì)整個(gè)redis中的鍵值對(duì)進(jìn)行統(tǒng)計(jì),輸出每種數(shù)據(jù)類型中最大的 bigkey 的信息狠角。一般會(huì)配合-i參數(shù)一起使用,控制掃描間隔号杠,避免長時(shí)間掃描降低 Redis 實(shí)例的性能。另外該命令不要在業(yè)務(wù)高峰期使用丰歌。
./redis-cli --bigkeys
-------- summary -------
Sampled 32 keys in the keyspace!
Total key length in bytes is 184 (avg len 5.75)
//統(tǒng)計(jì)每種數(shù)據(jù)類型中元素個(gè)數(shù)最多的bigkey
Biggest list found 'product1' has 8 items
Biggest hash found 'dtemp' has 5 fields
Biggest string found 'page2' has 28 bytes
Biggest stream found 'mqstream' has 4 entries
Biggest set found 'userid' has 5 members
Biggest zset found 'device:temperature' has 6 members
//統(tǒng)計(jì)每種數(shù)據(jù)類型的總鍵值個(gè)數(shù)姨蟋,占所有鍵值個(gè)數(shù)的比例,以及平均大小
4 lists with 15 items (12.50% of keys, avg size 3.75)
5 hashs with 14 fields (15.62% of keys, avg size 2.80)
10 strings with 68 bytes (31.25% of keys, avg size 6.80)
1 streams with 4 entries (03.12% of keys, avg size 4.00)
7 sets with 19 members (21.88% of keys, avg size 2.71)
5 zsets with 17 members (15.62% of keys, avg size 3.40)
或者我們可以通過debug object key 命令去查看serializedlength屬性立帖,serializedlength表示key對(duì)應(yīng)的value序列化后的字節(jié)數(shù)眼溶,通過觀察serializedlength的大小可以輔助排查bigKey。使用scan + debug object key命令晓勇,我們可以計(jì)算其中每個(gè)key的serializedlength堂飞,進(jìn)而發(fā)現(xiàn)其中的bigKey,并做好相應(yīng)的監(jiān)控和處理绑咱。不過對(duì)于集合類型的bigKey绰筛,debug object key 命令的執(zhí)行效率不高,存在阻塞redis的風(fēng)險(xiǎn)描融。
4.怎樣避免和處理BigKey铝噩?
對(duì)于字符串類型的key,我們通常要在業(yè)務(wù)層面將value的大小控制在10KB左右窿克,如果value確實(shí)很大骏庸,可以考慮采用序列化算法和壓縮算法來處理,推薦常用的幾種序列化算法:Protostuff年叮、Kryo或者Fst具被。以及常用的壓縮算法:zstd、lz4或者谷歌的snappy(需要根據(jù)吞吐量和壓縮比自行取舍)只损。下面附上各種壓縮算法的相關(guān)性能:(來源:Facebook Zstandard 官網(wǎng))
對(duì)于集合類型的key一姿,我們通常要通過控制集合內(nèi)元素?cái)?shù)量來避免bigKey,通常的做法是將一個(gè)大的集合類型的key拆分成若干小集合類型的key來達(dá)到目的跃惫。值得一提的是啸蜜,List、Hash辈挂、Set 和ZSet來說衬横,在集合元素個(gè)數(shù)和元素大小小于一定的閾值時(shí),會(huì)使用內(nèi)存緊湊型的底層數(shù)據(jù)結(jié)構(gòu)進(jìn)行保存终蒂,從而節(jié)省內(nèi)存蜂林,規(guī)則如下:
- List:當(dāng)List對(duì)象保存的所有字符串元素長度都小于list-max-ziplist-value(默認(rèn)64字節(jié)),且List對(duì)象保存的元素?cái)?shù)量小于list-max-ziplist-entries(默認(rèn)512)時(shí),List對(duì)象將采用ziplist編碼以節(jié)省內(nèi)存拇泣。
- Hash:當(dāng)Hash對(duì)象保存的鍵值對(duì)的key和value的字符串長度都小于hash-max-ziplist-value(默認(rèn)64字節(jié))噪叙,且Hash對(duì)象保持的鍵值對(duì)數(shù)量小于hash-max-ziplist-entries(默認(rèn)512)時(shí),Hash對(duì)象將采用ziplist編碼以節(jié)省內(nèi)存霉翔。
- Set:當(dāng)Set對(duì)象保存的所有元素都是整數(shù)值睁蕾,且Set對(duì)象保存的元素?cái)?shù)量不超過set-max-intset-entries(默認(rèn)512)時(shí),Set對(duì)象將采用intset編碼以節(jié)省內(nèi)存。
- ZSet:當(dāng)ZSet對(duì)象保存的元素?cái)?shù)量小于zset-max-ziplist-entries(默認(rèn)128)子眶,且ZSet對(duì)象保存的所有元素的長度小于zset-max-ziplist-value(默認(rèn)64)時(shí)瀑凝,ZSet對(duì)象將采用ziplist編碼以節(jié)省內(nèi)存。
另外臭杰,在讀取bigKey的時(shí)候粤咪,我們盡量不要一次性將全部數(shù)據(jù)讀取出來,而是采用分批的方式進(jìn)行讀取:利用scan命令進(jìn)行漸進(jìn)式遍歷渴杆,將大量數(shù)據(jù)分批多次讀取出來寥枝,減小redis的壓力,避免阻塞的風(fēng)險(xiǎn)磁奖。
同樣的囊拜,在刪除bigKey的時(shí)候我們也可以使用scan命令來進(jìn)行批量刪除。如果你是用的redis是4.0之后的版本比搭,則可以利用unlink命令配合lazy free配置(需要手動(dòng)開啟)來進(jìn)行異步刪除冠跷,避免主線程阻塞。