Redis 是一種記憶體資料庫,將資料保存在記憶體中,讀寫效率要比傳統的將資料保存在磁盤上的資料庫要快很多,所以,監控 Redis 的記憶體消耗并了解 Redis 記憶體模型對高效并長期穩定使用 Redis 至關重要,

記憶體使用統計
通過 info memory 命令可以獲得 Redis 記憶體相關的指標,較為重要的指標和解釋如下所示:
| 屬性名 | 屬性說明 |
|---|---|
| used_memory | Redis 分配器分配的記憶體總量,也就是內部存盤的所有資料記憶體占用量 |
| usedmemoryhuman | 以可讀的格式回傳 used_memory |
| usedmemoryrss | 從作業系統的角度顯示 Redis 行程占用的物理記憶體總量 |
| usedmemoryrss_human | usedmemoryrss 的用戶宜讀格式的顯示 |
| usedmemorypeak | 記憶體使用的最大值,表示 used_memory 的峰值 |
| usedmemorypeak_human | 以可讀的格式回傳 usedmemorypeak的值 |
| usedmemorylua | Lua 引擎所消耗的記憶體大小, |
| memfragmentationratio | usedmemoryrss / used_memory 的比值,可以代表記憶體碎片率 |
| maxmemory | Redis 能夠使用的最大記憶體上限,0表示沒有限制,以位元組為單位, |
| maxmemory_policy | Redis 使用的記憶體回收策略,可以是 noeviction、allkeys-lru、volatile-lru、allkeys-random、volatile-random 或者 volatile-ttl,默認是noeviction,也就是不會回收, |

當 memfragmentationratio > 1 時,說明有部分記憶體并沒有用于資料存盤,而是被記憶體碎片所消耗,如果該值很大,說明碎片率嚴重, 當 memfragmentationratio < 1 時,這種情況一般出現在作業系統把 Redis 記憶體交換 (swap) 到硬碟導致,出現這種情況要格外關注,由于硬碟速度遠遠慢于記憶體,Redis 性能會變得很差,甚至僵死,
當 Redis 記憶體超出可以獲得記憶體時,作業系統會進行 swap,將舊的頁寫入硬碟,從硬碟讀寫大概比從記憶體讀寫要慢5個數量級,used_memory 指標可以幫助判斷 Redis 是否有被swap的風隙訓者它已經被swap,
在 Redis Administration 一文 (鏈接在文末) 建議要設定和記憶體一樣大小的交換區,如果沒有交換區,一旦 Redis 突然需要的記憶體大于當前作業系統可用記憶體時,Redis 會因為 out of memory 而被 Linix Kernel 的 OOM Killer 直接殺死,雖然當 Redis 的資料被換出 (swap out) 時,Redis的性能會變差,但是總比直接被殺死的好,
Redis 使用 maxmemory 引數限制最大可用記憶體,限制記憶體的目的主要有:
- 用于快取場景,當超出記憶體上限 maxmemory 時使用 LRU 等洗掉策略釋放空間,
- 防止所用的記憶體超過服務器物理記憶體,導致 OOM 后行程被系統殺死,
maxmemory 限制的是 Redis 實際使用的記憶體量,也就是 used_memory 統計項對應的記憶體,實際消耗的記憶體可能會比 maxmemory 設定的大,要小心因為這部記憶體導致 OOM,所以,如果你有 10GB 的記憶體,最好將 maxmemory 設定為 8 或者 9G
記憶體消耗劃分
Redis 行程內消耗主要包括:自身記憶體 + 物件記憶體 + 緩沖記憶體 + 記憶體碎片,其中 Redis 空行程自身記憶體消耗非常少,通常 usedmemoryrss 在 3MB 左右時,used_memory 一般在 800KB 左右,一個空的 Redis 行程消耗記憶體可以忽略不計,

物件記憶體
物件記憶體是 Redis 記憶體占用最大的一塊,存盤著用戶所有的資料,Redis 所有的資料都采用 key-value 資料型別,每次創建鍵值對時,至少創建兩個型別物件:key 物件和 value 物件,物件記憶體消耗可以簡單理解為這兩個物件的記憶體消耗之和(還有類似過期之類的資訊),鍵物件都是字串,在使用 Redis 時很容易忽略鍵對記憶體消耗的影響,應當避免使用過長的鍵,有關 Redis 物件系統的詳細內容,請看我之前的文章十二張圖帶你了解 Redis 的資料結構和物件系統,
緩沖記憶體
緩沖記憶體主要包括:客戶端緩沖、復制積壓緩沖區和 AOF 緩沖區,
客戶端緩沖指的是所有接入到 Redis 服務器 TCP 連接的輸入輸出緩沖,

