3萬(wàn)字聊聊什么是Redis(四)
大家好,我是Leo
繼上篇Redis技術(shù)總結(jié)三,我們繼續(xù)聊聊Redis的相關(guān)技術(shù)!
上一篇我們介紹了
- 主要介紹了Redis的類(lèi)型的底層實(shí)現(xiàn)以及技術(shù),類(lèi)型選擇的依據(jù)
- 通過(guò)時(shí)間序列數(shù)據(jù)引出多種類(lèi)型的搭配使用思路以及擴(kuò)展一下RedisTimeSeries模塊的使用。
- Redis作為消息隊(duì)列也是高頻的面試問(wèn)題,通過(guò)這一問(wèn)題延伸了List的優(yōu)劣和Streams的應(yīng)用
這篇主要是介紹一下 Redis 有哪些阻塞原因
推薦閱讀
單線程模型阻塞
聊到阻塞原因,這應(yīng)該是單線程的噩夢(mèng)吧。平時(shí)在使用中,想了解Redis的阻塞原因,必然是要從它的交互對(duì)象入手的,下面我列舉了Redis常見(jiàn)的交互場(chǎng)景
- 客戶(hù)端
- 磁盤(pán)
- 主從節(jié)點(diǎn)
- 切片集群實(shí)例
客戶(hù)端
與客戶(hù)端主要是有網(wǎng)絡(luò)IO的開(kāi)銷(xiāo),鍵值對(duì)增刪改查操作,數(shù)據(jù)庫(kù)操作等。
Redis使用了IO多路復(fù)用機(jī)制,避免了主線程一直處在等待網(wǎng)絡(luò)連接或請(qǐng)求到來(lái)的狀態(tài),所以網(wǎng)絡(luò)IO不是導(dǎo)致Redis阻塞的因素。
查詢(xún):鍵值對(duì)查詢(xún)可以通過(guò)時(shí)間復(fù)雜度來(lái)判斷,當(dāng)使用集合類(lèi)型時(shí),一般的時(shí)間復(fù)雜度都是O(n)。所以?集合類(lèi)型的列表查詢(xún),以及聚合統(tǒng)計(jì)操作將是Redis的第一個(gè)阻塞點(diǎn)。
刪除:刪除也是Redis阻塞的重要因素之一,很多人不明白,刪除不就是直接刪掉指針的索引嗎?其實(shí)不然,這里和MySQL是有點(diǎn)區(qū)別的。Redis這里為了保證高效的管理內(nèi)存,操作系統(tǒng)需要把釋放掉的內(nèi)存,插入一個(gè)空閑內(nèi)存塊,以便后續(xù)進(jìn)行管理和分配。所以如果一下子釋放大量?jī)?nèi)存,插入空閑內(nèi)存塊的效率就被拉下來(lái)了。這里也可以稱(chēng)為?bigkey刪除,也為第二個(gè)阻塞點(diǎn)
數(shù)據(jù)庫(kù):如果刪除bigkey為阻塞Redis的因素之一的話,清空數(shù)據(jù)庫(kù)也是名副其實(shí)的第三個(gè)阻塞點(diǎn)了
磁盤(pán)
與磁盤(pán)的話主要是生成RDB快照保存到本地,記錄AOF日志,AOF日志重寫(xiě)等。
Redis意識(shí)到了磁盤(pán)IO帶來(lái)的阻塞影響,所以采用子線程生成RDB快照,以及子線程執(zhí)行AOF重寫(xiě)操作。
唯一的一個(gè)阻塞點(diǎn)就是Redis記錄AOF日志時(shí),會(huì)根據(jù)不同的寫(xiě)會(huì)策略對(duì)數(shù)據(jù)進(jìn)行保存。AOF日志同步寫(xiě)
主從節(jié)點(diǎn)
與從庫(kù)主要就是在做數(shù)據(jù)同步時(shí),主庫(kù)需要生成RDB快照發(fā)給從庫(kù),從庫(kù)接收,從庫(kù)清空數(shù)據(jù)庫(kù),加載RDB文件等。
通過(guò)子線程生成RDB不會(huì)阻塞,清空數(shù)據(jù)庫(kù)在上述已經(jīng)被列為阻塞點(diǎn)了,主從節(jié)點(diǎn)這里主要說(shuō)一下這個(gè)加載RDB文件,如果RDB文件過(guò)大同樣會(huì)阻塞。所以?加載RDB文件也成了阻塞點(diǎn)之一
切片集群實(shí)例
與切片集群主要就是查詢(xún)一個(gè)數(shù)據(jù)時(shí),當(dāng)前數(shù)據(jù)不在這臺(tái)實(shí)例上,會(huì)向其他實(shí)例傳輸哈希槽信息,數(shù)據(jù)遷移,哈希槽的信息量不會(huì)太大,而數(shù)據(jù)的遷移又是漸進(jìn)式的。所以哈希槽和數(shù)據(jù)遷移對(duì)Redis的阻塞影響不大。
不過(guò)如果使用的是Redis Cluster方案,同時(shí)又在遷移bigkey時(shí),就會(huì)造成阻塞了。
調(diào)優(yōu)方案
如何調(diào)優(yōu)?我們可以把不是必須等待的操作全部改成異步執(zhí)行。
大查詢(xún),聚合查詢(xún)顯然是必須操作完成之后才會(huì)繼續(xù)操作的,所以這個(gè)無(wú)法調(diào)優(yōu),只能從數(shù)據(jù)類(lèi)型下手,或者借助其他方案。
bigkey和清空數(shù)據(jù)庫(kù)都不需要等待返回后再繼續(xù)執(zhí)行,所以這兩個(gè)阻塞點(diǎn)就可以?xún)?yōu)化成異步執(zhí)行。
加載RDB文件的話為了保證從庫(kù)數(shù)據(jù)接收完成所以必須要等待的。嚴(yán)格來(lái)說(shuō)從庫(kù)會(huì)給主庫(kù)發(fā)一個(gè)ack的信息。
AOF日志同步寫(xiě)也可以異步操作,它并不需要返回結(jié)果給實(shí)例。
子線程機(jī)制
上述聊到五個(gè)阻塞點(diǎn),三個(gè)阻塞點(diǎn)都可以通過(guò)異步的方式優(yōu)化Redis的整體性能,那我們聊什么子線程機(jī)制吧。
Redis主線程啟動(dòng)后,會(huì)使用操作系統(tǒng)提供的pthread_create函數(shù)創(chuàng)建3個(gè)子線程,分別由他們負(fù)責(zé)AOF日志同步寫(xiě),bigkey刪除,清空數(shù)據(jù)庫(kù)進(jìn)行異步執(zhí)行。(下圖來(lái)自蔣德鈞老師)

