Redis所有的數據都存在內存中,相對於廉價的硬盤,內存資源還是比較昂貴的,因此如何高效利用redis內存變得非常重要。
- 內存消耗分析
- 管理內存的原理和方法
- 內存優化技巧
一、內存消耗
理解redis內存,首先要掌握redis內存消耗在哪些方面。有些內存消耗是必不可少的,而有些可以通過參數調整和合理使用來規避內存浪費。
1.1 內存使用統計
首先需要了解redis自身使用內存的統計數據,可通過執行info memory命令來獲取內存相關指標
- used_memory redis分配器分配的內存總量,也就是內部存儲的所有數據內存占用量
- used_memory_human 以可讀的格式返回used_memory
- used_memory_rss 從操作系統的角度顯示redis進程占用的物理內存總量
- used_memory_peak 內存使用的最大值,表示used_memory的峰值
- used_memory_peak_human 以可讀的格式返回used_memory_peak
- used_memory_lua Lua引擎所消耗的內存大小
- mem_fragmentation_ratio used_memory_rss/used_memory比值。表示內存碎片率
- mem_allocator redis 所使用的內存分配器,默認為jemalloc
需要重點關注的指標有:used_memory_rss和used_memory 以及它們的比值mem_fragmentation_ratio。
當mem_fragmentation_ratio>1時,說明used_memory_rss - used_memory 多出的部分內存並沒有用於數據存儲,而是被內存所消耗,如果兩者相差很大,說明碎片率嚴重。
當mem_fragmentation_ratio<1時,這種情況一般出現在操作系統把redis內存交換(swap)到磁盤導致,出現這種情況要格外關注,由於硬盤速度遠遠慢於內存,redis性能會變得很差,甚至僵死。
1.2 內存消耗划分
redis進程內消耗主要包括:
- 自身內存
- 對象內存
- 緩沖內存
- 內存碎片
redis自身內存消耗非常少,通常空進程中used_memory_rss在3MB左右,used_memory在800KB左右,一個空的redis進程消耗的內存可以忽略不計。
1.2.1 對象內存
對象內存是redis內存占用最大的一塊,存儲着用戶所有的數據,對象內存可以簡單理解為sizeof(key)+ sizeof(values)。鍵對象都是字符串,在使用redis時很容易忽略鍵對內存消耗的影響,應當避免使用過長的鍵。value對象更復雜些,主要包含五種基本數據類型:字符串、列表、哈希、集合、有序集合。其它數據類型都是建立在這5種數據結構之上實現的,如:Bitmaps和HyperLogLog是使用字符串實現的,GEO使用有序集合實現等。每種value對象類型根據使用規模不同,占用內存不同。在使用時一定要合理預估並監控value對象占用情況,避免內存溢出。
1.2.2 緩沖內存
緩沖內存主要包括:客戶端緩沖、復制積壓緩沖區、AOF緩沖區。
客戶端緩沖指的是所有接入到redis服務器tcp連接的輸入輸出緩沖。輸入緩沖無法控制,最大空間為1G,如果超過將斷開連接。對於 Redis 的輸出(也就是命令的返回值)來說,其大小經常是不可控的,可能是一個簡單的命令,能夠產生體積龐大的返回數據。另外也有可能因為執行命令太多,產生的返 回數據的速率超過了往客戶端發送的速率,這時也會產生消息堆積,從而造成輸出緩沖區越來越大,占用過多內存,甚至導致系統崩潰。所以 Redis 設置了一些保護機制來避免這種情況的出現,這些機制作用於不同種類的客戶端,有不同的輸出緩沖區大小限制,限制方式有兩種:
- 一種是大小限制,當某一個客戶端的緩沖區超過某一大小時,直接關閉掉這個客戶端連接
- 另一種是當某一個客戶端的緩沖區持續一段時間占用空間過大時,也直接關閉掉客戶端連接
輸出緩沖區通過client-output-buffer-limit控制:
- 對普通客戶端來說,默認配置是client-output-buffer-limit normal 0 0 0,也就是不限制,因為普通客戶端通常采用阻塞式的消息應答模式,如:發送請求,等待返回,再發請求,再等待返回。這種模式通常不會導致輸出緩沖區的堆積膨脹。但是當有大量慢連接客戶端接入時,這部分內存消耗就不能忽略不計了,可以設置maxclients做限制。特別是當使用大量數據輸出的命令且數據無法及時推送給客戶端時,如monitor命令,容易造成redis服務器內存突然飆升。
- 對於 Pub/Sub 客戶端來說,大小限制是32m,當輸出緩沖區超過32m時,會關閉連接。持續性限制是,當客戶端緩沖區大小持續60秒超過8m,也會導致連接關閉。當訂閱服務的消息生產快於消費速度時,輸出緩沖區會產生積壓造成輸出緩沖區空間溢出。
- 而對於 Slave 客戶端來說,大小限制是256m,持續性限制是當客戶端緩沖區大小持續60秒超過64m時,關閉連接。當從節點之間網絡延遲較高或主節點掛在大量從節點時這部分內存消耗占用很大一部分,建議主節點掛在的從節點不要多於2個,主節點不要部署在網絡較差的環境下,如異地機房環境,防止復制客戶端連接緩慢造成溢出。
輸入輸出緩沖區在大流量的場景中容易失控,造成redis內存不穩定,需要重點監控。
復制積壓緩沖區:redis2.8版本之后提供了一個可重用的固定大小緩沖區用於實現部分復制功能,根據repl-backlog-size參數控制,默認是1M。對於復制積壓緩沖區整個主節點只有一個,所有的從節點共享此緩沖區,因此可以設置較大的緩沖區空間,如100M,這部分內存投入是有價值的,可以有效避免全量復制
AOF緩沖區:這部分空間用於在redis重寫期間保存最近寫入命令,此緩沖區空間消耗用戶無法控制,消耗的內存取決於AOF重寫時間和寫入命令量,這部分空間占用通常很小。
1.2.3 內存碎片
redis默認的內存分配器采用jemalloc,可選的分配器還有:glibc、tcmalloc。內存分配器為了更好的管理和重復利用內存,分配內存策略一般采用固定范圍的內存塊進行分配。例如jemalloc在64位系統中將內存空間分為:小、大、巨大三個范圍。每個范圍內又划分為多個小的內存塊單位。比如當保存5KB對象時,jemalloc可能會采用8KB的塊存儲,而剩下的3KB空間變為了內存碎片不能再分配給其他對象存儲。內存碎片雖然是所有內存服務的通病,但是jemalloc針對碎片化問題專門做了優化,一般不會存在過度碎片化的問題,正常的碎片率(mem_fragmentation_tatio)在1.03左右。但是當存儲的數據長短差異較大時,以下場景容易出現高內存碎片問題:
- 頻繁做更新操作,例如頻繁對已存在的鍵執行append、setrange等更新操作。
- 大量過期鍵刪除,鍵對象過期刪除后,釋放的空間無法得到充分利用,導致碎片率上升
出現高內存碎片問題時,常見的解決方式如下:
- 數據對齊:在條件允許的情況下盡量做數據對齊,比如數據盡量采用數字類型或者固定長度字符串等,但是這要視具體的業務而定,有些場景無法做到。
- 安全重啟:重啟節點可以做到內存碎片重新整理,因此可以利用高可用架構,如sentinel或cluster,將碎片率過高的主節點轉換為從節點,進行安全重啟。
1.3 子進程內存消耗
子進程內存消耗主要指執行AOF/RDB重寫時redis創建的子進程內存消耗。redis執行fork操作產生的子進程內存占用量對外表現與父進程相同,理論上需要一倍的物理內存來完成重寫操作。但linux具有寫時復制技術(copy-on-write),父子進程會共享相同的物理內存頁,當父進程處理寫請求時會對需要修改的頁復制出一份副本來完成寫操作,而子進程依然讀取fork時整個父進程的內存快照。
linux kernel在2.6.38內核增加了Transparent Huge Page(THP)機制,而有些linux發行版即使內核達不到2.6.38也會默認加入並開啟這個功能,如Redhat Enterprise Linux在6.0以上版本默認會引入THP。雖然開啟THP可以降低fork子進程的速度,但之后cory-on-write期間復制內存頁的單位從4KB變為2MB,如果父進程有大量寫命令,會加重內存拷貝量,從而造成過度內存消耗。例如,以下兩個執行AOF重寫時的內存消耗日志:
#開啟THP:
C * AOF rewrite: 1039 MB of memory used by copy-on-write
#關閉THP:
C * AOF rewrite: 9 MB of memory used by copy-on-write
這兩個日志出自同一redis進程,used_memory總量為1.5GB,子進程執行期間每秒寫命令量都在200左右。當分別開啟和關閉THP時,子進程的內存消耗有天壤之別。如果在高並發寫的場景下開啟THP,子進程內存消耗可能是父進程的數倍,極易造成機器物理內存溢出,從而觸發SWAP或者OOM,子進程消耗總結如下:
- redis產生的子進程並不需要消耗一倍的父進程內存,實際消耗根據期間寫入命令量決定,但是依然要預留出一些內存防止溢出。
- 需要設置sysctl vm.overcommit_memory=1 允許內核可以分配所有的物理內存,防止redis進程執行fork時因系統剩余內存不足而失敗。
- 排查當前系統是否支持THP,如果開啟建議關閉,防止copy-on-write期間內存過度消耗。