輸入緩沖無法控制,最大空間為 1G,如果超過將斷開連接,而且輸入緩沖區不受 maxmemory 控制,假設一個 Redis 實體設定了 maxmemory 為 4G,已經存盤了 2G 資料,但是如果此時輸入緩沖區使用了 3G,就已經超出了 maxmemory 限制,可能導致資料丟失、鍵值淘汰或者 OOM,
輸入緩沖區過大主要是因為 Redis 的處理速度跟不上輸入緩沖區的輸入速度,并且每次進入輸入緩沖區的命令包含了大量的 bigkey,
輸出緩沖通過引數 client-output-buffer-limit 控制,其格式如下所示,
client-output-buffer-limit [hard limit] [soft limit] [duration]
hard limit 是指一旦緩沖區大小達到了這個閾值,Redis 就會立刻關閉該連接,而 soft limit 和時間 duration 共同生效,比如說 soft time 為 64mb、duration 為 60,則只有當緩沖區持續 60s 大于 64mb 時,Redis 才會關閉該連接,
普通客戶端是除了復制和訂閱的客戶端之外的所有連接,Reids 對其的默認配置是 client-output-buffer-limit normal 0 0 0 , Redis 并沒有對普通客戶端的輸出緩沖區做限制,一般普通客戶端的記憶體消耗可以忽略不計,但是當有大量慢連接客戶端接入時這部分記憶體消耗就不能忽略,可以設定 maxclients 做限制,特別當使用大量資料輸出的命令且資料無法及時推送到客戶端時,如 monitor 命令,容易造成 Redis 服務器記憶體突然飆升,相關案例可以查看這篇文章美團在Redis上踩過的一些坑-3.redis記憶體占用飆升,
從客戶端用于主從復制,主節點會為每個從節點單獨建立一條連接用于命令復制,默認配置為 client-output-buffer-limit slave 256mb 64mb 60,當主從節點之間網路延遲較高或主節點掛載大量從節點時這部分記憶體消耗將占用很大一部分,建議主節點掛載的從節點不要多于 2 個,主從節點不要部署在較差的網路環境下,如異地跨機房環境,防止復制客戶端連接緩慢造成溢位,與主從復制相關的一共有兩類緩沖區,一個是從客戶端輸出緩沖區,另外一個是下面會介紹到的復制積壓緩沖區,