客戶(hù)端請(qǐng)求Redis實(shí)例,Redis通過(guò)一個(gè)鏈表存儲(chǔ)一系列異步任務(wù),任務(wù)列表與子線程交互進(jìn)行異步執(zhí)行AOF日志同步寫(xiě),刪除bigkey,文件關(guān)閉(清空數(shù)據(jù)庫(kù))操作。
隨后Redis會(huì)立刻給客戶(hù)端返回執(zhí)行完成。表明刪除已經(jīng)完成了。
CPU結(jié)構(gòu)阻塞Redis性能
很多人聊到CPU是比較懵逼的,會(huì)問(wèn)為什么CPU影響性能呢?
不好意思各位,實(shí)在沒(méi)看懂CPU類(lèi)的相關(guān)知識(shí),計(jì)算機(jī)組成原理底子不夠,這塊知識(shí)我們后續(xù)再輸出吧!
Redis變慢排查思路
Redis突然變慢了,你會(huì)如何排查呢?一定不要病急亂投醫(yī),因?yàn)榇a這個(gè)東西就像自來(lái)水一樣,補(bǔ)了一塊漏了另一塊。最后有可能窟窿越來(lái)越大
步入正題!
首先第一步就是要查看Redis的響應(yīng)延遲。大部分Redis延遲很低,但是有些實(shí)例會(huì)出現(xiàn)很高的延遲。
Redis的延遲與硬件有著很大的關(guān)系,
第二步就是基于當(dāng)前環(huán)境下的Redis基線做判斷。所謂的基線性能呢,就是一個(gè)系統(tǒng)在低壓力,無(wú)干擾下的基本性能,這個(gè)性能只能由當(dāng)前的軟硬件決定。
我們可以使用redis-cli命令提供的 intrinsic-latency 選項(xiàng),用來(lái)監(jiān)測(cè)和統(tǒng)計(jì)測(cè)試期間內(nèi)的最大延遲,這個(gè)延遲可以作為基線性能。
第三步就是可以通過(guò)iPef這樣的工具測(cè)試從客戶(hù)端到服務(wù)端的網(wǎng)絡(luò)延遲,如果這個(gè)延遲有幾十毫秒甚至幾百毫秒,說(shuō)明Redis的運(yùn)行的網(wǎng)絡(luò)環(huán)境有很大流量在其他的應(yīng)用程序在運(yùn)行導(dǎo)致阻塞
如何解決Redis變慢
Redis變慢這個(gè)應(yīng)該屬于T0級(jí)事故了。為什么這么說(shuō)呢?
一旦Redis延遲就會(huì)引起業(yè)務(wù)系統(tǒng)中的一串連鎖反應(yīng),我們舉個(gè)例子吧。
我目前自己負(fù)責(zé)的千萬(wàn)級(jí)跨境電商中,訂單號(hào)的生成采用Redis存取,我的邏輯就是今天的第一個(gè)訂單,并且Redis中沒(méi)有以當(dāng)前日期定義的key時(shí),我會(huì)在程序中通過(guò)當(dāng)前時(shí)間戳+雪花算法+隨機(jī)函數(shù)+當(dāng)天訂單數(shù)生成一串?dāng)?shù)字并且寫(xiě)入Redis中。
等下次生成訂單號(hào)后發(fā)送Redis中存在當(dāng)前的訂單號(hào)就會(huì)直接取value并且執(zhí)行incrdy命令進(jìn)行加1。示例如下。
訂單號(hào):2021120121523033473
如果在Redis這邊阻塞了或者延遲了,那么訂單系統(tǒng)就無(wú)法正常提供服務(wù)了,因?yàn)橐却@個(gè)訂單號(hào)才能插入數(shù)據(jù)表中,積分模塊,用戶(hù)模塊,信息通知模塊,商品庫(kù)存模塊都將會(huì)受到或多收少的影響。
那么如何解決Redis變慢問(wèn)題?我們要基于Redis本身的工作原理的理解并且結(jié)合和它交互的操作系統(tǒng)機(jī)制,再借助一些輔助工具來(lái)定位原因,再指定一套有效的解決方案。
自身影響
不管干啥,先反思自己!
Redis中的?慢查詢(xún)?會(huì)導(dǎo)致實(shí)例延遲,比如查詢(xún)一個(gè)大數(shù)據(jù)量的集合列表。如果請(qǐng)求量不大還好,一旦請(qǐng)求量較大就要優(yōu)化操作命令了。
有兩種解決思路
- 用其他高效命令代替
- 當(dāng)執(zhí)行列表的聚合統(tǒng)計(jì)時(shí),為了不影響整個(gè)系統(tǒng),可以選擇在客戶(hù)端執(zhí)行。
keys命令
少用keys命令,因?yàn)樾枰闅v存儲(chǔ)的鍵值對(duì),所以操作延時(shí)高。keys命令一般不建議用于生產(chǎn)環(huán)境中。
過(guò)期key操作
聊到過(guò)期key,我們還是先來(lái)了解一下Redis的過(guò)期刪除機(jī)制吧
過(guò)期刪除機(jī)制是Redis用來(lái)回收內(nèi)存空間的常用機(jī)制,可以對(duì)鍵值對(duì)設(shè)置過(guò)期時(shí)間,默認(rèn)情況下,Redis每100毫秒就會(huì)刪除一些過(guò)期的key,具體算法如下:
- 采樣 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 個(gè)數(shù)的 key,并將其中過(guò)期的 key 全部刪除;
- 如果超過(guò) 25% 的 key 過(guò)期了,則重復(fù)刪除的過(guò)程,直到過(guò)期 key 的比例降至 25% 以下。
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 默認(rèn)為20。毫秒的概念可能不太明顯,我們換算一下也就是每秒刪除200個(gè)過(guò)期key(20個(gè)/100毫秒)=(200個(gè)/秒)
如果按照第一種的話,并不是造成太大的影響。如果命中了第二種,就會(huì)造成key大面積失效過(guò)期,過(guò)期的key超過(guò)了25%,會(huì)一直刪除直至降至25%。這段刪除期間會(huì)大量釋放內(nèi)存空間,大量插入鏈表填補(bǔ)。Redis就變慢了。
建議
其一:在生產(chǎn)環(huán)境不要頻繁使用帶有相同時(shí)間參數(shù)的expireat命令設(shè)置過(guò)期key。會(huì)導(dǎo)致一秒內(nèi)有大量的key同時(shí)過(guò)期。
其二:在使用 EXPIREAT 命令設(shè)置 key 過(guò)期時(shí)間時(shí),是否使用了相同的 UNIX 時(shí)間戳,有沒(méi)有使用 EXPIRE 命令給批量的 key 設(shè)置相同的過(guò)期秒數(shù)。因?yàn)椋@都會(huì)造成大量 key 在同一時(shí)間過(guò)期,導(dǎo)致性能變慢。
其三:首先要根據(jù)實(shí)際業(yè)務(wù)的使用需求,決定 EXPIREAT 和 EXPIRE 的過(guò)期時(shí)間參數(shù)。其次,如果一批 key 的確是同時(shí)過(guò)期,你還可以在 EXPIREAT 和 EXPIRE 的過(guò)期時(shí)間參數(shù)上,加上一個(gè)一定大小范圍內(nèi)的隨機(jī)數(shù),這樣,既保證了 key 在一個(gè)鄰近時(shí)間范圍內(nèi)被刪除,又避免了同時(shí)過(guò)期造成的壓力。
外在影響
除了Redis的內(nèi)在影響,繼續(xù)介紹一下外在的影響。
- Redis要想持久化肯定是要通過(guò)文件系統(tǒng)的,所以,文件系統(tǒng)將數(shù)據(jù)寫(xiě)回磁盤(pán)的機(jī)制,會(huì)直接影響到 Redis 持久化的效率。而且,在持久化的過(guò)程中,Redis 也還在接收其他請(qǐng)求,持久化的效率高低又會(huì)影響到 Redis 處理請(qǐng)求的性能。
- Redis 是內(nèi)存數(shù)據(jù)庫(kù),內(nèi)存操作非常頻繁,所以,操作系統(tǒng)的內(nèi)存機(jī)制會(huì)直接影響到 Redis 的處理效率。比如說(shuō),如果 Redis 的內(nèi)存不夠用了,操作系統(tǒng)會(huì)啟動(dòng) swap 機(jī)制,這就會(huì)直接拖慢 Redis
文件系統(tǒng)
Redis是內(nèi)存數(shù)據(jù)庫(kù),但是想要數(shù)據(jù)持久化必然離不開(kāi)文件系統(tǒng),我們知道RDB快照和AOF日志作為Redis持久化機(jī)制的兩大招牌。就從這里說(shuō)起吧。
AOF日志的寫(xiě)回策略主要有三種no,everysec,always。這三種寫(xiě)回策略主要依托?write?和?fsync?。
write:把數(shù)據(jù)寫(xiě)入緩沖區(qū)(內(nèi)核緩沖區(qū))等待數(shù)據(jù)刷新到磁盤(pán)。
fsync:把緩沖區(qū)(內(nèi)核緩沖區(qū))的數(shù)據(jù)寫(xiě)入到磁盤(pán)。
fsync的執(zhí)行時(shí)間大于write,也比write更耗時(shí)!
no:調(diào)用write寫(xiě)日志文件,由操作系統(tǒng)周期性地將日志寫(xiě)會(huì)磁盤(pán)
everysec:每秒調(diào)用一次fsync,將日志寫(xiě)回磁盤(pán)
always:每執(zhí)行一個(gè)操作,就調(diào)用一次fsync將日志寫(xiě)回磁盤(pán)
當(dāng)AOF的策略為everysec時(shí),Redis允許丟失1秒中的操作記錄,由后臺(tái)子線程異步完成fsyc操作。
當(dāng)AOF的策略為always時(shí),就代表每一個(gè)操作記錄都要保證寫(xiě)入磁盤(pán)后才能進(jìn)行下一操作,所以無(wú)法通過(guò)后臺(tái)子線程異步執(zhí)行。
在使用AOF日志時(shí),為了避免日志不斷增大,Redis提供了AOF重寫(xiě),生成體量較小的AOF日志文件,AOF重寫(xiě)需要很長(zhǎng)的時(shí)間,也容易阻塞Redis主線程,所以會(huì)采用異步子線程重寫(xiě)。
但是,AOF重寫(xiě)會(huì)對(duì)磁盤(pán)進(jìn)行大量的IO操作,fsync需要等數(shù)據(jù)寫(xiě)入到磁盤(pán)之后才能返回,所以這個(gè)地方就會(huì)容易造成阻塞。如果上一個(gè)fsync沒(méi)有執(zhí)行完成,Redis主線新來(lái)了一些數(shù)據(jù)需要調(diào)用fsync系統(tǒng)這個(gè)時(shí)候就阻塞了從而導(dǎo)致Redis性能變慢。
根據(jù)不同的業(yè)務(wù)需求來(lái)決定AOF的寫(xiě)入策略,在Redis.config 配置文件中可以通過(guò) appendfsync 配置項(xiàng)來(lái)決定當(dāng)前Redis實(shí)例寫(xiě)入策略,從而達(dá)到性能的優(yōu)化!
如果不了解Redis AOF機(jī)制的話,往往我們會(huì)用一個(gè)大杯子會(huì)裝很小的水。這樣不就很浪費(fèi)性能嘛!
如果業(yè)務(wù)應(yīng)用對(duì)延遲非常敏感,但同時(shí)允許一定量的數(shù)據(jù)丟失,那么,可以把配置項(xiàng)?no-appendfsync-on-rewrite 設(shè)置為 yes?,表示AOF重寫(xiě)時(shí),不進(jìn)行fsync操作。write寫(xiě)入到內(nèi)核緩沖區(qū)之后就可以直接返回繼續(xù)提供服務(wù)了。
swap機(jī)制
內(nèi)存swap是操作系統(tǒng)里將內(nèi)存數(shù)據(jù)在內(nèi)存和磁盤(pán)間來(lái)回?fù)Q入和換出的機(jī)制,涉及到磁盤(pán)的讀寫(xiě)。所以,一旦觸發(fā)swap,無(wú)論是被換入數(shù)據(jù)的進(jìn)程,還是被換出數(shù)據(jù)的進(jìn)程,其性能都會(huì)受到慢速磁盤(pán)讀寫(xiě)的影響。
如果不掌握好Redis的內(nèi)存使用,萬(wàn)一誤撞上swap機(jī)制,Redis 的請(qǐng)求操作需要等到磁盤(pán)數(shù)據(jù)讀寫(xiě)完成才行。而且,和我剛才說(shuō)的 AOF 日志文件讀寫(xiě)使用 fsync 線程不同,swap 觸發(fā)后影響的是 Redis 主 IO 線程,這會(huì)極大地增加 Redis 的響應(yīng)時(shí)間。也是影響Redis性能的因素之一。
什么時(shí)候觸發(fā)swap機(jī)制呢?
- Redis 實(shí)例自身使用了大量的內(nèi)存,導(dǎo)致物理機(jī)器的可用內(nèi)存不足;
- 和 Redis 實(shí)例在同一臺(tái)機(jī)器上運(yùn)行的其他進(jìn)程,在進(jìn)行大量的文件讀寫(xiě)操作。文件讀寫(xiě)本身會(huì)占用系統(tǒng)內(nèi)存,這會(huì)導(dǎo)致分配給 Redis 實(shí)例的內(nèi)存量變少,進(jìn)而觸發(fā) Redis 發(fā)生 swap。
內(nèi)存大頁(yè)機(jī)制
內(nèi)存大頁(yè)機(jī)制也會(huì)影響Redis性能。
任何機(jī)制的設(shè)定幾乎都是取舍的一個(gè)過(guò)程,優(yōu)勢(shì)和劣勢(shì)并存的。我們只需要了解Redis對(duì)應(yīng)的不足,加上業(yè)務(wù)的需求把性能優(yōu)化到極致即可!
Redis在做持久化時(shí),由子線程執(zhí)行寫(xiě)入磁盤(pán),在此期間主線程依然可以處理讀寫(xiě)請(qǐng)求。一旦有新的寫(xiě)請(qǐng)求,Redis會(huì)采用寫(xiě)時(shí)復(fù)制機(jī)制將這些數(shù)據(jù)拷貝一份,然后再進(jìn)行修改。而不會(huì)直接修改內(nèi)存中的數(shù)據(jù)。
如果采用內(nèi)存大頁(yè)機(jī)制,客戶(hù)端修改了100B的數(shù)據(jù),Redis還是需要2MB的大頁(yè)。
如果不采用內(nèi)存大頁(yè)機(jī)制,客戶(hù)端修改了100B的數(shù)據(jù),Redis只需要拷貝4KB就夠了。
兩者相比使用內(nèi)存大頁(yè)機(jī)制會(huì)影響Redis正常的訪存操作,最終影響性能變慢。
說(shuō)完劣勢(shì),優(yōu)勢(shì)呢?肯定也是有的!
Redis 是內(nèi)存數(shù)據(jù)庫(kù),在分配相同的內(nèi)存量時(shí),內(nèi)存大頁(yè)能減少分配次數(shù),可以給 Redis 帶來(lái)內(nèi)存分配方面的收益。
關(guān)閉內(nèi)存大頁(yè)方法?cat /sys/kernel/mm/transparent_hugepage/enabled
如果執(zhí)行結(jié)果是 always,就表明內(nèi)存大頁(yè)機(jī)制被啟動(dòng)了;如果是 never,就表示,內(nèi)存大頁(yè)機(jī)制被禁止。
莫名其妙的內(nèi)存占用率
聊一下為什么數(shù)據(jù)已經(jīng)刪除了,但內(nèi)存卻閑置著沒(méi)有用以及對(duì)應(yīng)的解決方案。
通常情況下,內(nèi)存空間閑置往往是因?yàn)椴僮飨到y(tǒng)發(fā)生較為嚴(yán)重的內(nèi)存碎片。
什么是內(nèi)存碎片呢?
我們舉一個(gè)類(lèi)似的例子吧。我們平時(shí)住的房子,有一個(gè)房間是5 * 5 m。這個(gè)時(shí)候放了一個(gè)床是 2*2 m。如下圖。

