Redis(八)理解內存

  Redis全部的數據都存在內存中,當前內存雖然愈來愈便宜,但跟廉價的硬盤相比成本仍是比較昂貴,所以如何高效利用Redis內存變得很是重要。redis

  高效利用Redis內存首先須要理解Redis內存消耗在哪裏,如何管理內存,最後才能考慮如何優化內存。算法

  1、內存消耗數據庫

  有些內存消耗是必不可少的,而有些能夠經過參數調整和合理使用來規避內存浪費。json

  內存消耗能夠分爲進程自身消耗和子進程消耗。數組

  1.內存使用統計緩存

root@bigjun:~# redis-cli
127.0.0.1:6379> info memory
# Memory
used_memory:848336  Redis分配器分配的內存總量,也就是內部存儲全部數據內存佔用量
used_memory_human:828.45K  以可讀的形式返回user_memory
used_memory_rss:4304896  從操做系統的角度顯示Redis進程佔用的物理內存空間
used_memory_rss_human:4.11M  以可讀的形式返回used_memory_rss
used_memory_peak:848336 內存使用的最大值,表示used_memory的峯值
used_memory_peak_human:828.45K 以可讀的形式返回used_memory_peak
used_memory_peak_perc:100.01%
used_memory_overhead:836078
used_memory_startup:786448
used_memory_dataset:12258
used_memory_dataset_perc:19.81%
total_system_memory:4112789504   系統總內存大小
total_system_memory_human:3.83G  以可讀的形式返回total_system_memory
used_memory_lua:37888 Lua引擎所消耗的內存大小
used_memory_lua_human:37.00K  以可讀的形式返回used_memory_lua
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:5.07  used_memory_rss/used_memory比值,表示內存碎片率
mem_allocator:jemalloc-4.0.3  Redis使用的內存分配器,默認爲jemalloc
active_defrag_running:0
lazyfree_pending_objects:0

