為什么刪除數(shù)據(jù)后,Redis 內(nèi)存占用依然很高?
前言
上周剛來(lái)了個(gè)應(yīng)屆小師弟,組長(zhǎng)說(shuō)讓我?guī)е?,周二?wèn)了我這樣一個(gè)問(wèn)題:師兄啊,我用top命令看了下服務(wù)器的內(nèi)存占用情況,發(fā)現(xiàn)Redis內(nèi)存占用嚴(yán)重,于是我就刪除了大部分不用的keys,為什么內(nèi)存占用還是很嚴(yán)重,并沒(méi)有釋放呢?
嗯?為什么呢?今天就帶著這個(gè)問(wèn)題來(lái)介紹一下如何正確釋放Redis的內(nèi)存。
什么是內(nèi)存碎片?
內(nèi)存碎片這個(gè)概念應(yīng)該不是第一聽說(shuō)了,熟悉JVM或者操作系統(tǒng)的應(yīng)該都熟悉,以火車賣票為例,一個(gè)車廂128個(gè)車位,由于高峰期,只剩余兩個(gè)位置了,但是此時(shí)三個(gè)人想要坐在一起,能夠吹吹牛批,喝喝酒的,那么這三個(gè)人肯定不會(huì)買這節(jié)車廂的兩個(gè)位置了,此時(shí)這兩個(gè)位置可以稱之為座位碎片 。
操作系統(tǒng)中對(duì)于內(nèi)存分配也是一樣的,比如應(yīng)用需要申請(qǐng)一塊連續(xù)N個(gè)字節(jié)的空間,雖然剩余內(nèi)存總量大于N個(gè)字節(jié),但是沒(méi)有一塊連續(xù)的內(nèi)存空間是N個(gè)字節(jié),那么剩余的空間就是內(nèi)存碎片。如下圖:
上圖中的空閑3個(gè)字節(jié)和空閑2個(gè)字節(jié)都是內(nèi)存碎片。
那么什么原因會(huì)造成內(nèi)存碎片呢?這個(gè)其實(shí)大致分為兩個(gè)原因,一個(gè)是操作系統(tǒng)的內(nèi)存分配策略,一個(gè)是Redis自身原因,下面就這兩個(gè)原因詳細(xì)分析。
內(nèi)存分配器的分配策略
內(nèi)存分配器的分配策略一般是按照固定大小來(lái)分配內(nèi)存,而不是按照應(yīng)用程序申請(qǐng)的內(nèi)存空間按需分配。比如8字節(jié)、16字節(jié)、32字節(jié)......
Redis提供了多種的內(nèi)存分配策略,比如libc、jemalloc、tcmalloc,默認(rèn)使用jemalloc。
jemalloc這種分配策略,是按照固定的空間分配,比如8字節(jié)、32字節(jié)....2KB、4KB等。當(dāng)應(yīng)用程序申請(qǐng)的內(nèi)存接近某個(gè)固定值的時(shí)候,jemalloc則會(huì)分配固定的大小。比如申請(qǐng)了6字節(jié),則會(huì)分配8字節(jié)的空間。
這種分配的方式的好處很明顯,則會(huì)減少內(nèi)存分配的次數(shù),比如申請(qǐng)了20字節(jié)的內(nèi)存,實(shí)際分配的是32字節(jié)的內(nèi)存空間,當(dāng)應(yīng)用再寫入10字節(jié)的數(shù)據(jù)時(shí),則不會(huì)再次分配,剩余的12字節(jié)足夠用了。這樣就避免了一次的內(nèi)存分配。如下圖:
但是壞處也很明顯,申請(qǐng)的和分配的空間不一樣,則剩余的空間很可能形成內(nèi)存碎片,一旦內(nèi)存碎片多了,內(nèi)存利用率也會(huì)隨之降低,這是很可怕的。
Redis自身的原因
Redis作為鍵值對(duì)存儲(chǔ)的數(shù)據(jù)庫(kù),本身鍵值對(duì)的大小就是不確定的,正如上面的例子中,Redis申請(qǐng)了20字節(jié)的空間,但實(shí)際分配卻是32字節(jié),那么剩余的12字節(jié)則會(huì)被閑置成為內(nèi)存碎片。如下圖:
上圖中剩余12個(gè)字節(jié)空間則是閑置的,很有可能成為內(nèi)存碎片,因此鍵值對(duì)大小不同則會(huì)造成一定的內(nèi)存碎片,這是第一個(gè)原因。
第二個(gè)原因其實(shí)理解起來(lái)很簡(jiǎn)單,鍵值對(duì)的修改或者刪除肯定會(huì)造成空間的擴(kuò)容或者釋放;
一方面,如果修改后的鍵值對(duì)變大或者變小了,勢(shì)必會(huì)將占用的空間擴(kuò)大或者釋放不用的空間,如下圖:
上圖中鍵值對(duì)修改后變小了,從原來(lái)的10個(gè)字節(jié)變成了7個(gè)字節(jié),從而釋放了3個(gè)字節(jié),此時(shí)剩余了5個(gè)字節(jié)的空閑空間。
另一方面,如果鍵值對(duì)刪除了,則會(huì)釋放掉占用的空間,形成空閑空間。
如何判斷存在內(nèi)存碎片?
這個(gè)對(duì)于運(yùn)維人員來(lái)說(shuō)很重要,一旦出現(xiàn)Redis運(yùn)行緩慢或者阻塞了,一定需要先判斷內(nèi)存的占用情況,而不是說(shuō)胡亂的重啟Redis。
Redis自身提供了INFO命令,可以用來(lái)查詢內(nèi)存的使用情況,命令如下:
INFO memory
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_memory_rss_human:1.86G
…
mem_fragmentation_ratio:1.86
上面的各種屬性含義如下:
mem_fragmentation_ratio這個(gè)指標(biāo)很清楚的展示了當(dāng)前內(nèi)存的碎片率,比如Redis申請(qǐng)了1000字節(jié),但是操作系統(tǒng)實(shí)際分配的內(nèi)存1800個(gè)字節(jié),則mem_fragmentation_ratio=1800/1000=1.8。
從上文也知道了,由于內(nèi)存分配器的局限性,實(shí)際分配的內(nèi)存絕大部分都是大于實(shí)際申請(qǐng)的內(nèi)存,則如何通過(guò)mem_fragmentation_ratio這個(gè)值來(lái)衡量呢?這個(gè)值的范圍在多少是正常的呢?
作者這里參照了許多開發(fā)人員的建議,列出了以下經(jīng)驗(yàn)閥值:
- >1&&<1.5:在這個(gè)范圍內(nèi)是合理的,畢竟大部分情況下操作系統(tǒng)分配的內(nèi)存總是總是大于實(shí)際申請(qǐng)的空間。
- >1.5:這表明內(nèi)存碎片率已經(jīng)超過(guò)50%,此時(shí)需要采取一些措施來(lái)降低碎片率了。
- <1:what?表明實(shí)際分配的內(nèi)存小于申請(qǐng)的內(nèi)存了,很顯然內(nèi)存不足了,這樣會(huì)導(dǎo)致部分?jǐn)?shù)據(jù)寫入到Swap中,之后Redis訪問(wèn)Swap中的數(shù)據(jù)時(shí),延遲會(huì)變大,性能會(huì)降低。
如何清理內(nèi)存碎片?
既然存在內(nèi)存碎片,那么的一定有方法清除內(nèi)存碎片,最簡(jiǎn)單的方法則是重啟Redis
但是這也存在一些風(fēng)險(xiǎn),如下;
- 如果Redis未持久化,則數(shù)據(jù)會(huì)丟失(忽略從后端恢復(fù))
- 即使持久化了,但是恢復(fù)數(shù)據(jù)時(shí)長(zhǎng)不定,這個(gè)要根據(jù)AOF和RDB文件大小決定,在恢復(fù)階段則無(wú)法提供服務(wù)。
好在Redis 4.0-RC3版本之后,Redis自身提供了一種清除內(nèi)存碎片的方法。
清除的原理很簡(jiǎn)單,通過(guò)復(fù)制拷貝將不連續(xù)的存放的數(shù)據(jù)搬到一起形成一塊連續(xù)的內(nèi)存空間,如下圖:
如上圖,清除之前A和B不是連續(xù)的,中間隔著兩個(gè)字節(jié)空閑1,但是在執(zhí)行清除內(nèi)存碎片操作之后,Redis拷貝了B到空閑1,釋放掉之前B的空間,此時(shí)空閑1和空閑2則變成了連續(xù)的空閑空間了。
那么問(wèn)題來(lái)了,這種方式固然好,但是對(duì)于單線程的Redis來(lái)說(shuō),通過(guò)這種拷貝復(fù)制的方式顯然是一種耗時(shí)的操作,性能大大降低,那么有什么好的方法呢?
Redis提供了參數(shù)配置,可以控制清除內(nèi)存碎片的時(shí)機(jī),命令如下:
config set activedefrag yes
以上命令啟動(dòng)自動(dòng)清理,但是具體什么時(shí)候清理,還要受以下兩個(gè)參數(shù)的影響:
- active-defrag-ignore-bytes 400mb:如果內(nèi)存碎片達(dá)到了400mb,開始清理(自定義)
- active-defrag-threshold-lower 20:內(nèi)存碎片空間占操作系統(tǒng)分配給 Redis 的總空間比例達(dá)到20%時(shí),開始清理(自定義)
以上兩個(gè)參數(shù)只有全部滿足才會(huì)開始清理。
除了以上觸發(fā)清理內(nèi)存碎片的參數(shù),Redis還提供了兩個(gè)參數(shù)來(lái)保證在清理過(guò)程中不影響處理正常的請(qǐng)求,如下:
- active-defrag-cycle-min 25:表示自動(dòng)清理過(guò)程所用 CPU 時(shí)間的比例不低于 25%,保證清理能正常開展
- active-defrag-cycle-max 75:表示自動(dòng)清理過(guò)程所用 CPU 時(shí)間的比例不高于 75%,一旦超過(guò),就停止清理,從而避免在清理時(shí),大量的內(nèi)存拷貝阻塞 Redis,導(dǎo)致響應(yīng)延遲升高。
以上兩個(gè)參數(shù)控制了清理過(guò)程中的CPU時(shí)間占比,保證了正常處理請(qǐng)求不受影響。
總結(jié)
本文以師弟的一個(gè)疑問(wèn)開頭介紹了刪除數(shù)據(jù)導(dǎo)致內(nèi)存占用還是很高的原因是存在內(nèi)存碎片,導(dǎo)致內(nèi)存碎片大致分為兩個(gè)原因,如下:
- 內(nèi)存分配策略局限性,一般都會(huì)分配固定的空間大小,導(dǎo)致實(shí)際分配的內(nèi)存空間大于實(shí)際申請(qǐng)的,從而多出了許多不連續(xù)的空閑內(nèi)存塊。
- 鍵值對(duì)的修改、刪除導(dǎo)致了內(nèi)存的擴(kuò)容或者釋放,導(dǎo)致多余的不連續(xù)的空閑內(nèi)存塊。
- 介紹了如何通過(guò)INFO memory命令查看內(nèi)存的碎片率,通過(guò)mem_fragmentation_ratio的經(jīng)驗(yàn)閥值來(lái)判斷異常。
- 介紹了Redis清理內(nèi)存碎片的方式以自動(dòng)清理的兩個(gè)觸發(fā)條件、保證正常處理請(qǐng)求的兩個(gè)控制CPU時(shí)間的參數(shù)。