再放一個(gè)衣柜有地方嘛?這個(gè)房間剩余20平方米,為什么還放不下16平方米的柜子呢?這個(gè)就是內(nèi)存碎片的大概意思。
如何形成內(nèi)存碎片?
主要有兩個(gè)原因
- 操作系統(tǒng)的內(nèi)存分配機(jī)制
- Redis的負(fù)載特征
內(nèi)存分配機(jī)制
操作系統(tǒng)的內(nèi)存分配機(jī)制的分配策略決定了操作系統(tǒng)無(wú)法做到按需分配。這是因?yàn)閮?nèi)存分配器一般是按固定大小來(lái)分配內(nèi)存,而不是按照應(yīng)用程序申請(qǐng)的內(nèi)存空間大小分配的。這就導(dǎo)致每次分配都會(huì)存在部分空隙。
Redis采用jemalloc內(nèi)存分配器來(lái)分配內(nèi)存。
分配策略:首先會(huì)給出固定大小劃分的內(nèi)存空間,例如8,16,32,48字節(jié),2,4,8KB等。當(dāng)前程序申請(qǐng)的內(nèi)存最接近某個(gè)固定值時(shí),jemalloc會(huì)給他分配相應(yīng)大小的空間。
這樣的分配好處是為了減少分配次數(shù)。假如Redis申請(qǐng)了一個(gè)20字節(jié)的空間保存數(shù)據(jù),jemalloc就會(huì)開(kāi)辟32字節(jié),如果應(yīng)用還要寫(xiě)入10字節(jié)數(shù)據(jù),Redis就不用再向操作系統(tǒng)申請(qǐng)空間也可以夠存放了。
Redis的負(fù)載特征
第一點(diǎn)就是分配的空間大小沒(méi)有完成利用
如果Redis每次向分配器申請(qǐng)的內(nèi)存空間大小不一樣,這種分配方式就會(huì)有形成碎片的風(fēng)險(xiǎn)!從而降低內(nèi)存空間存儲(chǔ)效率。
比如應(yīng)用A保存6字節(jié)數(shù)據(jù),jemalloc按照分配策略會(huì)分配8字節(jié),還剩余2字節(jié)。如果后續(xù)不再使用就形成了內(nèi)存碎片!
第二點(diǎn)就是就是修改和刪除操作,會(huì)導(dǎo)致內(nèi)存空間的擴(kuò)容和釋放。?(這里引用一下蔣德均老師的圖)