須要重點關注的指標有:used_memory_rss、used_memory以及他們的比值mem_fragmentation_ratio
當mem_fragmentation_ratio > 1時,說明used_memory_rss - used_memory多處的部份內存並無用於數據存儲,而是被內存碎片所消耗,若是二者相差很大,說明碎片率嚴重。
當mem_fragmentation_ratio  < 1 時,這種狀況通常出如今操做系統把Redis內存交換到硬盤致使,出現這種狀況要格外關注,因爲硬盤速度遠遠慢於內存,Redis性能會變得不好,甚至僵死。

  2.內存消耗劃分安全

  Redis進程內消耗主要包括:自身內存+對象內存+緩衝內存+內存碎片,其中Redis空進程自身內存消耗很是少,一般used_memory_rss在3MB左右,used_memory在800KB左右,一個空的Redis進程消耗內存能夠忽略不計。服務器

  

  (1)對象內存數據結構

  對象內存是Redis內存佔用最大的一塊,存儲着用戶全部的數據。Redis全部的數據都採用key-value數據類型,每次建立鍵值對時,至少建立兩個類型對象:key對象和value對象。架構

  對象內存消耗能夠簡單理解爲sizeof(keys)+sizeof(values)。

  鍵對象都是字符串,在使用Redis時很容易忽略鍵對內存消耗的影響,應當避免使用過長的鍵。

  value對象更復雜些,主要包含5種基本數據類型:字符串、列表、哈希、集合、有序集合。

  其餘數據類型都是創建在這5種數據結構之上實現的,如:Bitmaps和HyperLogLog使用字符串實現,GEO使用有序集合實現等。每種value對象類型根據使用規模不一樣,佔用內存不一樣。在使用時必定要合理預估並監控value對象佔用狀況,避免內存溢出。

  (2)緩衝內存

  緩衝內存主要包括:客戶端緩衝、複製積壓緩衝區、AOF緩衝區。

  • 客戶端緩衝指的是全部接入到Redis服務器TCP鏈接的輸入輸出緩衝。輸入緩衝沒法控制,最大空間爲1G,若是超過將斷開鏈接。
  • 複製積壓緩衝區是指Redis提供了一個可重用的固定大小緩衝區用於實現部分複製功能,根據repl-backlog-size參數控制,默認1MB。對於複製積壓緩衝區整個主節點只有一個,全部的從節點共享此緩衝區,所以能夠設置較大的緩衝區空間,如100MB,這部份內存投入是有價值的,能夠有效避免全量複製。
  • AOF緩衝區用於在Redis重寫期間保存最近的寫入命令,AOF緩衝區空間消耗用戶沒法控制,消耗的內存取決於AOF重寫時間和寫入命令量,這部分空間佔用一般很小。

  (3)內存碎片

  Redis默認的內存分配器採用jemalloc,可選的分配器還有:glibc、tcmalloc。

  內存分配器爲了更好地管理和重複利用內存,分配內存策略通常採用固定範圍的內存塊進行分配。

  例如jemalloc在64位系統中將內存空間劃分爲:小、大、巨大三個範圍。每一個範圍內又劃分爲多個小的內存塊單位,以下所示:

  • 小:[8byte],[16byte,32byte,48byte,...,128byte],[192byte,256byte,...,512byte],[768byte,1024byte,...,3840byte]
  • 大:[4KB,8KB,12KB,...,4072KB]
  • 巨大:[4MB,8MB,12MB,...]

  好比當保存5KB對象時jemalloc可能會採用8KB的塊存儲,而剩下的3KB空間變爲了內存碎片不能再分配給其餘對象存儲。內存碎片問題雖然是全部內存服務的通病,可是jemalloc針對碎片化問題專門作了優化,通常不會存在過分碎片化的問題,正常的碎片率(mem_fragmentation_ratio)在1.03左右。

  可是當存儲的數據長短差別較大時,如下場景容易出現高內存碎片問題:

  • 頻繁作更新操做,例如頻繁對已存在的鍵執行append、setrange等更新操做。
  • 大量過時鍵刪除,鍵對象過時刪除後,釋放的空間沒法獲得充分利用,致使碎片率上升。

  出現高內存碎片問題時常見的解決方式以下:

  • 數據對齊:在條件容許的狀況下儘可能作數據對齊,好比數據儘可能採用數字類型或者固定長度字符串等,可是這要視具體的業務而定,有些場景沒法作到。
  • 安全重啓:重啓節點能夠作到內存碎片從新整理,所以能夠利用高可用架構,如Sentinel或Cluster,將碎片率太高的主節點轉換爲從節點,進行安全重啓。

  3.子進程內存消耗

  子進程內存消耗主要指執行AOF/RDB重寫時Redis建立的子進程內存消耗。

  Redis執行fork操做產生的子進程內存佔用量對外表現爲與父進程相同,理論上須要一倍的物理內存來完成重寫操做。但Linux具備寫時複製技術(copy-on-write),父子進程會共享相同的物理內存頁,當父進程處理寫請求時會對須要修改的頁複製出一份副本完成寫操做,而子進程依然讀取fork時整個父進程的內存快照。

  子進程內存消耗總結以下:

  • Redis產生的子進程並不須要消耗1倍的父進程內存,實際消耗根據期間寫入命令量決定,可是依然要預留出一些內存防止溢出。
  • 須要設置sysctl vm.overcommit_memory=1容許內核能夠分配全部的物理內存,防止Redis進程執行fork時因系統剩餘內存不足而失敗。
  • 排查當前系統是否支持並開啓THP,若是開啓建議關閉,防止copy-on-write期間內存過分消耗。

  2、內存管理

  Redis主要經過控制內存上限和回收策略實現內存管理。

  1.設置內存上限

  Redis使用maxmemory參數限制最大可用內存。限制內存的目的主要有:

  • 用於緩存場景,當超出內存上限maxmemory時使用LRU等刪除策略釋放空間。
  • 防止所用內存超過服務器物理內存。

  須要注意,maxmemory限制的是Redis實際使用的內存量,也就是used_memory統計項對應的內存。因爲內存碎片率的存在,實際消耗的內存可能會比maxmemory設置的更大,實際使用時要當心這部份內存溢出。經過設置內存上限能夠很是方便地實現一臺服務器部署多個Redis進程的內存控制。

  好比一臺24GB內存的服務器,爲系統預留4GB內存,預留4GB空閒內存給其餘進程或Redis fork進程,留給Redis16GB內存,這樣能夠部署4個maxmemory=4GB的Redis進程。得益於Redis單線程架構和內存限制機制,即便沒有采用虛擬化,不一樣的Redis進程之間也能夠很好地實現CPU和內存的隔離性。

  2.動態調整內存上限

  Redis的內存上限能夠經過config set maxmemory進行動態修改,即修改最大可用內存。

  例如以前的示例,當發現Redis-2沒有作好內存預估,實際只用了不到2GB內存,而Redis-1實例須要擴容到6GB內存纔夠用,這時能夠分別執行以下命令進行調整:

Redis-1>config set maxmemory 6GB
Redis-2>config set maxmemory 2GB

  經過動態修改maxmemory,能夠實如今當前服務器下動態伸縮Redis內存的目的。

  若是此時Redis-3和Redis-4實例也須要分別擴容到6GB,這時超出系統物理內存限制就不能簡單的經過調整maxmemory來達到擴容的目的,須要採用在線遷移數據或者經過複製切換服務器來達到擴容的目的。

  3.內存回收策略

  Redis的內存回收機制主要體如今如下兩個方面:

  • 刪除到達過時時間的鍵對象。
  • 內存使用達到maxmemory上限時觸發內存溢出控制策略。  

  (1)刪除過時鍵對象

  Redis全部的鍵均可以設置過時屬性,內部保存在過時字典中。因爲進程內保存大量的鍵,維護每一個鍵精準的過時刪除機制會致使消耗大量的CPU,對於單線程的Redis來講成本太高,所以Redis採用惰性刪除和定時任務刪除機制實現過時鍵的內存回收。

  • 惰性刪除:惰性刪除用於當客戶端讀取帶有超時屬性的鍵時,若是已經超過鍵設置的過時時間,會執行刪除操做並返回空,這種策略是出於節省CPU成本考慮,不須要單獨維護TTL鏈表來處理過時鍵的刪除。可是單獨用這種方式存在內存泄露的問題,當過時鍵一直沒有訪問將沒法獲得及時刪除,從而致使內存不能及時釋放。正由於如此,Redis還提供另外一種定時任務刪除機制做爲惰性刪除的補充。
  • 定時任務刪除:Redis內部維護一個定時任務,默認每秒運行10次(經過配置hz控制)。定時任務中刪除過時鍵邏輯採用了自適應算法,根據鍵的過時比例、使用快慢兩種速率模式回收鍵,流程以下:

  

  流程說明:

  • 定時任務在每一個數據庫空間隨機檢查20個鍵,當發現過時時刪除對應的鍵。
  • 若是超過檢查數25%的鍵過時,循環執行回收邏輯直到不足25%或運行超時爲止,慢模式下超時時間爲25毫秒。
  • 若是以前回收鍵邏輯超時,則在Redis觸發內部事件以前再次以快模式運行回收過時鍵任務,快模式下超時時間爲1毫秒且2秒內只能運行1次。
  • 快慢兩種模式內部刪除邏輯相同,只是執行的超時時間不一樣。

  (2)內存溢出控制策略

  當Redis所用內存達到maxmemory上限時會觸發相應的溢出控制策略。具體策略受maxmemory-policy參數控制,Redis支持6種策略,以下所示:

  • noeviction:默認策略,不會刪除任何數據,拒絕全部寫入操做並返回客戶端錯誤信息(error)OOM command not allowed when used memory,此時Redis只響應讀操做。
  • volatile-lru:根據LRU算法刪除設置了超時屬性(expire)的鍵,直到騰出足夠空間爲止。若是沒有可刪除的鍵對象,回退到noeviction策略。
  • allkeys-lru:根據LRU算法刪除鍵,無論數據有沒有設置超時屬性,直到騰出足夠空間爲止。
  • allkeys-random:隨機刪除全部鍵,直到騰出足夠空間爲止。
  • volatile-random:隨機刪除過時鍵,直到騰出足夠空間爲止。
  • volatile-ttl:根據鍵值對象的ttl屬性,刪除最近將要過時數據。若是沒有,回退到noeviction策略。

  3、內存優化

  1.RedisObject對象

  Redis存儲的全部值對象在內部定義爲redisObject結構體,內部結構如圖

  

  Redis存儲的數據都使用redisObject來封裝,包括string、hash、list、set、zset在內的全部數據類型。

  • type字段:表示當前對象使用的數據類型,Redis主要支持5種數據類型:string、hash、list、set、zset。可使用type {key}命令查看對象所屬類型,type命令返回的是值對象類型,鍵都是string類型。
  • encoding字段:表示Redis內部編碼類型,encoding在Redis內部使用,表明當前對象內部採用哪一種數據結構實現。理解Redis內部編碼方式對於優化內存很是重要,同一個對象採用不一樣的編碼實現內存佔用存在明顯差別。
  • lru字段:記錄對象最後一次被訪問的時間,當配置了maxmemory和maxmemory-policy=volatile-lru或者allkeys-lru時,用於輔助LRU算法刪除鍵數據。可使用object idletime{key}命令在不更新lru字段狀況下查看當前鍵的空閒時間。
  • refcount字段:記錄當前對象被引用的次數,用於經過引用次數回收內存,當refcount=0時,能夠安全回收當前對象空間。使用object refcount {key}獲取當前對象引用。當對象爲整數且範圍在[0-9999]時,Redis可使用共享對象的方式來節省內存。
  • *ptr字段:與對象的數據內容相關,若是是整數,直接存儲數據;不然表示指向數據的指針。Redis在3.0以後對值對象是字符串且長度<=39字節的數據,內部編碼爲embstr類型,字符串sds和redisObject一塊兒分配,從而只要一次內存操做便可。

  2.縮減鍵值對象

  下降Redis內存使用最直接的方式就是縮減鍵(key)和值(value)的長度。

  • key長度:如在設計鍵時,在完整描述業務狀況下,鍵值越短越好。如user:{uid}:friends:notify:{fid}能夠簡化爲u:{uid}:fs:nt:{fid}。
  • value長度:值對象縮減比較複雜,常見需求是把業務對象序列化成二進制數組放入Redis。首先應該在業務上精簡業務對象,去掉沒必要要的屬性避免存儲無效數據。其次在序列化工具選擇上,應該選擇更高效的序列化工具來下降字節數組大小。
  • 以Java爲例,內置的序列化方式不管從速度仍是壓縮比都不盡如人意,這時能夠選擇更高效的序列化工具,如:protostuff、kryo等,Java常見序列化工具空間壓縮對比。

  

  值對象除了存儲二進制數據以外,一般還會使用通用格式存儲數據好比:json、xml等做爲字符串存儲在Redis中。這種方式優勢是方便調試和跨語言,可是一樣的數據相比字節數組所需的空間更大,在內存緊張的狀況下,可使用通用壓縮算法壓縮json、xml後再存入Redis,從而下降內存佔用,例如使用GZIP壓縮後的json可下降約60%的空間。

  3.共享對象池 

  共享對象池是指Redis內部維護[0-9999]的整數對象池。建立大量的整數類型redisObject存在內存開銷,每一個redisObject內部結構至少佔16字節,甚至超過了整數自身空間消耗。因此Redis內存維護一個[0-9999]的整數對象池,用於節約內存。除了整數值對象,其餘類型如list、hash、set、zset內部元素也可使用整數對象池。所以開發中在知足需求的前提下,儘可能使用整數對象以節省內存。

  Object refcount命令主要用於調試,可以返回指定key所對應的value被引用的次數,在0-9999之間的整數,都是共享內存的,因此返回值是同一個數:2147483647。

