天天看點

删除資料後,為什麼記憶體占用率還是很高?

在使用Redis時,我們經常會遇到這樣一個問題:明明做了資料删除,資料量已經不大了,為什麼使用top指令檢視時,還會發現Redis占用了很多記憶體呢?

實際上,這是因為,當資料删除後,Redis釋放的記憶體空間會由記憶體配置設定器管理,并不會立即傳回給作業系統。是以,作業系統仍然會記錄着給Redis配置設定了大量記憶體。

但是,這往往會伴随一個潛在的風險點:Redis釋放的記憶體空間可能并不是連續的,那麼,這些不連續的記憶體空間很有可能處于一種閑置的狀态。這就會導緻一個問題:雖然有空閑空間,Redis卻無法用來儲存資料,不僅會減少Redis能夠實際儲存的資料量,還會降低Redis運作機器的成本回報率。

打個形象的比喻。我們可以把Redis的記憶體空間比作高鐵上的車廂座位數。如果高鐵的車廂座位數很多,但運送的乘客數很少,那麼,高鐵運作一次的效率低,成本高,成本效益就會降低,Redis也是一樣。如果你正好租用了一台16GB記憶體的雲主機運作Redis,但是卻隻儲存了8GB的資料,那麼,你租用這台雲主機的成本回報率也會降低一半,這個結果肯定不是你想要的。

是以,這節課,我就和你聊聊Redis的記憶體空間存儲效率問題,探索一下,為什麼資料已經删除了,但記憶體卻閑置着沒有用,以及相應的解決方案。

什麼是記憶體碎片?

通常情況下,記憶體空間閑置,往往是因為作業系統發生了較為嚴重的記憶體碎片。那麼,什麼是記憶體碎片呢?

為了友善你了解,我還是借助高鐵的車廂座位來進行解釋。假設一個車廂的座位總共有60個,現在已經賣了57張票,你和2個小夥伴要乘坐高鐵出門旅行,剛好需要三張票。不過,你們想要坐在一起,這樣可以在路上聊天。但是,在選座位時,你們卻發現,已經買不到連續的座位了。于是,你們隻好換了一趟車。這樣一來,你們需要改變出行時間,而且這趟車就空置了三個座位。

其實,這趟車的空座位是和你們的人數相比對的,隻是這些空座位是分散的,如下圖所示:

删除資料後,為什麼記憶體占用率還是很高?

我們可以把這些分散的空座位叫作“車廂座位碎片”,知道了這一點,作業系統的記憶體碎片就很容易了解了。雖然作業系統的剩餘記憶體空間總量足夠,但是,應用申請的是一塊連續位址空間的N位元組,但在剩餘的記憶體空間中,沒有大小為N位元組的連續空間了,那麼,這些剩餘空間就是記憶體碎片(比如上圖中的“空閑2位元組”和“空閑1位元組”,就是這樣的碎片)。

那麼,Redis中的記憶體碎片是什麼原因導緻的呢?接下來,我帶你來具體看一看。我們隻有了解了記憶體碎片的成因,才能對症下藥,把Redis占用的記憶體空間充分利用起來,增加存儲的資料量。

記憶體碎片是如何形成的?

其實,記憶體碎片的形成有内因和外因兩個層面的原因。簡單來說,内因是作業系統的記憶體配置設定機制,外因是Redis的負載特征。

内因:記憶體配置設定器的配置設定政策

記憶體配置設定器的配置設定政策就決定了作業系統無法做到“按需配置設定”。這是因為,記憶體配置設定器一般是按固定大小來配置設定記憶體,而不是完全按照應用程式申請的記憶體空間大小給程式配置設定。

Redis可以使用libc、jemalloc、tcmalloc多種記憶體配置設定器來配置設定記憶體,預設使用jemalloc。接下來,我就以jemalloc為例,來具體解釋一下。其他配置設定器也存在類似的問題。

jemalloc的配置設定政策之一,是按照一系列固定的大小劃分記憶體空間,例如8位元組、16位元組、32位元組、48位元組,…, 2KB、4KB、8KB等。當程式申請的記憶體最接近某個固定值時,jemalloc會給它配置設定相應大小的空間。

這樣的配置設定方式本身是為了減少配置設定次數。例如,Redis申請一個20位元組的空間儲存資料,jemalloc就會配置設定32位元組,此時,如果應用還要寫入10位元組的資料,Redis就不用再向作業系統申請空間了,因為剛才配置設定的32位元組已經夠用了,這就避免了一次配置設定操作。

但是,如果Redis每次向配置設定器申請的記憶體空間大小不一樣,這種配置設定方式就會有形成碎片的風險,而這正好來源于Redis的外因了。

