Redis 是一個開源、高性能的 Key-Value 數據庫,被廣泛應用在服務器各種場景中。Redis 是一種內存數據庫,將數據保存在內存中,讀寫效率要比傳統的將數據保存在磁盤上的數據庫要快很多。所以,監控 Redis 的內存消耗并了解 Redis 內存模型對高效并長期穩定使用 Redis 至關重要。
在介紹之前先說明下,一般生產環境下,對開發同事不會開放直連 redis 集群的權限,一般是提供 daas 平臺,通過可視化命令窗口,輸入 redis 命令,一般只有 read 權限;對于 write 操作,需要提 redis 數據變更單,而對于 redis 內存、大 key、慢命令,一般都會將信息集成及中顯示在監控看板,而不需要開發同事自己去輸入命令;但是基本的相關知識還是要具備的。
reids 內存分析
redis 內存使用情況:info memory
示例:
可以看到,當前節點內存碎片率為 226893824/209522728 ≈ 1.08,使用的內存分配器是 jemalloc。
used_memory_rss 通常情況下是大于 used_memory 的,因為內存碎片的存在。
但是當操作系統把 redis 內存 swap 到硬盤時,memory_fragmentation_ratio 會小于 1。redis 使用硬盤作為內存,因為硬盤的速度,redis 性能會受到極大的影響。
redis 內存使用
redis 的內存使用分布:自身內存,鍵值對象占用、緩沖區內存占用及內存碎片占用。
redis 空進程自身消耗非常的少,可以忽略不計,優化內存可以不考慮此處的因素。
對象內存
對象內存,也即真實存儲的數據所占用的內存。
redis k-v 結構存儲,對象占用可以簡單的理解為 k-size + v-size。
redis 的鍵統一都為字符串類型,值包含多種類型:string、list、hash、set、zset五種基本類型及基于 string 的 Bitmaps 和 HyperLogLog 類型等。
在實際的應用中,一定要做好 kv 的構建形式及內存使用預期,。
緩沖內存
緩沖內存包括三部分:客戶端緩存、復制積壓緩存及?AOF 緩沖區。
客戶端緩存
接入redis服務器的TCP連接輸入輸出緩沖內存占用,TCP 輸入緩沖占用是不受控制的,最大允許空間為 1G。輸出緩沖占用可以通過 client-output-buffer-limit 參數配置。
redis 客戶端主要分為從客戶端、訂閱客戶端和普通客戶端。
從客戶端連接占用
也就是我們所說的 slave,主節點會為每一個從節點建立一條連接用于命令復制,緩沖配置為:client-output-buffer-limit slave 256mb 64mb 60。
主從之間的間絡延遲及掛載的從節點數量是影響內存占用的主要因素。因此在涉及需要異地部署主從時要特別注意,另外,也要避免主節點上掛載過多的從節點(<=2);
訂閱客戶端內存占用
發布訂閱功能連接客戶端使用單獨的緩沖區,默認配置:client-output-buffer-limit pubsub 32mb 8mb 60。
當消費慢于生產時會造成緩沖區積壓,因此需要特別注意消費者角色配比及生產、消費速度的監控。
普通客戶端內存占用
除了上述之外的其它客戶端,如我們通常的應用連接,默認配置:client-output-buffer-limit normal 1000。
可以看到,普通客戶端沒有配置緩沖區限制,通常一般的客戶端內存消耗也可以忽略不計。
但是當 redis 服務器響應較慢時,容易造成大量的慢連接,主要表現為連接數的突增,如果不能及時處理,此時會嚴重影響 redis 服務節點的服務及恢復。
關于此,在實際應用中需要注意幾點:
maxclients 最大連接數配置必不可少。
合理預估單次操作數據量(寫或讀)及網絡時延 ttl。
禁止線上大吞吐量命令操作,如 keys 等。
高并發應用情景下,redis內存使用需要有實時的監控預警機制。
復制積壓緩沖區
v2.8 之后提供的一個可重用的固定大小緩沖區,用以實現向從節點的部分復制功能,避免全量復制。配置單數:repl-backlog-size,默認 1M。單個主節點配置一個復制積壓緩沖區。
AOF緩沖區
AOF重寫期間增量的寫入命令保存,此部分緩存占用大小取決于 AOF 重寫時間及增量。
內存碎片內存占用
固定范圍內存塊兒分配。redis默認使用jemalloc內存分配器,其它包括glibc、tcmalloc。
內存分配器會首先將可管理的內存分配為規定不同大小的內存塊以備不同的數據存儲需求,但是,我們知道實際應用中需要存儲的數據大小不一,規范不一,內存分配器只能選擇最接近數據需求大小的內存塊兒進行分配,這樣就伴隨著“占不滿”空間的碎片浪費。
jemalloc針對內存碎片有相應的優化策略,正常碎片率為mem_fragmentation_ratio在1.03左右。
第二部分我們說過,對string值得頻繁append及range操作會會導致內存碎片問題,另外,第七部分,SDS惰性內存回收也會導致內存碎片,同時過期鍵內存回收也伴隨著所釋放空間的無法充分利用,導致內存碎片率上升的問題。
碎片處理:
應用層面:盡量避免差異化的鍵值使用,做好數據對齊。
redis服務層面:可以通過重啟服務,進行碎片整理。
maxmemory 及 maxmemory-policy
redis 基于以上配置控制 redis 最大可用內存及內存回收。需要注意的是內存回收執行影響redis的性能,避免頻繁的內存回收開銷。
redis 子進程內存消耗
子進程即 redis 執行持久化(RDB/AOF)時 fork 的子任務進程。
關于 linux 系統的寫時復制機制
父子進程會共享相同的物理內存頁,父進程處理寫請求時會對需要修改的頁復制一份副本進行修改,子進程讀取的內存則為fork時的父進程內存快照,因此,子進程的內存消耗由期間的寫操作增量決定。
關于 linux 的透明大頁機制THP(Transparent Huge Page)
THP 機制會降低 fork 子進程的速度:寫時復制內存頁由 4KB 增大至 2M。高并發情境下,寫時復制內存占用消耗影響會很大,因此需要選擇性關閉。
關于linux配置
一般需要配置 linux 系統 vm.overcommit_memory = 1,以允許系統可以分配所有的物理內存。防止fork任務因內存而失敗。
redis 內存管理
redis 的內存管理主要分為兩方面:內存上限控制及內存回收管理。
內存上限:maxmemory
目的:緩存應用內存回收機制觸發 + 防止物理內存用盡(redis 默認無限使用服務器內存) + 服務節點內存隔離(單服務器上部署多個 redis 服務節點)
在進行內存分配及限制時要充分考慮內存碎片占用影響。動態調整,擴展redis服務節點可用內存:config set maxmemory {}
內存回收
回收時機:鍵過期、內存占用達到上限
過期鍵刪除
redis 鍵過期時間保存在內部的過期字典中,redis 采用惰性刪除機制+定時任務刪除機制。
惰性刪除
即讀時刪除,讀取帶有超時屬性的鍵時,如果鍵已過期,則刪除然后返回空值。這種方式存在問題是,觸發時機,加入過期鍵長時間未被讀取,那么它將會一直存在內存中,造成內存泄漏。
定時任務刪除
redis 內部維護了一個定時任務(默認每秒10次,可配置),通過自適應法進行刪除。
刪除邏輯如下:
需要說明的一點是,快慢模式執行的刪除邏輯相同,這是超時時間不同。
內存溢出控制
當內存達到 maxmemory,會觸發內存回收策略,具體策略依據 maxmemory-policy 來執行。
noevication:默認不回收,達到內存上限,則不再接受寫操作,并返回錯誤。
volatile-lru:根據LRU算法刪除設置了過期時間的鍵,如果沒有則不執行回收。
allkeys-lru:根據LRU算法刪除鍵,針對所有鍵。
allkeys-random:隨機刪除鍵。
volatitle-random:隨機刪除設置了過期時間的鍵。
volatilte-ttl:根據鍵ttl,刪除最近過期的鍵,同樣如果沒有設置過期的鍵,則不執行刪除。
動態配置:config set maxmemory-policy {}
在設置了maxmemory情況下,每次的redis操作都會檢查執行內存回收,因此對于線上環境,要確保所這只的 maxmemory > used_memory。
另外,可以通過動態配置 maxmemory 來主動觸發內存回收。
內存回收策略
內存回收觸發有兩種情況,也就是內存使用達到maxmemory上限時候觸發的溢出回收,還有一種是我們設置了過期的對象到期的時候觸發的到期釋放的內存回收。
Redis內存使用達到maxmemory上限時候觸發的溢出回收;Redis 提供了幾種策略 (maxmemory-policy) 來讓用戶自己決定該如何騰出新的空間以繼續提供讀寫服務:
(1)volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰
(2)volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
(3)volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰
(4)allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的key(這個是最常用的)
(5)allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰
(6)no-eviction:禁止驅逐數據,也就是說當內存不足以容納新寫入數據時,新寫入操作會報錯。這個應該沒人使用吧!
4.0版本后增加以下兩種:
(7)volatile-lfu:從已設置過期時間的數據集(server.db[i].expires)中挑選最不經常使用的數據淘汰
(8)allkeys-lfu:當內存不足以容納新寫入數據時,在鍵空間中,移除最不經常使用的key
redis默認的策略就是noeviction策略,如果想要配置的話,需要在配置文件中寫這個配置:
maxmemory-policy?volatile-lru
Redis 的 LRU 算法
LRU是Least Recently Used 近期最少使用算法,很多緩存策略都使用了這種策略進行空間的釋放,在學習操作系統的內存回收的時候也用到了這種機制進行內存的回收,類似的還有LFU(Least Frequently Used)最不經常使用算法,這種算法。
我們在上面的描述中也可以了解到,redis使用的是一種類似LRU的算法進行內存溢出回收的,其算法的代碼:
/*?volatile-lru?and?allkeys-lru?policy?*/ else?if?(server.maxmemory_policy?==?REDIS_MAXMEMORY_ALLKEYS_LRU?|| ?server.maxmemory_policy?==?REDIS_MAXMEMORY_VOLATILE_LRU) { ?struct?evictionPoolEntry?*pool?=?db->eviction_pool; ? ?while(bestkey?==?NULL)?{ ??evictionPoolPopulate(dict,?db->dict,?db->eviction_pool); ??/*?Go?backward?from?best?to?worst?element?to?evict.?*/ ??for?(k?=?REDIS_EVICTION_POOL_SIZE-1;?k?>=?0;?k--)?{ ???if?(pool[k].key?==?NULL)?continue; ???de?=?dictFind(dict,pool[k].key); ? ???/*?Remove?the?entry?from?the?pool.?*/ ???sdsfree(pool[k].key); ???/*?Shift?all?elements?on?its?right?to?left.?*/ ???memmove(pool+k,pool+k+1, ????sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1)); ???/*?Clear?the?element?on?the?right?which?is?empty ????*?since?we?shifted?one?position?to?the?left.??*/ ???pool[REDIS_EVICTION_POOL_SIZE-1].key?=?NULL; ???pool[REDIS_EVICTION_POOL_SIZE-1].idle?=?0; ? ???/*?If?the?key?exists,?is?our?pick.?Otherwise?it?is ????*?a?ghost?and?we?need?to?try?the?next?element.?*/ ???if?(de)?{ ????bestkey?=?dictGetKey(de); ????break; ???}?else?{ ????/*?Ghost...?*/ ????continue; ???} ??} ?} } Redis會基于server.maxmemory_samples配置選取固定數目的key,然后比較它們的lru訪問時間,然后淘汰最近最久沒有訪問的key,maxmemory_samples的值越大,Redis的近似LRU算法就越接近于嚴格LRU算法,但是相應消耗也變高。所以,頻繁的進行這種內存回收是會降低redis性能的,主要是查找回收節點和刪除需要回收節點的開銷。
所以一般我們在配置redis的時候,盡量不要讓它進行這種內存溢出的回收操作,redis是可以配置maxmemory,used_memory指的是redis真實占用的內存,但是由于操作系統還有其他軟件以及內存碎片還有swap區的存在,所以我們實際的內存應該比redis里面設置的maxmemory要大,具體大多少視系統環境和軟件環境來定。maxmemory也要比used_memory大,一般由于碎片的存在需要做1~2個G的富裕。
編輯:黃飛
?
評論
查看更多