因爲100是0-9999之間的數,因此都會共享內存。
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set test:1 100
OK
127.0.0.1:6379> object refcount test:1
(integer) 2147483647
127.0.0.1:6379> set test:2 100
OK
127.0.0.1:6379> object refcount test:2
(integer) 2147483647

  開啓共享對象池以後,

redis> set foo 100
OK
redis> object refcount foo
(integer) 2
redis> set bar 100
OK
redis> object refcount bar
(integer) 3

  

  4.字符串優化

  字符串對象是Redis內部最經常使用的數據類型。全部的鍵都是字符串類型,值對象數據除了整數以外都使用字符串存儲。

  好比執行命令:lpush cache:type "redis" "memcache" "tair" "levelDB",Redis首先建立"cache:type"鍵字符串,而後建立鏈表對象,鏈表對象內再包含四個字符串對象,排除Redis內部用到的字符串對象以外至少建立5個字符串對象。

   (1)字符串結構

  Redis沒有采用原生C語言的字符串類型而是本身實現了字符串結構,內部簡單動態字符串(simple dynamic string,SDS)。

  

  Redis自身實現的字符串結構有以下特色:

  • O(1)時間複雜度獲取:字符串長度、已用長度、未用長度。
  • 可用於保存字節數組,支持安全的二進制數據存儲。
  • 內部實現空間預分配機制,下降內存再分配次數。
  • 惰性刪除機制,字符串縮減後的空間不釋放,做爲預分配空間保留。

   (2)預分配機制

  字符串之因此採用預分配的方式是防止修改操做須要不斷重分配內存和字節數據拷貝。但一樣也會形成內存的浪費。字符串預分配每次並不都是翻倍擴容,空間預分配規則以下:

  • 第一次建立len屬性等於數據實際大小,free等於0,不作預分配。
  • 修改後若是已有free空間不夠且數據小於1M,每次預分配一倍容量。如原有len=60byte,free=0,再追加60byte,預分配120byte,總佔用空間:60byte+60byte+120byte+1byte。
  • 修改後若是已有free空間不夠且數據大於1MB,每次預分配1MB數據。如原有len=30MB,free=0,當再追加100byte,預分配1MB,總佔用空間:1MB+100byte+1MB+1byte。

  (3)字符串重構

  字符串重構:指不必定把每份數據做爲字符串總體存儲,像json這樣的數據可使用hash結構,使用二級結構存儲也能幫咱們節省內存。同時可使用hmget、hmset命令支持字段的部分讀取修改,而不用每次總體存取。例以下面的json數據:

{
  "vid": "413368768",
  "title": " 搜狐屌絲男士 ",
  "videoAlbumPic":"http://photocdn.sohu.com/60160518/vrsa_ver8400079_ae433_pic26.jpg",
  "pid": "6494271",
  "type": "1024",
  "playlist": "6494271",
  "playTime": "468"
}

  分別使用字符串和hash結構,能夠看到使用內存的差別:

  

  根據測試結構,第一次默認配置下使用hash類型,內存消耗不但沒有下降反而比字符串存儲多出2倍,而調整hash-max-ziplist-value=66以後內存下降爲535.60M。由於json的videoAlbumPic屬性長度是65,而hash-max-ziplist-value默認值是64,Redis採用hashtable編碼方式,反而消耗了大量內存。調整配置後hash類型內部編碼方式變爲ziplist,相比字符串更省內存且支持屬性的部分操做。

  5.編碼優化

  (1)編碼類型

  Redis對外提供了string、list、hash、set、zet等類型,可是Redis內部針對不一樣類型存在編碼的概念,所謂編碼就是具體使用哪一種底層數據結構來實現。編碼不一樣將直接影響數據的內存佔用和讀寫效率。使用object encoding {key}命令獲取編碼類型。

127.0.0.1:6379> set str:1 hello
OK
127.0.0.1:6379> object encoding str:1
"embstr"
127.0.0.1:6379> lpush list:1 1 2 3 
(integer) 3
127.0.0.1:6379> object encoding list:1
"quicklist"

  Redis針對每種數據類型(type)能夠採用至少兩種編碼方式來實現。

  

  (2)控制編碼類型

  編碼類型轉換在Redis寫入數據時自動完成,這個轉換過程是不可逆的,轉換規則只能從小內存編碼向大內存編碼轉換。