- 一開(kāi)始,應(yīng)用 A、B、C、D 分別保存了 3、1、2、4 字節(jié)的數(shù)據(jù),并占據(jù)了相應(yīng)的內(nèi)存空間。
- 然后,應(yīng)用 D 刪除了 1 個(gè)字節(jié),這個(gè) 1 字節(jié)的內(nèi)存空間就空出來(lái)了。
- 緊接著,應(yīng)用 A 修改了數(shù)據(jù),從 3 字節(jié)變成了 4 字節(jié)。為了保持 A 數(shù)據(jù)的空間連續(xù)性,操作系統(tǒng)就需要把 B 的數(shù)據(jù)拷貝到別的空間,比如拷貝到 D 剛剛釋放的空間中。此時(shí),應(yīng)用 C 和 D 也分別刪除了 2 字節(jié)和 1 字節(jié)的數(shù)據(jù),整個(gè)內(nèi)存空間上就分別出現(xiàn)了 2 字節(jié)和 1 字節(jié)的空閑碎片。
如果應(yīng)用 E 想要一個(gè) 3 字節(jié)的連續(xù)空間,顯然是不能得到滿(mǎn)足的。因?yàn)椋m然空間總量夠,但卻是碎片空間,并不是連續(xù)的。
解決方案
知道了什么是內(nèi)存碎片,如何形成內(nèi)存碎片,下面自然到了解決方案了。
- 首先我們要知道如何判斷有內(nèi)存碎片
- 知道了之后如何清理
如何判斷內(nèi)存碎片
我們可以通過(guò)Redis提供INFO命令來(lái)監(jiān)控各項(xiàng)指標(biāo)。
used_memory:917544
used_memory_human:896.04K
used_memory_rss:11309056
used_memory_rss_human:10.79M
used_memory_peak:1033864
used_memory_peak_human:1009.63K
used_memory_peak_perc:88.75%
used_memory_overhead:853072
used_memory_startup:810088
used_memory_dataset:64472
used_memory_dataset_perc:60.00%
allocator_allocated:956776
allocator_active:1314816
allocator_resident:3551232
total_system_memory:1918222336
total_system_memory_human:1.79G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:228000000
maxmemory_human:217.44M
maxmemory_policy:noeviction
allocator_frag_ratio:1.37
allocator_frag_bytes:358040
allocator_rss_ratio:2.70
allocator_rss_bytes:2236416
rss_overhead_ratio:3.18
rss_overhead_bytes:7757824
mem_fragmentation_ratio:12.90
mem_fragmentation_bytes:10432528
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:41008
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
lazyfreed_objects:0
可以通過(guò)?mem_fragmentation_ratio?指標(biāo)來(lái)判斷Redis當(dāng)前的內(nèi)存碎片率。
來(lái)著蔣德均老師(課就不發(fā)了,怕被誤以為賣(mài)課)
內(nèi)存碎片率 = 操作系統(tǒng)給Redis的物理內(nèi)存空間 / Redis為了保護(hù)數(shù)據(jù)實(shí)際申請(qǐng)使用的空間
mem_fragmentation_ratio = used_memory_rss / used_memory
mem_fragmentation_ratio 大于 1 但小于 1.5。這種情況是合理的。這是因?yàn)椋瑒偛盼医榻B的那些因素是難以避免的。畢竟,內(nèi)因的內(nèi)存分配器是一定要使用的,分配策略都是通用的,不會(huì)輕易修改;而外因由 Redis 負(fù)載決定,也無(wú)法限制。所以,存在內(nèi)存碎片也是正常的。
mem_fragmentation_ratio 大于 1.5 。這表明內(nèi)存碎片率已經(jīng)超過(guò)了 50%。一般情況下,這個(gè)時(shí)候,我們就需要采取一些措施來(lái)降低內(nèi)存碎片率了。
如何清理內(nèi)存碎片
最簡(jiǎn)單的方式就是重啟Redis實(shí)例,使內(nèi)存重新合理的分配!但是如果有數(shù)據(jù)沒(méi)持久化就嗝屁了,我們不能因?yàn)橐峁┬阅芏绊懻鎸?shí)的生產(chǎn)數(shù)據(jù)!
第二種方式就是Redis自身提供了一種內(nèi)存碎片自動(dòng)清理的方法,我們來(lái)看看這個(gè)方法的基本機(jī)制吧!
內(nèi)存清理簡(jiǎn)單來(lái)說(shuō),頁(yè)面A的所有數(shù)據(jù)都緊鄰的拷貝到頁(yè)面B上。
操作系統(tǒng)在清理碎片時(shí),會(huì)把不連續(xù)的內(nèi)存拷貝到緊鄰的內(nèi)存空間,并釋放原先所占的空間。這樣一來(lái),這個(gè)頁(yè)上的所有已用空間就都聚在一起了,剩余的空間就形成了一塊連續(xù)的空間。碎片清理就結(jié)束了。
事事沒(méi)有完美的!碎片清理是有代價(jià)的,操作系統(tǒng)需要把多份數(shù)據(jù)拷貝到新位置,把原有空間釋放出來(lái),這會(huì)帶來(lái)時(shí)間開(kāi)銷(xiāo)。因?yàn)镽edis是單線程的在拷貝數(shù)據(jù)時(shí),只能等待!這就導(dǎo)致Redis無(wú)法及時(shí)處理請(qǐng)求,性能就會(huì)降低!
如何優(yōu)化清理內(nèi)存碎片帶來(lái)的性能影響
Redis專(zhuān)門(mén)為自動(dòng)內(nèi)存碎片清理機(jī)制設(shè)置了參數(shù)。
我們可以通過(guò)設(shè)置參數(shù),來(lái)控制碎片清理的開(kāi)始和結(jié)束時(shí)機(jī),以及占用的 CPU 比例,從而減少碎片清理對(duì) Redis 本身請(qǐng)求處理的性能影響。首先,Redis 需要啟用自動(dòng)內(nèi)存碎片清理,可以把 activedefrag 配置項(xiàng)設(shè)置為 yes
config?set?activedefrag?yes
這個(gè)命令只是啟用了自動(dòng)清理功能,但是,具體什么時(shí)候清理,會(huì)受到下面這兩個(gè)參數(shù)的控制。這兩個(gè)參數(shù)分別設(shè)置了觸發(fā)內(nèi)存清理的一個(gè)條件,如果同時(shí)滿(mǎn)足這兩個(gè)條件,就開(kāi)始清理。在清理的過(guò)程中,只要有一個(gè)條件不滿(mǎn)足了,就停止自動(dòng)清理。
- active-defrag-ignore-bytes 100mb:表示內(nèi)存碎片的字節(jié)數(shù)達(dá)到 100MB 時(shí),開(kāi)始清理;
- active-defrag-threshold-lower 10:表示內(nèi)存碎片空間占操作系統(tǒng)分配給 Redis 的總空間比例達(dá)到 10% 時(shí),開(kāi)始清理。
為了盡可能減少碎片清理對(duì) Redis 正常請(qǐng)求處理的影響,自動(dòng)內(nèi)存碎片清理功能在執(zhí)行時(shí),還會(huì)監(jiān)控清理操作占用的 CPU 時(shí)間,而且還設(shè)置了兩個(gè)參數(shù),分別用于控制清理操作占用的 CPU 時(shí)間比例的上、下限,既保證清理工作能正常進(jìn)行,又避免了降低 Redis 性能。這兩個(gè)參數(shù)具體如下:
- active-defrag-cycle-min 25:表示自動(dòng)清理過(guò)程所用 CPU 時(shí)間的比例不低于 25%,保證清理能正常開(kāi)展;
- active-defrag-cycle-max 75:表示自動(dòng)清理過(guò)程所用 CPU 時(shí)間的比例不高于 75%,一旦超過(guò),就停止清理,從而避免在清理時(shí),大量的內(nèi)存拷貝阻塞 Redis,導(dǎo)致響應(yīng)延遲升高。
自動(dòng)內(nèi)存碎片清理機(jī)制在控制碎片清理啟停的時(shí)機(jī)上,既考慮了碎片的空間占比、對(duì) Redis 內(nèi)存使用效率的影響,還考慮了清理機(jī)制本身的 CPU 時(shí)間占比、對(duì) Redis 性能的影響。而且,清理機(jī)制還提供了 4 個(gè)參數(shù),讓我們可以根據(jù)實(shí)際應(yīng)用中的數(shù)據(jù)量需求和性能要求靈活使用,建議你在實(shí)踐中好好地把這個(gè)機(jī)制用起來(lái)。
結(jié)尾
這篇文章主要介紹了
- 外在原因,內(nèi)在原因?qū)edis的影響,外內(nèi)在原因的調(diào)優(yōu)方案。
- Redis的變慢之后的排查思路,變慢的解決方案。
- 面試高頻:內(nèi)存占用率怎么那么高的一系列分析
每個(gè)知識(shí)點(diǎn)都是自己整理濃縮表達(dá)出來(lái)的,部分有些不容易懂的地方請(qǐng)及時(shí)指出,我們一起共同進(jìn)步!
非常歡迎大家加我個(gè)人微信有關(guān)后端方面的問(wèn)題我們?cè)谌簝?nèi)一起討論!?我們下期再見(jiàn)!
長(zhǎng)按上方掃碼二維碼,加我微信,拉你進(jìn)群