訂閱客戶端用于發布訂閱功能,連接客戶端使用單獨的輸出緩沖區,默認配置為 client-output-buffer-limit pubsub 32mb 8mb 60,當訂閱服務的訊息生產快于消費速度時,輸出緩沖區會產生積壓造成記憶體空間溢位,
輸入輸出緩沖區在大流量場景中容易失控,造成 Redis 記憶體不穩定,需要重點監控,可以定期執行 client list 命令,監控每個客戶端的輸入輸出緩沖區大小和其他資訊,
| 屬性名 | 屬性說明 |
|---|---|
| qbuf | 查詢緩沖區的長度(位元組為單位, 0 表示沒有分配查詢緩沖區) |
| qbuf-free | 查詢緩沖區剩余空間的長度(位元組為單位, 0 表示沒有剩余空間) |
| obl | 輸出緩沖區的長度(位元組為單位, 0 表示沒有分配輸出緩沖區) |
| oll | 輸出串列包含的物件數量(當輸出緩沖區沒有剩余空間時,命令回復會以字串物件的形式被入隊到這個佇列里) |
127.0.0.1:6379> client listid=3 addr=127.0.0.1:58161 fd=8 name= \age=1408 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 \qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 \events=r cmd=client
client list 命令執行速度慢,客戶端較多時頻繁執行存在阻塞redis的可能,所以一般可以先使用 info clients 命令獲取最大的客戶端緩沖區大小,
127.0.0.1:6379> info clients# Clientsconnected_clients:1client_recent_max_input_buffer:2client_recent_max_output_buffer:0blocked_clients:0
復制積壓緩沖區是Redis 在 2.8 版本后提供的一個可重用的固定大小緩沖區,用于實作部分復制功能,根據 repl-backlog-size 引數控制,默認 1MB,對于復制積壓緩沖區整個主節點只有一個,所有的從節點共享此緩沖區,因此可以設定較大的緩沖區空間,比如說 100MB,可以有效避免全量復制,有關復制積壓緩沖區的詳情可以看我的舊文章 Redis 復制程序詳解,
AOF 重寫緩沖區:這部分空間用于在 Redis AOF 重寫期間保存最近的寫入命令,AOF 重寫緩沖區的大小用戶無法控制,取決于 AOF 重寫時間和寫入命令量,不過一般都很小,有關 AOF 持久化的詳情可以看我的舊文章 Redis AOF 持久化詳解,
Redis 記憶體碎片
Redis 默認的記憶體分配器采用 jemalloc,可選的分配器還有:glibc、tcmalloc,記憶體分配器為了更好地管理和重復利用記憶體,分配記憶體策略一般采用固定范圍的記憶體塊進行分配,具體的分配策略后續會具體講解,但是 Redis 正常碎片率一般在 1.03 左右(為什么是這個值),但是當存盤的資料長度長度差異較大時,以下場景容易出現高記憶體碎片問題:
- 頻繁做更新操作,例如頻繁對已經存在的鍵執行 append、setrange 等更新操作,
- 大量過期鍵洗掉,鍵物件過期洗掉后,釋放的空間無法得到重復利用,導致碎片率上升,
這部分內容我們后續再詳細講解 jemalloc,因為大量的框架都會使用記憶體分配器,比如說 Netty 等,
子行程記憶體消耗
子行程記憶體消耗主要指執行 AOF 重寫 或者進行 RDB 保存時 Redis 創建的子行程記憶體消耗,Redis 執行 fork 操作產生的子行程記憶體占用量表現為與父行程相同,理論上需要一倍的物理記憶體來完成相應的操作,但是 Linux 具有寫時復制技術 (copy-on-write),父子行程會共享相同的物理記憶體頁,當父行程處理寫請求時會對需要修改的頁復制出一份副本完成寫操作,而子行程依然讀取 fork 時整個父行程的記憶體快照,

如上圖所示,fork 時只拷貝 page table,也就是頁表,只有等到某一頁發生修改時,才真正進行頁的復制,
但是 Linux Kernel 在 2.6.38 記憶體增加了 Transparent Huge Pages (THP) 機制,簡單理解,它就是讓頁大小變大,本來一頁為 4KB,開啟 THP 機制后,一頁大小為 2MB,它雖然可以加快 fork 速度( 要拷貝的頁的數量減少 ),但是會導致 copy-on-write 復制記憶體頁的單位從 4KB 增大為 2MB,如果父行程有大量寫命令,會加重記憶體拷貝量,都是修改一個頁的內容,但是頁單位變大了,從而造成過度記憶體消耗,例如,以下兩個執行 AOF 重寫時的記憶體消耗日志:
// 開啟 THPC * AOF rewrite: 1039 MB of memory used by copy-on-write// 關閉 THPC * AOF rewrite: 9MB of memory used by copy-on-write
這兩個日志出自同一個 Redis 行程,used_memory 總量是 1.5GB,子行程執行期間每秒寫命令量都在 200 左右,當分別開啟和關閉 THP 時,子行程記憶體消耗有天壤之別,所以,在高并發寫的場景下開啟 THP,子行程記憶體消耗可能是父行程的數倍,造成機器物理記憶體溢位,
所以說,Redis 產生的子行程并不需要消耗 1 倍的父行程記憶體,實際消耗根據期間寫入命令量決定,所以需要預留一些記憶體防止溢位,并且建議關閉系統的 THP,防止 copy-on-write 期間記憶體過度消耗,不僅是 Redis,部署 MySQL 的機器一般也會關閉 THP,
本人博客,歡迎來玩
微信公眾號原文

參考文章
https://www.datadoghq.com/pdf/Understanding-the-Top-5-Redis-Performance-Metrics.pdf
Redis Administration https://redis.io/topics/admin
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/37952.html
標籤:NoSQL
