Redis的資料被刪除,佔用記憶體咋還那麼大?

語言: CN / TW / HK

通過 CONFIG SET maxmemory 100mb 或者在 redis.conf 配置檔案設定 maxmemory 100mb Redis 記憶體佔用限制。當達到記憶體最大值值,會觸發記憶體淘汰策略刪除資料。

除此之外,當 key 達到過期時間,Redis 會有以下兩種刪除過期資料的策略:

  • 後臺定時任務選取部分資料刪除;
  • 惰性刪除。

具體原理請移步《Redis 的過期資料刪除那些事》

假設 Redis 例項儲存了 5GB 的資料,現在刪除了 2GB 資料,Redis 程序佔用的記憶體一定會降低麼?(也叫做 RSS,程序消耗記憶體頁數)。

答案是:可能依然佔用了大約 5GB 的記憶體,即使 Redis 的資料只佔用了 3GB 左右。

大家一定要設定maxmemory,否則 Redis 會繼續為新寫入的資料分配記憶體,無法分配就會導致應用程式報錯,當然不會導致宕機。

釋放的記憶體去哪了

明明刪除了資料,使用 top 命令檢視,為何還是佔用了那麼多記憶體?

記憶體都去哪了?使用 info memory 命令獲取 Redis 記憶體相關指標,我列舉了幾個重要的資料:

127.0.0.1:6379> info memory
# Memory
used_memory:1132832  // Redis 儲存資料佔用的記憶體量
used_memory_human:1.08M  // 人類可讀形式返回記憶體總量
used_memory_rss:2977792  // 作業系統角度,程序佔用的物理總記憶體
used_memory_rss_human:2.84M // used_memory_rss 可讀性模式展示
used_memory_peak:1183808 // 記憶體使用的最大值,表示 used_memory 的峰值

used_memory_peak_human:1.13M  // 以可讀的格式返回 used_memory_peak的值
used_memory_lua:37888   // Lua 引擎所消耗的記憶體大小。
used_memory_lua_human:37.00K
maxmemory:2147483648    // 能使用的最大記憶體值,位元組為單位。
maxmemory_human:2.00G  // 可讀形式
maxmemory_policy:noeviction // 記憶體淘汰策略

// used_memory_rss / used_memory 的比值,代表記憶體碎片率
mem_fragmentation_ratio:2.79 

Redis 程序記憶體消耗主要由以下部分組成:

  • Redis 自身啟動所佔用的記憶體;
  • 儲存物件資料記憶體;
  • 緩衝區記憶體:主要由 client-output-buffer-limit 客戶端輸出緩衝區、複製積壓緩衝區、AOF 緩衝區。
  • 記憶體碎片。

記憶體佔用

Redis 自身空程序佔用的記憶體很小可以忽略不計,物件記憶體是佔比對打的一塊,裡面儲存著所有的資料。

緩衝區記憶體在大流量場景容易失控,造成 Redis 記憶體不穩定,需要重點關注。

記憶體碎片過大會導致明明有空間可用,但是卻無法儲存資料。

碎片 = used_memory_rss 實際使用的實體記憶體(RSS 值)除以 used_memory 實際儲存資料記憶體。

什麼是記憶體碎片

記憶體碎片會造成明明有記憶體空間空閒,可是卻無法儲存資料。舉個例子,你跟漂亮小姐姐去電影院看電影,肯定想連在一塊坐。

假設現在有 8 個座位,已經賣出了 4 張票,還有 4 張可以買。可是好巧不巧,買票的人很奇葩,分別間隔一個座位買票。

即使還有 4 個座位空閒,可是你卻買不到兩個座位連在一塊的票,厚禮蟹!

記憶體碎片

記憶體碎片形成原因

記憶體碎片是什麼原因導致呢?

主要有兩個原因:

  • 記憶體分配器的分配策略。
  • 鍵值對的大小不一樣和刪改操作:Redis 頻繁做更新操作、大量過期資料刪除,釋放的空間(不夠連續)無法得到複用,導致碎片率上升。

接下來我分別探討實際發生的原因……

記憶體分配器的分配策略

Redis 預設的記憶體分配器採用 jemalloc,可選的分配器還有:glibc、tcmalloc。

記憶體分配器並不能做到按需分配,而是採用固定範圍的記憶體塊進行分配。

例如 8 位元組、16 位元組…..,2 KB,4KB,當申請記憶體最近接某個固定值的時候,jemalloc 會給它分配最接近固定值大小的空間。

這樣就會出現記憶體碎片,比如程式只需要 1.5 KB,記憶體分配器會分配 2KB 空間,那麼這 0.5KB 就是碎片。

這麼做的目的是減少記憶體分配次數,比如申請 22 位元組的空間儲存資料,jemalloc 就會分配 32 位元組,如果後邊還要寫入 10 位元組,就不需要再向作業系統申請空間了,可以使用之前申請的 32 位元組。

刪除 key 的時候,Redis 並不會立馬把記憶體歸還給作業系統,出現這個情況是因為底層記憶體分配器管理導致,比如大多數已經刪除的 key 依然與其他有效的 key分配在同一個記憶體頁中。

