|
源 / 程序員歷小兵 Redis 是一種內(nèi)存數(shù)據(jù)庫,將數(shù)據(jù)保存在內(nèi)存中,讀寫效率要比傳統(tǒng)的將數(shù)據(jù)保存在磁盤上的數(shù)據(jù)庫要快很多。所以,監(jiān)控 Redis 的內(nèi)存消耗并了解 Redis 內(nèi)存模型對(duì)高效并長期穩(wěn)定使用 Redis 至關(guān)重要。 內(nèi)存使用統(tǒng)計(jì)通過 info memory 命令可以獲得 Redis 內(nèi)存相關(guān)的指標(biāo)。較為重要的指標(biāo)和解釋如下所示: 當(dāng) memfragmentationratio > 1 時(shí),說明有部分內(nèi)存并沒有用于數(shù)據(jù)存儲(chǔ),而是被內(nèi)存碎片所消耗,如果該值很大,說明碎片率嚴(yán)重。當(dāng) memfragmentationratio < 1 時(shí),這種情況一般出現(xiàn)在操作系統(tǒng)把 Redis 內(nèi)存交換 (swap) 到硬盤導(dǎo)致,出現(xiàn)這種情況要格外關(guān)注,由于硬盤速度遠(yuǎn)遠(yuǎn)慢于內(nèi)存,Redis 性能會(huì)變得很差,甚至僵死。 當(dāng) Redis 內(nèi)存超出可以獲得內(nèi)存時(shí),操作系統(tǒng)會(huì)進(jìn)行 swap,將舊的頁寫入硬盤。從硬盤讀寫大概比從內(nèi)存讀寫要慢5個(gè)數(shù)量級(jí)。used_memory 指標(biāo)可以幫助判斷 Redis 是否有被swap的風(fēng)險(xiǎn)或者它已經(jīng)被swap。 在 Redis Administration 一文 (鏈接在文末) 建議要設(shè)置和內(nèi)存一樣大小的交換區(qū),如果沒有交換區(qū),一旦 Redis 突然需要的內(nèi)存大于當(dāng)前操作系統(tǒng)可用內(nèi)存時(shí),Redis 會(huì)因?yàn)?out of memory 而被 Linix Kernel 的 OOM Killer 直接殺死。雖然當(dāng) Redis 的數(shù)據(jù)被換出 (swap out) 時(shí),Redis的性能會(huì)變差,但是總比直接被殺死的好。 Redis 使用 maxmemory 參數(shù)限制最大可用內(nèi)存。限制內(nèi)存的目的主要有:
maxmemory 限制的是 Redis 實(shí)際使用的內(nèi)存量,也就是 used_memory 統(tǒng)計(jì)項(xiàng)對(duì)應(yīng)的內(nèi)存。實(shí)際消耗的內(nèi)存可能會(huì)比 maxmemory 設(shè)置的大,要小心因?yàn)檫@部內(nèi)存導(dǎo)致 OOM。所以,如果你有 10GB 的內(nèi)存,最好將 maxmemory 設(shè)置為 8 或者 9G 內(nèi)存消耗劃分Redis 進(jìn)程內(nèi)消耗主要包括:自身內(nèi)存 + 對(duì)象內(nèi)存 + 緩沖內(nèi)存 + 內(nèi)存碎片,其中 Redis 空進(jìn)程自身內(nèi)存消耗非常少,通常 usedmemoryrss 在 3MB 左右時(shí),used_memory 一般在 800KB 左右,一個(gè)空的 Redis 進(jìn)程消耗內(nèi)存可以忽略不計(jì)。 對(duì)象內(nèi)存對(duì)象內(nèi)存是 Redis 內(nèi)存占用最大的一塊,存儲(chǔ)著用戶所有的數(shù)據(jù)。Redis 所有的數(shù)據(jù)都采用 key-value 數(shù)據(jù)類型,每次創(chuàng)建鍵值對(duì)時(shí),至少創(chuàng)建兩個(gè)類型對(duì)象:key 對(duì)象和 value 對(duì)象。對(duì)象內(nèi)存消耗可以簡單理解為這兩個(gè)對(duì)象的內(nèi)存消耗之和(還有類似過期之類的信息)。鍵對(duì)象都是字符串,在使用 Redis 時(shí)很容易忽略鍵對(duì)內(nèi)存消耗的影響,應(yīng)當(dāng)避免使用過長的鍵。 緩沖內(nèi)存緩沖內(nèi)存主要包括:客戶端緩沖、復(fù)制積壓緩沖區(qū)和 AOF 緩沖區(qū)。 客戶端緩沖指的是所有接入到 Redis 服務(wù)器 TCP 連接的輸入輸出緩沖。 輸入緩沖無法控制,最大空間為 1G,如果超過將斷開連接。而且輸入緩沖區(qū)不受 maxmemory 控制,假設(shè)一個(gè) Redis 實(shí)例設(shè)置了 maxmemory 為 4G,已經(jīng)存儲(chǔ)了 2G 數(shù)據(jù),但是如果此時(shí)輸入緩沖區(qū)使用了 3G,就已經(jīng)超出了 maxmemory 限制,可能導(dǎo)致數(shù)據(jù)丟失、鍵值淘汰或者 OOM。 輸入緩沖區(qū)過大主要是因?yàn)?Redis 的處理速度跟不上輸入緩沖區(qū)的輸入速度,并且每次進(jìn)入輸入緩沖區(qū)的命令包含了大量的 bigkey。 輸出緩沖通過參數(shù) client-output-buffer-limit 控制,其格式如下所示。
hard limit 是指一旦緩沖區(qū)大小達(dá)到了這個(gè)閾值,Redis 就會(huì)立刻關(guān)閉該連接。而 soft limit 和時(shí)間 duration 共同生效,比如說 soft time 為 64mb、duration 為 60,則只有當(dāng)緩沖區(qū)持續(xù) 60s 大于 64mb 時(shí),Redis 才會(huì)關(guān)閉該連接。 普通客戶端是除了復(fù)制和訂閱的客戶端之外的所有連接。Reids 對(duì)其的默認(rèn)配置是 client-output-buffer-limit normal 0 0 0 , Redis 并沒有對(duì)普通客戶端的輸出緩沖區(qū)做限制,一般普通客戶端的內(nèi)存消耗可以忽略不計(jì),但是當(dāng)有大量慢連接客戶端接入時(shí)這部分內(nèi)存消耗就不能忽略,可以設(shè)置 maxclients 做限制。特別當(dāng)使用大量數(shù)據(jù)輸出的命令且數(shù)據(jù)無法及時(shí)推送到客戶端時(shí),如 monitor 命令,容易造成 Redis 服務(wù)器內(nèi)存突然飆升。相關(guān)案例可以查看這篇文章美團(tuán)在Redis上踩過的一些坑-3.redis內(nèi)存占用飆升。 從客戶端用于主從復(fù)制,主節(jié)點(diǎn)會(huì)為每個(gè)從節(jié)點(diǎn)單獨(dú)建立一條連接用于命令復(fù)制,默認(rèn)配置為 client-output-buffer-limit slave 256mb 64mb 60。當(dāng)主從節(jié)點(diǎn)之間網(wǎng)絡(luò)延遲較高或主節(jié)點(diǎn)掛載大量從節(jié)點(diǎn)時(shí)這部分內(nèi)存消耗將占用很大一部分,建議主節(jié)點(diǎn)掛載的從節(jié)點(diǎn)不要多于 2 個(gè),主從節(jié)點(diǎn)不要部署在較差的網(wǎng)絡(luò)環(huán)境下,如異地跨機(jī)房環(huán)境,防止復(fù)制客戶端連接緩慢造成溢出。與主從復(fù)制相關(guān)的一共有兩類緩沖區(qū),一個(gè)是從客戶端輸出緩沖區(qū),另外一個(gè)是下面會(huì)介紹到的復(fù)制積壓緩沖區(qū)。 訂閱客戶端用于發(fā)布訂閱功能,連接客戶端使用單獨(dú)的輸出緩沖區(qū),默認(rèn)配置為 client-output-buffer-limit pubsub 32mb 8mb 60,當(dāng)訂閱服務(wù)的消息生產(chǎn)快于消費(fèi)速度時(shí),輸出緩沖區(qū)會(huì)產(chǎn)生積壓造成內(nèi)存空間溢出。 輸入輸出緩沖區(qū)在大流量場(chǎng)景中容易失控,造成 Redis 內(nèi)存不穩(wěn)定,需要重點(diǎn)監(jiān)控??梢远ㄆ趫?zhí)行 client list 命令,監(jiān)控每個(gè)客戶端的輸入輸出緩沖區(qū)大小和其他信息。
client list 命令執(zhí)行速度慢,客戶端較多時(shí)頻繁執(zhí)行存在阻塞redis的可能,所以一般可以先使用 info clients 命令獲取最大的客戶端緩沖區(qū)大小。
復(fù)制積壓緩沖區(qū)是Redis 在 2.8 版本后提供的一個(gè)可重用的固定大小緩沖區(qū),用于實(shí)現(xiàn)部分復(fù)制功能。根據(jù) repl-backlog-size 參數(shù)控制,默認(rèn) 1MB。對(duì)于復(fù)制積壓緩沖區(qū)整個(gè)主節(jié)點(diǎn)只有一個(gè),所有的從節(jié)點(diǎn)共享此緩沖區(qū)。因此可以設(shè)置較大的緩沖區(qū)空間,比如說 100MB,可以有效避免全量復(fù)制。 AOF 重寫緩沖區(qū):這部分空間用于在 Redis AOF 重寫期間保存最近的寫入命令。AOF 重寫緩沖區(qū)的大小用戶無法控制,取決于 AOF 重寫時(shí)間和寫入命令量,不過一般都很小。 Redis 內(nèi)存碎片Redis 默認(rèn)的內(nèi)存分配器采用 jemalloc,可選的分配器還有:glibc、tcmalloc。內(nèi)存分配器為了更好地管理和重復(fù)利用內(nèi)存,分配內(nèi)存策略一般采用固定范圍的內(nèi)存塊進(jìn)行分配。具體的分配策略后續(xù)會(huì)具體講解,但是 Redis 正常碎片率一般在 1.03 左右(為什么是這個(gè)值)。但是當(dāng)存儲(chǔ)的數(shù)據(jù)長度長度差異較大時(shí),以下場(chǎng)景容易出現(xiàn)高內(nèi)存碎片問題:
這部分內(nèi)容我們后續(xù)再詳細(xì)講解 jemalloc,因?yàn)榇罅康目蚣芏紩?huì)使用內(nèi)存分配器,比如說 Netty 等。 子進(jìn)程內(nèi)存消耗子進(jìn)程內(nèi)存消耗主要指執(zhí)行 AOF 重寫 或者進(jìn)行 RDB 保存時(shí) Redis 創(chuàng)建的子進(jìn)程內(nèi)存消耗。Redis 執(zhí)行 fork 操作產(chǎn)生的子進(jìn)程內(nèi)存占用量表現(xiàn)為與父進(jìn)程相同,理論上需要一倍的物理內(nèi)存來完成相應(yīng)的操作。但是 Linux 具有寫時(shí)復(fù)制技術(shù) (copy-on-write),父子進(jìn)程會(huì)共享相同的物理內(nèi)存頁,當(dāng)父進(jìn)程處理寫請(qǐng)求時(shí)會(huì)對(duì)需要修改的頁復(fù)制出一份副本完成寫操作,而子進(jìn)程依然讀取 fork 時(shí)整個(gè)父進(jìn)程的內(nèi)存快照。 如上圖所示,fork 時(shí)只拷貝 page table,也就是頁表。只有等到某一頁發(fā)生修改時(shí),才真正進(jìn)行頁的復(fù)制。 但是 Linux Kernel 在 2.6.38 內(nèi)存增加了 Transparent Huge Pages (THP) 機(jī)制,簡單理解,它就是讓頁大小變大,本來一頁為 4KB,開啟 THP 機(jī)制后,一頁大小為 2MB。它雖然可以加快 fork 速度( 要拷貝的頁的數(shù)量減少 ),但是會(huì)導(dǎo)致 copy-on-write 復(fù)制內(nèi)存頁的單位從 4KB 增大為 2MB,如果父進(jìn)程有大量寫命令,會(huì)加重內(nèi)存拷貝量,都是修改一個(gè)頁的內(nèi)容,但是頁單位變大了,從而造成過度內(nèi)存消耗。例如,以下兩個(gè)執(zhí)行 AOF 重寫時(shí)的內(nèi)存消耗日志:
這兩個(gè)日志出自同一個(gè) Redis 進(jìn)程,used_memory 總量是 1.5GB,子進(jìn)程執(zhí)行期間每秒寫命令量都在 200 左右。當(dāng)分別開啟和關(guān)閉 THP 時(shí),子進(jìn)程內(nèi)存消耗有天壤之別。所以,在高并發(fā)寫的場(chǎng)景下開啟 THP,子進(jìn)程內(nèi)存消耗可能是父進(jìn)程的數(shù)倍,造成機(jī)器物理內(nèi)存溢出。 所以說,Redis 產(chǎn)生的子進(jìn)程并不需要消耗 1 倍的父進(jìn)程內(nèi)存,實(shí)際消耗根據(jù)期間寫入命令量決定,所以需要預(yù)留一些內(nèi)存防止溢出。并且建議關(guān)閉系統(tǒng)的 THP,防止 copy-on-write 期間內(nèi)存過度消耗。不僅是 Redis,部署 MySQL 的機(jī)器一般也會(huì)關(guān)閉 THP。 |
|
|