by shihang.mai
Redis 作為優(yōu)秀的內(nèi)存數(shù)據(jù)庫(kù),其擁有非常高的性能,單個(gè)實(shí)例的 OPS 能夠達(dá)到 10W 左右私爷。但也正因此如此,當(dāng)我們?cè)谑褂?Redis 時(shí)膊夹,如果發(fā)現(xiàn)操作延遲變大的情況衬浑,就會(huì)與我們的預(yù)期不符
1. 基準(zhǔn)測(cè)試
判斷redis是否變慢,肯定要先準(zhǔn)星基準(zhǔn)測(cè)試放刨,基準(zhǔn)測(cè)試可以用以下命令
# 測(cè)試出這個(gè)實(shí)例 60 秒內(nèi)的最大響應(yīng)延遲
redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60
# 每間隔 1 秒工秩,采樣 Redis 的平均操作耗時(shí)
redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1
2. redis變慢判斷
- 在相同配置的服務(wù)器上,測(cè)試一個(gè)正常 Redis 實(shí)例的基準(zhǔn)性能
- 找到你認(rèn)為可能變慢的 Redis 實(shí)例进统,測(cè)試這個(gè)實(shí)例的基準(zhǔn)性能
- 如果你觀察到助币,這個(gè)實(shí)例的運(yùn)行延遲是正常 Redis 基準(zhǔn)性能的 2 倍以上,即可認(rèn)為這個(gè) Redis 實(shí)例確實(shí)變慢了
3 redis慢原因
3.1 使用復(fù)雜度過(guò)高的命令
- 設(shè)置慢日志閾值
# 命令執(zhí)行耗時(shí)超過(guò) 5 毫秒螟碎,記錄慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 條慢日志
CONFIG SET slowlog-max-len 500
- 查看慢日志
# 獲取5條慢記錄
SLOWLOG get5
- 復(fù)雜度高的命令導(dǎo)致慢的原因
- 經(jīng)常使用 O(N) 以上復(fù)雜度的命令眉菱,例如 SORT、SUNION抚芦、ZUNIONSTORE 聚合類命令
原因:Redis 在操作內(nèi)存數(shù)據(jù)時(shí)倍谜,時(shí)間復(fù)雜度過(guò)高,要花費(fèi)更多的 CPU 資源 - 使用 O(N) 復(fù)雜度的命令叉抡,但 N 的值非常大
原因:Redis 一次需要返回給客戶端的數(shù)據(jù)過(guò)多尔崔,更多時(shí)間花費(fèi)在數(shù)據(jù)協(xié)議的組裝和網(wǎng)絡(luò)傳輸過(guò)程中
如果你的應(yīng)用程序操作 Redis 的 OPS (每秒操作的次數(shù))不是很大,但 Redis 實(shí)例的 CPU 使用率卻很高褥民,那么很有可能是使用了復(fù)雜度過(guò)高的命令導(dǎo)致的
- 解決方案
- 盡量不使用 O(N) 以上復(fù)雜度過(guò)高的命令季春,對(duì)于數(shù)據(jù)的聚合操作,放在客戶端做
- 執(zhí)行 O(N) 命令消返,保證 N 盡量的性嘏(推薦 N <= 300)耘拇,每次獲取盡量少的數(shù)據(jù),讓 Redis 可以及時(shí)處理返回
3.2 操作bigkey
bigKey定義:如果一個(gè) key 寫入的 value 非常大宇攻,那么 Redis 在分配內(nèi)存時(shí)就會(huì)比較耗時(shí)惫叛。同樣的,當(dāng)刪除這個(gè) key 時(shí)逞刷,釋放內(nèi)存也會(huì)比較耗時(shí)嘉涌,這種類型的 key 我們一般稱之為 bigkey
- 查看bigKey在實(shí)例中分布情況
redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01
使用這個(gè)命令的原理,就是 Redis 在內(nèi)部執(zhí)行了 SCAN 命令夸浅,遍歷整個(gè)實(shí)例中所有的 key仑最,然后針對(duì) key 的類型,分別執(zhí)行 STRLEN帆喇、LLEN警医、HLEN、SCARD坯钦、ZCARD 命令预皇,來(lái)獲取 String 類型的長(zhǎng)度、容器類型(List婉刀、Hash深啤、Set、ZSet)的元素個(gè)數(shù)
- 對(duì)線上實(shí)例進(jìn)行 bigkey 掃描時(shí)路星,Redis 的 OPS 會(huì)突增溯街,為了降低掃描過(guò)程中對(duì) Redis 的影響,最好控制一下掃描的頻率洋丐,指定 -i 參數(shù)即可呈昔,它表示掃描過(guò)程中每次掃描后休息的時(shí)間間隔,單位是秒
- 掃描結(jié)果中友绝,對(duì)于容器類型(List堤尾、Hash、Set迁客、ZSet)的 key郭宝,只能掃描出元素最多的 key。但一個(gè) key 的元素多掷漱,不一定表示占用內(nèi)存也多粘室,你還需要根據(jù)業(yè)務(wù)情況,進(jìn)一步評(píng)估內(nèi)存占用情況
- 解決方案
- 業(yè)務(wù)應(yīng)用盡量避免寫入 bigkey
- 如果你使用的 Redis 是 4.0 以上版本卜范,用 UNLINK 命令替代 DEL衔统,此命令可以把釋放 key 內(nèi)存的操作,放到后臺(tái)線程中去執(zhí)行,從而降低對(duì) Redis 的影響
- 如果你使用的 Redis 是 6.0 以上版本锦爵,可以開啟 lazy-free 機(jī)制(lazyfree-lazy-user-del = yes)舱殿,在執(zhí)行 DEL 命令時(shí),釋放內(nèi)存也會(huì)放到后臺(tái)線程中執(zhí)行
lazy-free的使用分為2類
主動(dòng)刪除
UNLINK,與DEL命令對(duì)應(yīng)
被動(dòng)刪除(TTL刪除和maxmemory key驅(qū)逐淘汰刪除)
- lazyfree-lazy-eviction no
針對(duì)redis內(nèi)存使用達(dá)到maxmeory险掀,并設(shè)置有淘汰策略時(shí)沪袭;在被動(dòng)淘汰鍵時(shí),是否采用lazy free機(jī)制 - lazyfree-lazy-expire no
針對(duì)設(shè)置有TTL的鍵樟氢,達(dá)到過(guò)期后枝恋,被redis清理刪除時(shí)是否采用lazy free機(jī)制 - lazyfree-lazy-server-del no
針對(duì)有些指令在處理已存在的鍵時(shí),會(huì)帶有一個(gè)隱式的DEL鍵的操作嗡害。如rename命令,當(dāng)目標(biāo)鍵已存在,redis會(huì)先刪除目標(biāo)鍵畦攘,如果這些目標(biāo)鍵是一個(gè)big key,那就會(huì)引入阻塞刪除的性能問(wèn)題 - slave-lazy-flush no
從庫(kù)接受完 rdb 文件后的 flush 操作
3.3 key集中過(guò)期
- 現(xiàn)象
變慢的時(shí)間點(diǎn)很有規(guī)律霸妹,例如某個(gè)整點(diǎn),或者每間隔多久就會(huì)發(fā)生一波延遲 - 變慢原因
Redis 的過(guò)期數(shù)據(jù)采用惰性刪除 + 定時(shí)主動(dòng)刪除兩種策略.這個(gè)定時(shí)主動(dòng)刪除過(guò)期 key 的任務(wù)知押,是在 Redis 主線程中執(zhí)行的
如果在執(zhí)行主動(dòng)過(guò)期的過(guò)程中叹螟,出現(xiàn)了需要大量刪除過(guò)期 key 的情況,那么此時(shí)應(yīng)用程序在訪問(wèn) Redis 時(shí)台盯,必須要等待這個(gè)過(guò)期任務(wù)執(zhí)行結(jié)束罢绽,Redis 才可以服務(wù)這個(gè)客戶端請(qǐng)求. - 查找
在代碼中查找設(shè)置過(guò)期時(shí)間的關(guān)鍵字 - 方案
- 集中過(guò)期 key 增加一個(gè)隨機(jī)過(guò)期時(shí)間牧挣,把集中過(guò)期的時(shí)間打散轴总,降低 Redis 清理過(guò)期 key 的壓力
- 如果你使用的 Redis 是 4.0 以上版本猴伶,可以開啟 lazy-free 機(jī)制惫确,當(dāng)刪除過(guò)期 key 時(shí)耘婚,把釋放內(nèi)存的操作放到后臺(tái)線程中執(zhí)行皿伺,避免阻塞主線程
3.4 實(shí)例內(nèi)存達(dá)到上限
- 變慢原因
當(dāng) Redis 內(nèi)存達(dá)到 maxmemory 后神得,每次寫入新的數(shù)據(jù)之前频轿,Redis 必須先從實(shí)例中踢出一部分?jǐn)?shù)據(jù)市咽,讓整個(gè)實(shí)例的內(nèi)存維持在 maxmemory 之下痊银,然后才能把新數(shù)據(jù)寫進(jìn)來(lái)
一般最常使用的是 allkeys-lru、volatile-lru 淘汰策略施绎,它們的處理邏輯是溯革,每次從實(shí)例中隨機(jī)取出一批 key(這個(gè)數(shù)量可配置),然后淘汰一個(gè)最少訪問(wèn)的 key谷醉,之后把剩下的 key 暫存到一個(gè)池子中致稀,繼續(xù)隨機(jī)取一批 key,并與之前池子中的 key 比較俱尼,再淘汰一個(gè)最少訪問(wèn)的 key豺裆。以此往復(fù),直到實(shí)例內(nèi)存降到 maxmemory 之下
Redis 的淘汰數(shù)據(jù)的邏輯與刪除過(guò)期 key 的一樣,也是在命令真正執(zhí)行之前執(zhí)行的臭猜,也就是說(shuō)它也會(huì)增加我們操作 Redis 的延遲
- 解決方案
- 避免存儲(chǔ) bigkey躺酒,降低釋放內(nèi)存的耗時(shí)
- 淘汰策略改為隨機(jī)淘汰,隨機(jī)淘汰比 LRU 要快很多(視業(yè)務(wù)情況調(diào)整)
- 拆分實(shí)例蔑歌,把淘汰 key 的壓力分?jǐn)偟蕉鄠€(gè)實(shí)例上
- 如果使用的是 Redis 4.0 以上版本羹应,開啟 layz-free 機(jī)制,把淘汰 key 釋放內(nèi)存的操作放到后臺(tái)線程中執(zhí)行(配置 lazyfree-lazy-eviction = yes)
3.5 fork耗時(shí)嚴(yán)重
- 現(xiàn)象
操作 Redis 延遲變大次屠,都發(fā)生在 Redis 后臺(tái) RDB 和 AOF rewrite 期間 - 變慢原因
- 當(dāng) Redis 開啟了后臺(tái) RDB 和 AOF rewrite 后园匹,在執(zhí)行時(shí),它們都需要主進(jìn)程創(chuàng)建出一個(gè)子進(jìn)程進(jìn)行數(shù)據(jù)的持久化劫灶。主進(jìn)程創(chuàng)建子進(jìn)程裸违,會(huì)調(diào)用操作系統(tǒng)提供的 fork 函數(shù)。
而 fork 在執(zhí)行過(guò)程中本昏,主進(jìn)程需要拷貝自己的內(nèi)存頁(yè)表給子進(jìn)程供汛,如果這個(gè)實(shí)例很大,那么這個(gè)拷貝的過(guò)程也會(huì)比較耗時(shí)涌穆。而且這個(gè) fork 過(guò)程會(huì)消耗大量的 CPU 資源怔昨,在完成 fork 之前,整個(gè) Redis 實(shí)例會(huì)被阻塞住宿稀,無(wú)法處理任何客戶端請(qǐng)求趁舀。 - 除了數(shù)據(jù)持久化會(huì)生成 RDB 之外,當(dāng)主從節(jié)點(diǎn)第一次建立數(shù)據(jù)同步時(shí)祝沸,主節(jié)點(diǎn)也創(chuàng)建子進(jìn)程生成 RDB矮烹,然后發(fā)給從節(jié)點(diǎn)進(jìn)行一次全量同步,所以罩锐,這個(gè)過(guò)程也會(huì)對(duì) Redis 產(chǎn)生性能影響
- 查找
INFO
# 上一次 fork 耗時(shí)擂送,單位微秒
latest_fork_usec:59477
- 解決方案
- 控制 Redis 實(shí)例的內(nèi)存:盡量在 10G 以下,執(zhí)行 fork 的耗時(shí)與實(shí)例大小有關(guān)唯欣,實(shí)例越大嘹吨,耗時(shí)越久
- 合理配置數(shù)據(jù)持久化策略:在 slave 節(jié)點(diǎn)執(zhí)行 RDB 備份,推薦在低峰期執(zhí)行境氢,而對(duì)于丟失數(shù)據(jù)不敏感的業(yè)務(wù)(例如把 Redis 當(dāng)做純緩存使用)蟀拷,可以關(guān)閉 AOF 和 AOF rewrite
- Redis 實(shí)例不要部署在虛擬機(jī)上:fork 的耗時(shí)也與系統(tǒng)也有關(guān),虛擬機(jī)比物理機(jī)耗時(shí)更久
- 降低主從庫(kù)全量同步的概率:適當(dāng)調(diào)大 repl-backlog-size 參數(shù)萍聊,避免主從全量同步
3.6 開啟內(nèi)存大頁(yè)
Linux 內(nèi)核從 2.6.38 開始问芬,支持了內(nèi)存大頁(yè)機(jī)制,該機(jī)制允許應(yīng)用程序以 2MB 大小為單位寿桨,向操作系統(tǒng)申請(qǐng)內(nèi)存頁(yè)(以前是4K)此衅,應(yīng)用程序每次向操作系統(tǒng)申請(qǐng)的內(nèi)存單位變大了强戴,但這也意味著申請(qǐng)內(nèi)存的耗時(shí)變長(zhǎng)
3.6.1 變慢原因
當(dāng) Redis 在執(zhí)行后臺(tái) RDB 和 AOF rewrite 時(shí),采用 fork 子進(jìn)程的方式來(lái)處理挡鞍。但主進(jìn)程 fork 子進(jìn)程后骑歹,此時(shí)的主進(jìn)程依舊是可以接收寫請(qǐng)求的,而進(jìn)來(lái)的寫請(qǐng)求墨微,會(huì)采用 Copy On Write(寫時(shí)復(fù)制)的方式操作內(nèi)存數(shù)據(jù)
主進(jìn)程在拷貝內(nèi)存數(shù)據(jù)時(shí)道媚,這個(gè)階段就涉及到新內(nèi)存的申請(qǐng),如果此時(shí)操作系統(tǒng)開啟了內(nèi)存大頁(yè)翘县,那么在此期間最域,客戶端即便只修改 10B 的數(shù)據(jù),Redis 在申請(qǐng)內(nèi)存時(shí)也會(huì)以 2MB 為單位向操作系統(tǒng)申請(qǐng)锈麸,申請(qǐng)內(nèi)存的耗時(shí)變長(zhǎng)镀脂,進(jìn)而導(dǎo)致每個(gè)寫請(qǐng)求的延遲增加,影響到 Redis 性能
3.6.2 查找
cat /sys/kernel/mm/transparent_hugepage/enabled
值:ALWAYS或者NEVER
3.6.3 解決方案
echo never > /sys/kernel/mm/transparent_hugepage/enabled
3.7 開啟AOF(沒(méi)理解)
- 變慢原因
當(dāng) Redis 后臺(tái)線程在執(zhí)行 AOF 文件刷盤時(shí)忘伞,如果此時(shí)磁盤的 IO 負(fù)載很高薄翅,那這個(gè)后臺(tái)線程在執(zhí)行刷盤操作(fsync系統(tǒng)調(diào)用)時(shí)就會(huì)被阻塞住。
此時(shí)的主線程依舊會(huì)接收寫請(qǐng)求虑省,緊接著,主線程又需要把數(shù)據(jù)寫到文件內(nèi)存中(write 系統(tǒng)調(diào)用)僧凰,但此時(shí)的后臺(tái)子線程由于磁盤負(fù)載過(guò)高探颈,導(dǎo)致 fsync 發(fā)生阻塞,遲遲不能返回训措,那主線程在執(zhí)行 write 系統(tǒng)調(diào)用時(shí)伪节,也會(huì)被阻塞住,直到后臺(tái)線程 fsync 執(zhí)行完成后绩鸣,主線程執(zhí)行 write 才能成功返回 - 解決方案
# AOF rewrite 期間怀大,AOF 后臺(tái)子線程不進(jìn)行刷盤操作
# 相當(dāng)于在這期間,臨時(shí)把 appendfsync 設(shè)置為了 none
no-appendfsync-on-rewrite yes
3.8 使用Swap
操作系統(tǒng)為了緩解內(nèi)存不足對(duì)應(yīng)用程序的影響呀闻,允許把一部分內(nèi)存中的數(shù)據(jù)換到磁盤上化借,以達(dá)到應(yīng)用程序?qū)?nèi)存使用的緩沖,這些內(nèi)存數(shù)據(jù)被換到磁盤上的區(qū)域捡多,就是 Swap
- 現(xiàn)象
發(fā)現(xiàn) Redis 突然變得非常慢蓖康,每次的操作耗時(shí)都達(dá)到了幾百毫秒甚至秒級(jí),那此時(shí)就需要檢查 Redis 是否使用到了 Swap,在這種情況下 Redis 基本上已經(jīng)無(wú)法提供高性能的服務(wù)了 - 變慢原因
當(dāng)內(nèi)存中的數(shù)據(jù)被換到磁盤上后垒手,Redis 再訪問(wèn)這些數(shù)據(jù)時(shí)蒜焊,就需要從磁盤上讀取,訪問(wèn)磁盤的速度要比訪問(wèn)內(nèi)存慢幾百倍 - 查找
# 先找到 Redis 的進(jìn)程 ID
$ ps -aux | grep redis-server
# 查看 Redis Swap 使用情況
$ cat /proc/$pid/smaps | egrep '^(Swap|Size)'
- 解決方案
- 增加機(jī)器的內(nèi)存科贬,讓 Redis 有足夠的內(nèi)存可以使用
3.9 碎片整理
這個(gè)開啟后會(huì)導(dǎo)致redis的性能下降