外因:鍵值對大小不一樣和删改操作

Redis通常作為共用的緩存系統或鍵值資料庫對外提供服務,是以,不同業務應用的資料都可能儲存在Redis中,這就會帶來不同大小的鍵值對。這樣一來,Redis申請記憶體空間配置設定時,本身就會有大小不一的空間需求。這是第一個外因。

但是咱們剛剛講過,記憶體配置設定器隻能按固定大小配置設定記憶體,是以,配置設定的記憶體空間一般都會比申請的空間大一些,不會完全一緻,這本身就會造成一定的碎片,降低記憶體空間存儲效率。

比如說,應用A儲存6位元組資料,jemalloc按配置設定政策配置設定8位元組。如果應用A不再儲存新資料,那麼,這裡多出來的2位元組空間就是記憶體碎片了,如下圖所示:

删除資料後,為什麼記憶體占用率還是很高?

第二個外因是,這些鍵值對會被修改和删除,這會導緻空間的擴容和釋放。具體來說,一方面,如果修改後的鍵值對變大或變小了,就需要占用額外的空間或者釋放不用的空間。另一方面,删除的鍵值對就不再需要記憶體空間了,此時,就會把空間釋放出來,形成空閑空間。

我畫了下面這張圖來幫助你了解。

删除資料後,為什麼記憶體占用率還是很高?

一開始,應用A、B、C、D分别儲存了3、1、2、4位元組的資料,并占據了相應的記憶體空間。然後,應用D删除了1個位元組,這個1位元組的記憶體空間就空出來了。緊接着,應用A修改了資料,從3位元組變成了4位元組。為了保持A資料的空間連續性,作業系統就需要把B的資料拷貝到别的空間,比如拷貝到D剛剛釋放的空間中。此時,應用C和D也分别删除了2位元組和1位元組的資料,整個記憶體空間上就分别出現了2位元組和1位元組的空閑碎片。如果應用E想要一個3位元組的連續空間,顯然是不能得到滿足的。因為,雖然空間總量夠,但卻是碎片空間,并不是連續的。

好了,到這裡,我們就知道了造成記憶體碎片的内外因素,其中,記憶體配置設定器政策是内因,而Redis的負載屬于外因,包括了大小不一的鍵值對和鍵值對修改删除帶來的記憶體空間變化。

大量記憶體碎片的存在,會造成Redis的記憶體實際使用率變低,接下來,我們就要來解決這個問題了。不過,在解決問題前,我們要先判斷Redis運作過程中是否存在記憶體碎片。

如何判斷是否有記憶體碎片?

Redis是記憶體資料庫,記憶體使用率的高低直接關系到Redis運作效率的高低。為了讓使用者能監控到實時的記憶體使用情況,Redis自身提供了INFO指令,可以用來查詢記憶體使用的詳細資訊,指令如下:

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的名額,它表示的就是Redis目前的記憶體碎片率。那麼,這個碎片率是怎麼計算的呢?其實,就是上面的指令中的兩個名額used_memory_rss和used_memory相除的結果。

mem_fragmentation_ratio = used_memory_rss/ used_memory      

used_memory_rss是作業系統實際配置設定給Redis的實體記憶體空間,裡面就包含了碎片;而used_memory是Redis為了儲存資料實際申請使用的空間。

我簡單舉個例子。例如,Redis申請使用了100位元組(used_memory),作業系統實際配置設定了128位元組(used_memory_rss),此時,mem_fragmentation_ratio就是1.28。

那麼,知道了這個名額,我們該如何使用呢?在這兒,我提供一些經驗門檻值:

  • mem_fragmentation_ratio 大于1但小于1.5。這種情況是合理的。這是因為,剛才我介紹的那些因素是難以避免的。畢竟,内因的記憶體配置設定器是一定要使用的,配置設定政策都是通用的,不會輕易修改;而外因由Redis負載決定,也無法限制。是以,存在記憶體碎片也是正常的。
  • mem_fragmentation_ratio 大于 1.5。這表明記憶體碎片率已經超過了50%。一般情況下,這個時候,我們就需要采取一些措施來降低記憶體碎片率了。

如何清理記憶體碎片?

當Redis發生記憶體碎片後,一個“簡單粗暴”的方法就是重新開機Redis執行個體。當然,這并不是一個“優雅”的方法,畢竟,重新開機Redis會帶來兩個後果:

  • 如果Redis中的資料沒有持久化,那麼,資料就會丢失;
  • 即使Redis資料持久化了,我們還需要通過AOF或RDB進行恢複,恢複時長取決于AOF或RDB的大小,如果隻有一個Redis執行個體,恢複階段無法提供服務。

