Redis 內(nèi)存優(yōu)化在 vivo 的探索與實踐
作者:互聯(lián)網(wǎng)服務(wù)器團(tuán)隊- Tang Wenjian
一、 背景
使用過 Redis 的同學(xué)應(yīng)該都知道,它基于鍵值對(key-value)的內(nèi)存數(shù)據(jù)庫,所有數(shù)據(jù)存放在內(nèi)存中,內(nèi)存在 Redis 中扮演一個核心角色,所有的操作都是圍繞它進(jìn)行。
我們在實際維護(hù)過程中經(jīng)常會被問到如下問題,比如數(shù)據(jù)怎么存儲在 Redis 里面能節(jié)約成本、提升性能?Redis內(nèi)存告警是什么原因?qū)е?
本文主要是通過分析 Redis內(nèi)存結(jié)構(gòu)、介紹內(nèi)存優(yōu)化手段,同時結(jié)合生產(chǎn)案例,幫助大家在優(yōu)化內(nèi)存使用,快速定位 Redis 相關(guān)內(nèi)存異常問題。
二、 Redis 內(nèi)存管理
本章詳細(xì)介紹 Redis 是怎么管理各內(nèi)存結(jié)構(gòu)的,然后主要介紹幾個占用內(nèi)存可能比較多的內(nèi)存結(jié)構(gòu)。首先我們看下Redis 的內(nèi)存模型。
內(nèi)存模型如圖:
- 【used_memory】:Redis內(nèi)存占用中最主要的部分,Redis分配器分配的內(nèi)存總量(單位是KB)(在編譯時指定編譯器,默認(rèn)是jemalloc),主要包含自身內(nèi)存(字典、元數(shù)據(jù))、對象內(nèi)存、緩存,lua內(nèi)存。
- 【自身內(nèi)存】:自身維護(hù)的一些數(shù)據(jù)字典及元數(shù)據(jù),一般占用內(nèi)存很低。
- 【對象內(nèi)存】:所有對象都是Key-Value型,Key對象都是字符串,Value對象則包括5種類(String,List,Hash,Set,Zset),5.0還支持stream類型。
- 【緩存】:客戶端緩沖區(qū)(普通 + 主從復(fù)制 + pubsub)以及aof緩沖區(qū)。
- 【Lua內(nèi)存】:主要是存儲加載的 Lua 腳本,內(nèi)存使用量和加載的 Lua 腳本數(shù)量有關(guān)。
- 【used_memory_rss】:Redis 主進(jìn)程占據(jù)操作系統(tǒng)的內(nèi)存(單位是KB),是從操作系統(tǒng)角度得到的值,如top、ps等命令。
- 【內(nèi)存碎片】:如果對數(shù)據(jù)的更改頻繁,可能導(dǎo)致redis釋放的空間在物理內(nèi)存中并沒有釋放,但redis又無法有效利用,這就形成了內(nèi)存碎片。
- 【運行內(nèi)存】:運行時消耗的內(nèi)存,一般占用內(nèi)存較低,在10M內(nèi)。
- 【子進(jìn)程內(nèi)存】:主要是在持久化的時候,aof rewrite或者rdb產(chǎn)生的子進(jìn)程消耗的內(nèi)存,一般也是比較小。
2.1 對象內(nèi)存
對象內(nèi)存存儲 Redis 所有的key-value型數(shù)據(jù)類型,key對象都是 string 類型,value對象主要有五種數(shù)據(jù)類型String、List、Hash、Set、Zset,不同類型的對象通過對應(yīng)的編碼各種封裝,對外定義為RedisObject結(jié)構(gòu)體,RedisObject都是由字典(Dict)保存的,而字典底層是通過哈希表來實現(xiàn)的。通過哈希表中的節(jié)點保存字典中的鍵值對,結(jié)構(gòu)如下:
(來源:書籍《Redis設(shè)計與實現(xiàn)》)
為了達(dá)到極大的提高 Redis 的靈活性和效率,Redis 根據(jù)不同的使用場景來對一個對象設(shè)置不同的編碼,從而優(yōu)化某一場景下的效率。
各類對象選擇編碼的規(guī)則如下:
string (字符串)
- 【int】:(整數(shù)且數(shù)字長度小于20,直接記錄在ptr*里面)
- 【embstr】: (連續(xù)分配的內(nèi)存(字符串長度小于等于44字節(jié)的字符串))
- 【raw】: 動態(tài)字符串(大于44個字節(jié)的字符串,同時字符長度小于 512M(512M是字符串的大小限制))
list (列表)
- 【ziplist】:(元素個數(shù)小于hash-max-ziplist-entries配置(默認(rèn)512個),同時所有值都小于hash-max-ziplist-value配置(默認(rèn)64個字節(jié)))
- 【linkedlist】:(當(dāng)列表類型無法滿足ziplist的條件時,Redis會使用linkedlist作為列表的內(nèi)部實現(xiàn))
- 【quicklist】:(Redis 3.2 版本引入了 quicklist 作為 list 的底層實現(xiàn),不再使用 linkedlist 和 ziplist 實現(xiàn))
set (集合)
- 【intset 】:(元素都是整數(shù)且元素個數(shù)小于set-max-intset-entries配置(默認(rèn)512個))
- 【hashtable】:(集合類型無法滿足intset的條件時就會使用hashtable)
hash (hash列表)
- 【ziplist】:(元素個數(shù)小于hash-max-ziplist-entries配置(默認(rèn)512個),同時任意一個value的長度都小于hash-max-ziplist-value配置(默認(rèn)64個字節(jié)))
- 【hashtable】:(hash類型無法滿足intset的條件時就會使用hashtable
zset(有序集合)
- 【ziplist】:(元素個數(shù)小于zset-max-ziplist-entries配置(默認(rèn)128個)同時每個元素的value小于zset-max-ziplist-value配置(默認(rèn)64個字節(jié)))
- 【skiplist】:(當(dāng)ziplist條件不滿足時,有序集合會使用skiplist作為內(nèi)部實現(xiàn))
2.2 緩沖內(nèi)存
2.2 1 客戶端緩存
客戶端緩沖指的是所有接入 Redis 服務(wù)的 TCP 連接的輸入輸出緩沖。有普通客戶端緩沖、主從復(fù)制緩沖、訂閱緩沖,這些都由對應(yīng)的參數(shù)緩沖控制大小(輸入緩沖無參數(shù)控制,最大空間為1G),若達(dá)到設(shè)定的最大值,客戶端將斷開。
【client-output-buffer-limit】: 限制客戶端輸出緩存的大小,后面接客戶端種類(normal、slave、pubsub)及限制大小,默認(rèn)是0,不做限制,如果做了限制,達(dá)到閾值之后,會斷開鏈接,釋放內(nèi)存。
【repl-backlog-size】:默認(rèn)是1M,backlog是一個主從復(fù)制的緩沖區(qū),是一個環(huán)形buffer,假設(shè)達(dá)到設(shè)置的閾值,不存在溢出的問題,會循環(huán)覆蓋,比如slave中斷過程中同步數(shù)據(jù)沒有被覆蓋,執(zhí)行增量同步就可以。backlog設(shè)置的越大,slave可以失連的時間就越長,受參數(shù)maxmemory限制,正常不要設(shè)置太大。
2.2 2 AOF 緩沖
當(dāng)我們開啟了 AOF 的時候,先將客戶端傳來的命令存放在AOF緩沖區(qū),再去根據(jù)具體的策略(always、everysec、no)去寫入磁盤中的 AOF 文件中,同時記錄刷盤時間。
AOF 緩沖沒法限制,也不需要限制,因為主線程每次進(jìn)行 AOF會對比上次刷盤成功的時間;如果超過2s,則主線程阻塞直到fsync同步完成,主線程被阻塞的時候,aof_delayed_fsync狀態(tài)變量記錄會增加。因此 AOF 緩存只會存幾秒時間的數(shù)據(jù),消耗內(nèi)存比較小。
2.3 內(nèi)存碎片
程序出現(xiàn)內(nèi)存碎片是個很常見的問題,Redis的默認(rèn)分配器是jemalloc ,它的策略是按照一系列固定的大小劃分內(nèi)存空間,例如 8 字節(jié)、16 字節(jié)、32 字節(jié)、…, 4KB、8KB 等。當(dāng)程序申請的內(nèi)存最接近某個固定值時,jemalloc 會給它分配比它大一點的固定大小的空間,所以會產(chǎn)生一些碎片,另外在刪除數(shù)據(jù)的時候,釋放的內(nèi)存不會立刻返回給操作系統(tǒng),但redis自己又無法有效利用,就形成碎片。
內(nèi)存碎片不會被統(tǒng)計在used_memory中,內(nèi)存碎片比率在redis info里面記錄了一個動態(tài)值mem_fragmentation_ratio,該值是used_memory_rss / used_memory的比值, mem_fragmentation_ratio越接近1,碎片率越低,正常值在1~1.5內(nèi),超過了說明碎片很多。
2.4 子進(jìn)程內(nèi)存
前面提到子進(jìn)程主要是為了生成 RDB 和 AOF rewrite產(chǎn)生的子進(jìn)程,也會占用一定的內(nèi)存,但是在這個過程中寫操作不頻繁的情況下內(nèi)存占用較少,寫操作很頻繁會導(dǎo)致占用內(nèi)存較多。
三、Redis 內(nèi)存優(yōu)化
內(nèi)存優(yōu)化的對象主要是對象內(nèi)存、客戶端緩沖、內(nèi)存碎片、子進(jìn)程內(nèi)存等幾個方面,因為這幾個內(nèi)存消耗比較大或者有的時候不穩(wěn)定,我們優(yōu)化內(nèi)存的方向分為如:減少內(nèi)存使用、提高性能、減少內(nèi)存異常發(fā)生。
3.1 對象內(nèi)存優(yōu)化
對象內(nèi)存的優(yōu)化可以降低內(nèi)存使用率,提高性能,優(yōu)化點主要針對不同對象不同編碼的選擇上做優(yōu)化。
在優(yōu)化前,我們可以了解下如下的一些知識點:
(1)首先是字符串類型的3種編碼,int編碼除了自身object無需分配內(nèi)存,object 的指針不需要指向其他內(nèi)存空間,無論是從性能還是內(nèi)存使用都是最優(yōu)的,embstr是會分配一塊連續(xù)的內(nèi)存空間,但是假設(shè)這個value有任何變化,那么value對象會變成raw編碼,而且是不可逆的。
(2)ziplist 存儲 list 時每個元素會作為一個 entry; 存儲 hash 時 key 和 value 會作為相鄰的兩個 entry; 存儲 zset 時 member 和 score 會作為相鄰的兩個entry,當(dāng)不滿足上述條件時,ziplist 會升級為 linkedlist, hashtable 或 skiplist 編碼。
(3)在任何情況下大內(nèi)存的編碼都不會降級為 ziplist。
(4)linkedlist 、hashtable 便于進(jìn)行增刪改操作但是內(nèi)存占用較大。
(5)ziplist 內(nèi)存占用較少,但是因為每次修改都可能觸發(fā) realloc 和 memcopy, 可能導(dǎo)致連鎖更新(數(shù)據(jù)可能需要挪動)。因此修改操作的效率較低,在 ziplist 的條目很多時這個問題更加突出。
(6)由于目前大部分redis運行的版本都是在3.2以上,所以 List 類型的編碼都是quicklist,它是 ziplist 組成的雙向鏈表linkedlist ,它的每個節(jié)點都是一個ziplist,考慮了綜合平衡空間碎片和讀寫性能兩個維度所以使用了個新編碼quicklist,quicklist有個比較重要的參數(shù)list-max-ziplist-size,當(dāng)它取正數(shù)的時候,正數(shù)表示限制每個節(jié)點ziplist中的entry數(shù)量,如果是負(fù)數(shù)則只能為-1~-5,限制ziplist大小,從-1~-5的限制分別為4kb、8kb、16kb、32kb、64kb,默認(rèn)是-2,也就是限制不超過8kb。
(7)【rehash】: redis存儲底層很多是hashtable,客戶端可以根據(jù)key計算的hash值找到對應(yīng)的對象,但是當(dāng)數(shù)據(jù)量越來越大的時候,可能就會存在多個key計算的hash值相同,這個時候這些相同的hash值就會以鏈表的形式存放,如果這個鏈表過大,那么遍歷的時候性能就會下降,所以Redis定義了一個閾值(負(fù)載因子 loader_factor = 哈希表中鍵值對數(shù)量 / 哈希表長度),會觸發(fā)漸進(jìn)式的rehash,過程是新建一個更大的新hashtable,然后把數(shù)據(jù)逐步移動到新hashtable中。
(8)【bigkey】:bigkey一般指的是value的值占用內(nèi)存空間很大,但是這個大小其實沒有一個固定的標(biāo)準(zhǔn),我們自己定義超過10M就可以稱之為bigkey。
優(yōu)化建議:
(1)key盡量控制在44個字節(jié)數(shù)內(nèi),走embstr編碼,embstr比raw編碼減少一次內(nèi)存分配,同時因為是連續(xù)內(nèi)存存儲,性能會更好。
(2)多個string類型可以合并成小段hash類型去維護(hù),小的hash類型走ziplist是有很好的壓縮效果,節(jié)約內(nèi)存。
(3)非string的類型的value對象的元素個數(shù)盡量不要太多,避免產(chǎn)生大key。
(4)在value的元素較多且頻繁變動,不要使用ziplist編碼,因為ziplist是連續(xù)的內(nèi)存分配,對頻繁更新的對象并不友好,性能損耗反而大。
(5)hash類型對象包含的元素不要太多,避免在rehash的時候消耗過多內(nèi)存。
(6)盡量不要修改ziplist限制的參數(shù)值,因為ziplist編碼雖然可以對內(nèi)存有很好的壓縮,但是如果元素太多使用ziplist的話,性能可能會有所下降。
3.2 客戶端緩沖優(yōu)化
客戶端緩存是很多內(nèi)存異常增長的罪魁禍?zhǔn)?,大部分都是普通客戶端輸出緩沖區(qū)異常增長導(dǎo)致,我們先了解下執(zhí)行命令的過程,客戶端發(fā)送一個或者通過piplie發(fā)送一組請求命令給服務(wù)端,然后等待服務(wù)端的響應(yīng),一般客戶端使用阻塞模式來等待服務(wù)端響應(yīng),數(shù)據(jù)在被客戶端讀取前,數(shù)據(jù)是存放在客戶端緩存區(qū),命令執(zhí)行的簡易流程圖如下:
異常增長原因可能如下幾種:
- 客戶端訪問大key 導(dǎo)致客戶端輸出緩存異常增長。
- 客戶端使用monitor命令訪問Redis,monitor命令會把所有訪問redis的命令持續(xù)存放到輸出緩沖區(qū),導(dǎo)致輸出緩沖區(qū)異常增長。
- 客戶端為了加快訪問效率,使用pipline封裝了大量命令,導(dǎo)致返回的結(jié)果集異常大(pipline的特性是等所有命令全部執(zhí)行完才返回,返回前都是暫存在輸出緩存區(qū))。
- 從節(jié)點應(yīng)用數(shù)據(jù)較慢,導(dǎo)致輸出主從復(fù)制輸出緩存有很多數(shù)據(jù)積壓,最后導(dǎo)致緩沖區(qū)異常增長。
異常表現(xiàn):
- 在Redis的info命令返回的結(jié)果里面,client部分client_recent_max_output_buffer的值很大。
- 在執(zhí)行client list命令返回的結(jié)果集里面,omem不為0且很大,omem代表該客戶端的輸出代表緩存使用的字節(jié)數(shù)。
- 在集群中,可能少部分used_memory在監(jiān)控顯示存在異常增長,因為不管是monitor或者pipeline都是針對單個實例的下發(fā)的命令。
優(yōu)化建議:
- 應(yīng)用不要設(shè)計大key,大key盡量拆分。
- 服務(wù)端的普通客戶端輸出緩存區(qū)通過參數(shù)設(shè)置,因為內(nèi)存告警的閾值大部分是使用率80%開始,實際建議參數(shù)可以設(shè)置為實例內(nèi)存的5%~15%左右,最好不要超過20%,避免OOM。
- 非特殊情況下避免使用monitor命令或者rename該命令。
- 在使用pipline的時候,pipeline不能封裝過多的命令,特別是一些返回結(jié)果集較多的命令更應(yīng)該少封裝。
- 主從復(fù)制輸出緩沖區(qū)大小設(shè)置參考: 緩沖區(qū)大小=(主庫寫入命令速度 * 操作大小 - 主從庫間網(wǎng)絡(luò)傳輸命令速度 * 操作大小)* 2。
3.3 碎片優(yōu)化
碎片優(yōu)化可以降低內(nèi)存使用率,提高訪問效率,在4.0以下版本,我們只能使用重啟恢復(fù),重啟加載rdb或者重啟通過高可用主從切換實現(xiàn)數(shù)據(jù)的重新加載可以減少碎片,在4.0以上版本,Redis提供了自動和手動的碎片整理功能,原理大致是把數(shù)據(jù)拷貝到新的內(nèi)存空間,然后把老的空間釋放掉,這個是有一定的性能損耗的。
- 【a. redis手動整理碎片】:執(zhí)行memory purge命令即可。
- 【b.redis自動整理碎片】:通過如下幾個參數(shù)控制
- 【activedefrag yes 】:啟用自動碎片清理開關(guān)
- 【active-defrag-ignore-bytes 100mb】:內(nèi)存碎片空間達(dá)到多少才開啟碎片整理
- 【active-defrag-threshold-lower 10】:碎片率達(dá)到百分之多少才開啟碎片整理
- 【active-defrag-threshold-upper 100 】:內(nèi)存碎片率超過多少,則盡最大努力整理(占用最大資源去做碎片整理)
- 【active-defrag-cycle-min 25 】:內(nèi)存自動整理占用資源最小百分比
- 【active-defrag-cycle-max 75】:內(nèi)存自動整理占用資源最大百分比
3.4 子進(jìn)程內(nèi)存優(yōu)化
前面談到 AOF rewrite和 RDB 生成動作會產(chǎn)生子進(jìn)程,正常在兩個動作執(zhí)行的過程中,Redis 寫操作沒有那么頻繁的情況下fork出來的子進(jìn)程是不會消耗很多內(nèi)存的,這個主要是因為 Redis 子進(jìn)程使用了 Linux 的 copy on write 機(jī)制,簡稱COW。
COW的核心是在fork出子進(jìn)程后,與父進(jìn)程共享內(nèi)存空間,只有在父進(jìn)程發(fā)生寫操作修改內(nèi)存數(shù)據(jù)時,才會真正去分配內(nèi)存空間,并復(fù)制內(nèi)存數(shù)據(jù)。
但是有一點需要注意,不要開啟操作系統(tǒng)的大頁THP(Transparent Huge Pages),開啟 THP 機(jī)制后,本來頁的大小由4KB變?yōu)? 2MB了。它雖然可以加快 fork 完成的速度( 因為要拷貝的頁的數(shù)量減少 ),但是會導(dǎo)致 copy-on-write 復(fù)制內(nèi)存頁的單位從 4KB 增大為 2MB,如果父進(jìn)程有大量寫命令,會加重內(nèi)存拷貝量,從而造成過度內(nèi)存消耗。
四、內(nèi)存優(yōu)化案例
4.1 緩沖區(qū)異常優(yōu)化案例
線上業(yè)務(wù) Redis 集群出現(xiàn)內(nèi)存告警,內(nèi)存使用率增長很快達(dá)到100%,值班人員先進(jìn)行了緊急擴(kuò)容,同時反饋至業(yè)務(wù)群是否有大量新數(shù)據(jù)寫入,業(yè)務(wù)反饋并無大量新數(shù)據(jù)寫入,且同時擴(kuò)容后的內(nèi)存還在漲,很快又要觸發(fā)告警了,業(yè)務(wù) DBA 去查監(jiān)控看看具體原因。
首先我們看used_memory增長只是集群的少數(shù)幾個實例,同時內(nèi)存異常的實例的key的數(shù)量并沒有異常增長,說明沒有寫入大批量數(shù)據(jù)導(dǎo)致。
我們再往下分析,可能是客戶端的內(nèi)存占用異常比較大,查看實例 info 里面的客戶端相關(guān)指標(biāo),觀察發(fā)現(xiàn)output_list的增長曲線和used_memory一致,可以判定是客戶端的輸出緩沖異常導(dǎo)致。
接下來我們再去通過client list查看是什么客戶端導(dǎo)致output增長,客戶端在執(zhí)行什么命令,同時去分析是否訪問大key。
執(zhí)行 client list |grep -i omem=0 發(fā)現(xiàn)如下:
id=12593807 addr=192.168.101.1:52086 fd=10767 name= age=15301 idle=0 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=16173 oll=341101
omem=5259227504 events=rw cmd=get
說明下相關(guān)的幾個重點的字段的含義:
- 【id】:就是客戶端的唯一標(biāo)識,經(jīng)常用于我們kill客戶端用到id;
- 【addr】:客戶端信息;
- 【obl】:固定緩沖區(qū)大小(字節(jié)),默認(rèn)是16K;
- 【oll】:動態(tài)緩沖區(qū)大小(對象個數(shù)),客戶端如果每條命令的響應(yīng)結(jié)果超過16k或者固定緩沖區(qū)寫滿了會寫動態(tài)緩沖區(qū);
- 【omem】: 指緩沖區(qū)的總字節(jié)數(shù);
- 【cmd】: 最近一次的操作命令。
可以看到緩沖區(qū)內(nèi)存占用很大,最近的操作命令也是get,所以我們先看看是否大key導(dǎo)致(我們是直接分析RDB發(fā)現(xiàn)并沒有大key),但是發(fā)現(xiàn)并沒有大key,而且get對應(yīng)的肯定是string類型,string類型的value最大是512M,所以單個key也不太可能產(chǎn)生這么大的緩存,所以斷定是客戶端緩存了多個key。
這個時候為了盡快恢復(fù),和業(yè)務(wù)溝通臨時kill該連接,內(nèi)存釋放,然后為了避免防止后面還產(chǎn)生異常,和業(yè)務(wù)方
- 【int:】 (整數(shù)且數(shù)字長度小于20,直接記錄在ptr*里面)
- 【embstr】: (連續(xù)分配的內(nèi)存(字符串長度小于等于44字節(jié)的字符串))
- 【raw】: 動態(tài)字符串(大于44個字節(jié)的字符串,同時字符長度小于 512M(512M是字符串的大小限制))
溝通設(shè)置普通客戶端緩存限制,因為最大內(nèi)存是25G,我們把緩存設(shè)置了2G-4G, 動態(tài)設(shè)置參數(shù)如下:
config set client-output-buffer-limit normal
4096mb 2048mb 120
因為參數(shù)限制也只是針對單個client的輸出緩沖這么大,所以還需要檢查客戶端使用使用 pipline 這種管道命令或者類似實現(xiàn)了封裝大批量命令導(dǎo)致結(jié)果統(tǒng)一返回之前被阻塞,后面確定確實會有這個操作,業(yè)務(wù)層就需要去逐步優(yōu)化,不然我們限制了輸出緩沖,達(dá)到了上限,會話會被kill, 所以業(yè)務(wù)不改的話還是會有拋錯。
業(yè)務(wù)方反饋用的是 C++ 語言 brpc 自帶的 Redis客戶端,第一次直接搜索沒有pipline的關(guān)鍵字,但是現(xiàn)象又指向使用的管道,所以繼續(xù)仔細(xì)看了下代碼,發(fā)現(xiàn)其內(nèi)部是實現(xiàn)了pipline類似的功能,也是會對多個命令進(jìn)行封裝去請求redis,然后統(tǒng)一返回結(jié)果,客戶端GitHub鏈接如下:
??https://github.com/apache/incubator-brpc/blob/master/docs/cn/redis_client.md??
總結(jié):
pipline 在 Redis 客戶端中使用的挺多的,因為確實可以提供訪問效率,但是使用不當(dāng)反而會影響訪問,應(yīng)該控制好訪問,生產(chǎn)環(huán)境也盡量加這些內(nèi)存限制,避免部分客戶端的異常訪問影響全局使用。
4.2 從節(jié)點內(nèi)存異常增長案例
線上 Redis 集群出現(xiàn)內(nèi)存使用率超過 95% 的災(zāi)難告警,但是該集群是有190個節(jié)點的集群觸發(fā)異常內(nèi)存告警的只有3個節(jié)點。所以查看集群對應(yīng)信息以及監(jiān)控指標(biāo)發(fā)現(xiàn)如下有用信息:
- 3個從節(jié)點對應(yīng)的主節(jié)點內(nèi)存沒有變化,從節(jié)點的內(nèi)存是逐步增長的。
- 發(fā)現(xiàn)集群整體ops比較低,說明業(yè)務(wù)變化并不大,沒有發(fā)現(xiàn)有效命令突增。
- 主從節(jié)點的最大內(nèi)存不一致,主節(jié)點是6G,從節(jié)點是5G,這個是導(dǎo)致災(zāi)難告警的重要原因。
- 在出問題前,主節(jié)點比從節(jié)點的內(nèi)存大概多出1.3G,后面從節(jié)點used_memory逐步增長到超過主節(jié)點內(nèi)存,但是rss內(nèi)存是最后保持了一樣。
- 主從復(fù)制出現(xiàn)延遲也內(nèi)存增長的那個時間段。
處理過程:
首先想到的應(yīng)該是保持主從節(jié)點最大內(nèi)存一致,但是因為主機(jī)內(nèi)存使用率比較高暫時沒法擴(kuò)容,因為想到的是從節(jié)點可能什么原因阻塞,所以和業(yè)務(wù)方溝通是重啟下2從節(jié)點緩解下,重啟后從節(jié)點內(nèi)存釋放,降到發(fā)生問題前的水平,如上圖,后面主機(jī)空出了內(nèi)存資源,所以優(yōu)先把內(nèi)存調(diào)整一致。
內(nèi)存調(diào)整好了一周后,這3個從節(jié)點內(nèi)存又告警了,因為現(xiàn)在主從內(nèi)存是一致的,所以觸發(fā)的是嚴(yán)重告警(>85%),查看監(jiān)控發(fā)現(xiàn)情況是和之前一樣,猜測這個是某些操作觸發(fā)的,所以還是決定問問業(yè)務(wù)方這 兩個時間段都有哪些操作,業(yè)務(wù)反饋這段時間就是在寫業(yè)務(wù),那2個時間段都是在寫入,也看了寫redis的那段代碼,用了一個比較少見的命令append,append是對string類型的value進(jìn)行追加。
這里就得提下string類型在 Redis 里面是怎么分配內(nèi)存的:string類型都是都是sds存儲,當(dāng)前分配的sds內(nèi)存空間不足存儲且小于1M時候,Redis會重新分配一個2倍之前內(nèi)存大小的內(nèi)存空間。
根據(jù)上面到知識點,所以可以大致可以解析上述一系列的問題,大概是當(dāng)時做 append 操作,從節(jié)點需要分配空間從而發(fā)生內(nèi)存膨脹,而主節(jié)點不需要分配空間,因為內(nèi)存重新分配設(shè)計malloc和free操作,所以當(dāng)時有l(wèi)ag也是正常的。
Redis的主從本身是一個邏輯復(fù)制,加載 RDB 的過程其實也是拿到kv不斷的寫入到從節(jié)點,所以主從到內(nèi)存大小也經(jīng)常存在不相同的情況,特別是這種values大小經(jīng)常改變的場景,主從存儲的kv所用的空間很多可能是不一樣的。
為了證明這一猜測,我們可以通過獲取一個key(value大小要比較大)在主從節(jié)點占用空間的大小,因為是4.0以上版本,所以我們可以使用memory USAGE 去獲取大小,看看差異有多少,我們隨機(jī)找了幾個稍微大點的key去查看,發(fā)現(xiàn)在有些key從庫占用空間是主庫的近2倍,有的差不多,有的也是1倍多,rdb解析出來的這個key空間更小,說明從節(jié)點重啟后加載rdb進(jìn)行存放是最小的,然后因為某段時間大批量key操作,導(dǎo)致從節(jié)點的大批量的key分配的空間不足,需要擴(kuò)容1倍空間,導(dǎo)致內(nèi)存出現(xiàn)增長。
到這就分析的其實差不多了,因為append的特性,為了避免內(nèi)存再次出現(xiàn)內(nèi)存告警,決定把該集群的內(nèi)存進(jìn)行擴(kuò)容,控制內(nèi)存使用率在70%以下(避免可能發(fā)生的大量key使用內(nèi)存翻倍的情況)。
最后還有1個問題:上面的used_memory為什么會比memory_rss的值還大呢?(swap是關(guān)閉的)。
這是因為jemalloc內(nèi)存分配一開始其實分配的是虛擬內(nèi)存,只有往分配的page頁里面寫數(shù)據(jù)的時候才會真正分配內(nèi)存,memory_rss是實際內(nèi)存占用,used_memory其實是一個計數(shù)器,在 Redis做內(nèi)存的malloc/free的時候,對這個used_memory做加減法。
關(guān)于used_memory大于memory_rss的問題,redis作者也做了回答:
??https://github.com/redis/redis/issues/946#issuecomment-13599772??
總結(jié):
在知曉 Redis內(nèi)存分配原理的情況下,數(shù)據(jù)庫的內(nèi)存異常問題進(jìn)行分析會比較快速定位,另外可能某個問題看起來和業(yè)務(wù)沒什么關(guān)聯(lián),但是我們還是應(yīng)該多和業(yè)務(wù)方溝通獲取一些線索排查問題,最后主從內(nèi)存一定按照規(guī)范保持一致。
五、總結(jié)
Redis在數(shù)據(jù)存儲、緩存都是做了很巧妙的設(shè)計和優(yōu)化,我們在了解了它的內(nèi)部結(jié)構(gòu)、存儲方式之后,我們可以提前在key的設(shè)計上做優(yōu)化。我們在遇到內(nèi)存異?;蛘咝阅軆?yōu)化的時候,可以不再局限于表面的一些分析如:資源消耗、命令的復(fù)雜度、key的大小,還可以結(jié)合根據(jù)Redis的一些內(nèi)部運行機(jī)制和內(nèi)存管理方式去深入發(fā)現(xiàn)是否還有可能哪些方面導(dǎo)致異常或者性能下降。