Redis舊版本:
redis> lpush list:1 a b c d
(integer) 4 // 存儲 4 個元素
redis> object encoding list:1
"ziplist" // 採用 ziplist 壓縮列表編碼
redis> config set list-max-ziplist-entries 4
OK // 設置列表類型 ziplist 編碼最大容許 4 個元素
redis> lpush list:1 e
(integer) 5 // 寫入第 5 個元素 e
redis> object encoding list:1
"linkedlist" // 編碼類型轉換爲鏈表
redis> rpop list:1
"a" // 彈出元素 a
redis> llen list:1
(integer) 4 // 列表此時有 4 個元素
redis> object encoding list:1
"linkedlist" // 編碼類型依然爲鏈表,未作編碼回退

  Redis之因此不支持編碼回退,主要是數據增刪頻繁時,數據向壓縮編碼轉換很是消耗CPU,得不償失。以上示例用到了list-max-ziplist-entries參數,這個參數用來決定列表長度在多少範圍內使用ziplist編碼。固然還有其餘參數控制各類數據類型的編碼。

  

  6.控制鍵的數量

  當使用Redis存儲大量數據時,一般會存在大量鍵,過多的鍵一樣會消耗大量內存。

  Redis本質是一個數據結構服務器,它爲咱們提供多種數據結構,如hash、list、set、zset等。使用Redis時不要進入一個誤區,大量使用get/set這樣的API,把Redis當成Memcached使用。

  對於存儲相同的數據內容利用Redis的數據結構下降外層鍵的數量,也能夠節省大量內存。

  能夠經過在客戶端預估鍵規模,把大量鍵分組映射到多個hash結構中下降鍵的數量,如圖:

  

  hash結構下降鍵數量分析:

  • 根據鍵規模在客戶端經過分組映射到一組hash對象中,如存在100萬個鍵,能夠映射到1000個hash中,每一個hash保存1000個元素。
  • hash的field可用於記錄原始key字符串,方便哈希查找。
  • hash的value保存原始值對象,確保不要超過hash-max-ziplist-value限制。

  

  經過這個測試數據,能夠說明:

  • 一樣的數據使用ziplist編碼的hash類型存儲比string類型節約內存。
  • 節省內存量隨着value空間的減小愈來愈明顯。
  • hash-ziplist類型比string類型寫入耗時,但隨着value空間的減小,耗時逐漸下降。

  使用hash重構後節省內存量效果很是明顯,特別對於存儲小對象的場景,內存只有不到原來的1/5。下面分析這種內存優化技巧的關鍵點:

  • hash類型節省內存的原理是使用ziplist編碼,若是使用hashtable編碼方式反而會增長內存消耗。
  • ziplist長度須要控制在1000之內,不然因爲存取操做時間複雜度在O(n)到O(n2)之間,長列表會致使CPU消耗嚴重,得不償失。
  • ziplist適合存儲小對象,對於大對象不但內存優化效果不明顯還會增長命令操做耗時。
  • 須要預估鍵的規模,從而肯定每一個hash結構須要存儲的元素數量。
  • 根據hash長度和元素大小,調整hash-max-ziplist-entries和hash-max-ziplist-value參數,確保hash類型使用ziplist編碼。

  關於hash鍵和field鍵的設計:

  • 當鍵離散度較高時,能夠按字符串位截取,把後三位做爲哈希的field,以前部分做爲哈希的鍵。如:key=1948480哈希key=group:hash:1948,哈希field=480。
  • 當鍵離散度較低時,可使用哈希算法打散鍵,如:使用crc32(key)&10000函數把全部的鍵映射到「0-9999」整數範圍內,哈希field存儲鍵的原始值。
  • 儘可能減小hash鍵和field的長度,如使用部分鍵內容。

  使用hash結構控制鍵的規模雖然能夠大幅下降內存,但一樣會帶來問題,須要提早作好規避處理。以下所示:

  • 客戶端須要預估鍵的規模並設計hash分組規則,加劇客戶端開發成本。
  • hash重構後全部的鍵沒法再使用超時(expire)和LRU淘汰機制自動刪除,須要手動維護刪除。
  • 對於大對象,如1KB以上的對象,使用hash-ziplist結構控制鍵數量反而得不償失。

  不過瑕不掩瑜,對於大量小對象的存儲場景,很是適合使用ziplist編碼的hash類型控制鍵的規模來下降內存。

相關文章
相關標籤/搜索