是以,還有什麼其他好辦法嗎?

幸運的是,從4.0-RC3版本以後,Redis自身提供了一種記憶體碎片自動清理的方法,我們先來看這個方法的基本機制。

記憶體碎片清理,簡單來說,就是“搬家讓位,合并空間”。

我還以剛才的高鐵車廂選座為例,來解釋一下。你和小夥伴不想耽誤時間,是以直接買了座位不在一起的三張票。但是,上車後,你和小夥伴通過和别人調換座位,又坐到了一起。

這麼一說,碎片清理的機制就很容易了解了。當有資料把一塊連續的記憶體空間分割成好幾塊不連續的空間時,作業系統就會把資料拷貝到别處。此時,資料拷貝需要能把這些資料原來占用的空間都空出來,把原本不連續的記憶體空間變成連續的空間。否則,如果資料拷貝後,并沒有形成連續的記憶體空間,這就不能算是清理了。

我畫一張圖來解釋一下。

删除資料後,為什麼記憶體占用率還是很高?

在進行碎片清理前,這段10位元組的空間中分别有1個2位元組和1個1位元組的空閑空間,隻是這兩個空間并不連續。作業系統在清理碎片時,會先把應用D的資料拷貝到2位元組的空閑空間中,并釋放D原先所占的空間。然後,再把B的資料拷貝到D原來的空間中。這樣一來,這段10位元組空間的最後三個位元組就是一塊連續空間了。到這裡,碎片清理結束。

不過,需要注意的是:碎片清理是有代價的,作業系統需要把多份資料拷貝到新位置,把原有空間釋放出來,這會帶來時間開銷。因為Redis是單線程,在資料拷貝時,Redis隻能等着,這就導緻Redis無法及時處理請求,性能就會降低。而且,有的時候,資料拷貝還需要注意順序,就像剛剛說的清理記憶體碎片的例子,作業系統需要先拷貝D,并釋放D的空間後,才能拷貝B。這種對順序性的要求,會進一步增加Redis的等待時間,導緻性能降低。

那麼,有什麼辦法可以盡量緩解這個問題嗎?這就要提到,Redis專門為自動記憶體碎片清理功機制設定的參數了。我們可以通過設定參數,來控制碎片清理的開始和結束時機,以及占用的CPU比例,進而減少碎片清理對Redis本身請求處理的性能影響。

首先,Redis需要啟用自動記憶體碎片清理,可以把activedefrag配置項設定為yes,指令如下:

config set activedefrag yes      

這個指令隻是啟用了自動清理功能,但是,具體什麼時候清理,會受到下面這兩個參數的控制。這兩個參數分别設定了觸發記憶體清理的一個條件,如果同時滿足這兩個條件,就開始清理。在清理的過程中,隻要有一個條件不滿足了,就停止自動清理。

  • active-defrag-ignore-bytes 100mb:表示記憶體碎片的位元組數達到100MB時,開始清理;
  • active-defrag-threshold-lower 10:表示記憶體碎片空間占作業系統配置設定給Redis的總空間比例達到10%時,開始清理。

為了盡可能減少碎片清理對Redis正常請求處理的影響,自動記憶體碎片清理功能在執行時,還會監控清理操作占用的CPU時間,而且還設定了兩個參數,分别用于控制清理操作占用的CPU時間比例的上、下限,既保證清理工作能正常進行,又避免了降低Redis性能。這兩個參數具體如下:

  • active-defrag-cycle-min 25: 表示自動清理過程所用CPU時間的比例不低于25%,保證清理能正常開展;
  • active-defrag-cycle-max 75:表示自動清理過程所用CPU時間的比例不高于75%,一旦超過,就停止清理,進而避免在清理時,大量的記憶體拷貝阻塞Redis,導緻響應延遲升高。

自動記憶體碎片清理機制在控制碎片清理啟停的時機上,既考慮了碎片的空間占比、對Redis記憶體使用效率的影響,還考慮了清理機制本身的CPU時間占比、對Redis性能的影響。而且,清理機制還提供了4個參數,讓我們可以根據實際應用中的資料量需求和性能要求靈活使用,建議你在實踐中好好地把這個機制用起來。

小結

  • info memory指令是一個好工具,可以幫助你檢視碎片率的情況;
  • 碎片率門檻值是一個好經驗,可以幫忙你有效地判斷是否要進行碎片清理了;
  • 記憶體碎片自動清理是一個好方法,可以避免因為碎片導緻Redis的記憶體實際使用率降低,提升成本收益率。

繼續閱讀