另外,分配器為了複用空閒的記憶體塊,原有 5GB 的資料中刪除了 2 GB 後,當再次新增資料到例項中,Redis 的 RSS 會保持穩定,不會增長太多。

因為記憶體分配器基本上覆用了之前刪除釋放出來的 2GB 記憶體。

鍵值對大小不一樣和刪改操作

由於記憶體分配器是按照固定大小分配記憶體,所以通常分配的記憶體空間比實際資料佔用的大小多一些,會造成碎片,降低記憶體的儲存效率。

另外,鍵值對的頻繁修改和刪除,導致記憶體空間的擴容和釋放,比如原本佔用 32 位元組的字串,現在修改為佔用 20 位元組的字串,那麼釋放出的 12 位元組就是空閒空間。

如果下一個資料儲存請求需要申請 13 位元組的字串,那麼剛剛釋放的 12 位元組空間無法使用,導致碎片。

碎片最大的問題:空間總量足夠大,但是這些記憶體不是連續的,可能大致無法儲存資料。

記憶體碎片解決之道

那該如何解決呢?

首先要確定是否發生了記憶體碎片,重點關注前面 INFO memory 命令提示的 mem_fragmentation_ratio 指標,表示記憶體碎片率:

mem_fragmentation_ratio = used_memory_rss/ used_memory

如果 1 < 碎片率 < 1.5,可以認為是合理的,而大於 1.5 說明碎片已經超過 50%,我們需要採取一些手段解決碎片率過大的問題。

重啟大法

最簡單粗暴的方式就是重啟,如果沒有開啟持久化,資料會丟失。

開啟持久化的話,需要使用 RDB 或者 AOF 恢復資料,如果只有一個例項,資料大的話會導致恢復階段長時間無法提供服務,高可用大打折扣。

咋辦呢?碼哥靚仔

自動清理記憶體碎片

既然你都叫我靚仔了,就傾囊相助告訴你終極殺招:Redis 4.0 版本後,自身提供了一種記憶體碎片清理機制。

怎麼清理呢?

很簡單,還是上面的例子,想要買兩張連在一塊的電影票。與與別人溝通調換下位置,就實現了。

對於 Redis 來說,當一塊連續的記憶體空間被劃分為好幾塊不連續的空間的時候,作業系統先把資料以依次挪動拼接在一塊,並釋放原來資料佔據的空間,形成一塊連續空閒記憶體空間。。

如下圖所示:

碎片清理

自動清理記憶體碎片的代價

自動清理雖好,可不要肆意妄為,作業系統把資料移動到新位置,再把原有空間釋放是需要消耗資源的。

Redis 操作資料的指令是單執行緒,所以在資料複製移動的時候,只能等待清理碎片完成才能處理請求,造成效能損耗。

如何避免清理碎片對效能的影響又能實現自動清理呢?

好問題,通過以下兩個引數來控制記憶體碎片清理和結束時機,避免佔用 CPU 過多,減少清理碎片對 Redis 處理請求的效能影響。

開啟自動記憶體碎片清理

CONFIG SET activedefrag yes

這只是開啟自動清理,何時清理要同時滿足以下兩個條件才會觸發清理操作。

清理的條件

active-defrag-ignore-bytes 200mb:記憶體碎片佔用的記憶體達到 200MB,開始清理;

active-defrag-threshold-lower 20:記憶體碎片的空間佔慚怍系統分配給 Redis 空間的 20% ,開始清理。

避免對效能造成影響

清理時間有了,還需要控制清理對效能的影響。由一項兩個設定先分配清理碎片佔用的 CPU 資源,保證既能正常清理碎片,又能避免對 Redis 處理請求的效能影響。

active-defrag-cycle-min 20:自動清理過程中,佔用 CPU 時間的比例不低於 20%,從而保證能正常展開清理任務。

active-defrag-cycle-max 50:自動清理過程佔用的 CPU 時間比例不能高於 75%,超過的話就立刻停止清理,避免對 Redis 的阻塞,造成高延遲。

總結

如果你發現明明 Redis 儲存資料的記憶體佔用遠小於作業系統分配給 Redis 的記憶體,而又無法儲存資料,那可能出現大量記憶體碎片了。

通過 info memory 命令,看下記憶體碎片mem_fragmentation_ratio 指標是否正常。

那麼我們就開啟自動清理併合理設定清理時機和 CPU 資源佔用,該機制涉及到記憶體拷貝,會對 Redis 效能造成潛在風險。

如果遇到 Redis 效能變慢,排查下是否由於清理碎片導致,如果是,那就調小 active-defrag-cycle-max 的值。

碼哥建立了技術群摸魚群,想要與各個城市的程式設計師一起摸魚打屁聊人生學技術就加群吧。

最後,可以我叫我一聲靚仔麼?你有什麼問題想對碼哥說麼?在留言區留言吧,知無不言。

點贊、收藏、分享走起!

參考

[1].Redis 核心技術與實戰

[2].https://juejin.cn/post/6844903967298682893#heading-4

[3].https://redis.io/docs/reference/optimization/memory-optimization/#memory-allocation