Redis 是一種記憶體資料庫,将資料儲存在記憶體中,讀寫效率要比傳統的将資料儲存在磁盤上的資料庫要快很多。但是 Redis 也會發生延遲時,這是就需要我們對其産生原因有深刻的了解,以便于快速排查問題,解決 Redis的延遲問題
一條指令執行過程
在本文場景下,延遲 (latency) 是指從用戶端發送指令到用戶端接收到指令傳回值的時間間隔。是以我們先來看一下 Redis 一條指令執行的步驟,其中每個步驟出問題都可能導緻高延遲。

上圖是 Redis 用戶端發送一條指令的執行過程示意圖,綠色的是執行步驟,而藍色的則是可能出現的導緻高延遲的原因。
網絡連接配接限制、網絡傳輸速率和CPU性能等是所有服務端都可能産生的性能問題。但是 Redis 有自己獨有的可能導緻高延遲的問題:指令或者資料結構誤用、持久化阻塞和記憶體交換。
而且更為緻命的是,Redis 采用單線程和事件驅動的機制來處理網絡請求,分别有對應的連接配接應答處理器,指令請求處理器和指令回複處理器來處理用戶端的網絡請求事件,處理完一個事件就繼續處理隊列中的下一個。一條指令處理出現了高延遲會影響接下來處于排隊狀态的其他指令。有關 Redis 事件處理機制的可以參考
本篇文章。
對于高延遲,Redis 原生提供慢查詢統計功能,執行 slowlog get {n} 指令可以擷取最近的 n 條慢查詢指令,預設對于執行超過10毫秒(可配置)的指令都會記錄到一個定長隊列中,線上執行個體建議設定為1毫秒便于及時發現毫秒級以上的指令。
# 超過 slowlog-log-slower-than 門檻值的指令都會被記錄到慢查詢隊列中
# 隊列最大長度為 slowlog-max-len
slowlog-log-slower-than 10000
slowlog-max-len 128
如果指令執行時間在毫秒級,則執行個體實際OPS隻有1000左右。慢查詢隊列長度預設128,可适當調大。慢查詢本身隻記錄了指令執行時間,不包括資料網絡傳輸時間和指令排隊時間,是以用戶端發生阻塞異常 後,可能不是目前指令緩慢,而是在等待其他指令執行。需要重點比對異常和慢查詢發生的時間點,确認是否有慢查詢造成的指令阻塞排隊。
slowlog的輸出格式如下所示。第一個字段表示該條記錄在所有慢日志中的序号,最新的記錄被展示在最前面;第二個字段是這條記錄被記錄時的系統時間,可以用 date 指令來将其轉換為友好的格式第三個字段表示這條指令的響應時間,機關為 us (微秒);第四個字段為對應的 Redis 操作。
> slowlog get
1) 1) (integer) 26
2) (integer) 1450253133
3) (integer) 43097
4) 1) "flushdb"
下面我們就來依次看一下不合理地使用指令或者資料結構、持久化阻塞和記憶體交換所導緻的高延遲問題。
不合理的指令或者資料結構
一般來說 Redis 執行指令速度都非常快,但是當資料量達到一定級别時,某些指令的執行就會花費大量時間,比如對一個包含上萬個元素的 hash 結構執行 hgetall 操作,由于資料量比較大且指令算法複雜度是 O(n),這條指令執行速度必然很慢。
這個問題就是典型的不合理使用指令和資料結構。對于高并發的場景我們應該盡量避免在大對象上執行算法複雜度超過 O(n) 的指令。對于鍵值較多的 hash 結構可以使用 scan 系列指令來逐漸周遊,而不是直接使用 hgetall 來全部擷取。
Redis 本身提供發現大對象的工具,對應指令:redis-cli-h {ip} -p {port} bigkeys。這條指令會使用 scan 從指定的 Redis DB 中持續采樣,實時輸出當時得到的 value 占用空間最大的 key 值,并在最後給出各種資料結構的 biggest key 的總結報告。
> redis-cli -h host -p 12345 --bigkeys
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
[00.00%] Biggest hash found so far 'idx:user' with 1 fields
[00.00%] Biggest hash found so far 'idx:product' with 3 fields
[00.00%] Biggest hash found so far 'idx:order' with 14 fields
[02.29%] Biggest hash found so far 'idx:fund' with 16 fields
[02.29%] Biggest hash found so far 'idx:pay' with 69 fields
[04.45%] Biggest set found so far 'indexed_word_set' with 1482 members
[05.93%] Biggest hash found so far 'idx:address' with 159 fields
[11.79%] Biggest hash found so far 'idx:reply' with 196 fields
-------- summary -------
Sampled 1484 keys in the keyspace!
Total key length in bytes is 13488 (avg len 9.09)
Biggest set found 'indexed_word_set' has 1482 members
Biggest hash found 'idx:的' has 196 fields
0 strings with 0 bytes (00.00% of keys, avg size 0.00)
0 lists with 0 items (00.00% of keys, avg size 0.00)
2 sets with 1710 members (00.13% of keys, avg size 855.00)
1482 hashs with 6731 fields (99.87% of keys, avg size 4.54)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
持久化阻塞
對于開啟了持久化功能的Redis節點,需要排查是否是持久化導緻的阻 塞。持久化引起主線程阻塞的操作主要有:fork 阻塞、AOF刷盤阻塞。
fork 操作發生在 RDB 和 AOF 重寫時,Redis 主線程調用 fork 操作産生共享記憶體的子程序,由子程序完成對應的持久化工作。如果 fork 操作本身耗時過長,必然會導緻主線程的阻塞。
Redis 執行 fork 操作産生的子程序記憶體占用量表現為與父程序相同,理論上需要一倍的實體記憶體來完成相應的操作。但是 Linux 具有寫時複制技術 (copy-on-write),父子程序會共享相同的實體記憶體頁,當父程序處理寫請求時會對需要修改的頁複制出一份副本完成寫操作,而子程序依然讀取 fork 時整個父程序的記憶體快照。是以,一般來說,fork 不會消耗過多時間。
可以執行
info stats
指令擷取到 latest_fork_usec 名額,表示 Redis 最近一次 fork 操作耗時,如果耗時很大,比如超過1秒,則需要做出優化調整。
> redis-cli -c -p 7000 info | grep -w latest_fork_usec
latest_fork_usec:315
當我們開啟AOF持久化功能時,檔案刷盤的方式一般采用每秒一次,後 台線程每秒對AOF檔案做 fsync 操作。當硬碟壓力過大時,fsync 操作需要等待,直到寫入完成。如果主線程發現距離上一次的 fsync 成功超過2秒,為了資料安全性它會阻塞直到背景線程執行 fsync 操作完成。這種阻塞行為主要是硬碟壓力引起,可以檢視 Redis日志識别出這種情況,當發生這種阻塞行為時,會列印如下日志:
Asynchronous AOF fsync is taking too long (disk is busy). \
Writing the AOF buffer without waiting for fsync to complete, \
this may slow down Redis.
也可以檢視 info persistence 統計中的 aof_delayed_fsync 名額,每次發生 fdatasync 阻塞主線程時會累加。
>info persistence
loading:0
aof_pending_bio_fsync:0
aof_delayed_fsync:0
記憶體交換
記憶體交換(swap)對于 Redis 來說是非常緻命的,Redis 保證高性能的一個重要前提是所有的資料在記憶體中。如果作業系統把 Redis 使用的部分記憶體換出到硬碟,由于記憶體與硬碟讀寫速度差幾個數量級,會導緻發生交換後的 Redis 性能急劇下降。識别 Redis 記憶體交換的檢查方法如下:
>redis-cli -p 6383 info server | grep process_id # 查詢 redis 程序号
>cat /proc/4476/smaps | grep Swap # 查詢記憶體交換大小
Swap: 0 kB
Swap: 4 kB
Swap: 0 kB
Swap: 0 kB
如果交換量都是0KB或者個别的是4KB,則是正常現象,說明Redis程序記憶體沒有被交換。
有很多方法可以避免記憶體交換的發生。比如說:
- 保證機器充足的可用記憶體
- 確定所有Redis執行個體設定最大可用記憶體(maxmemory),防止極端情況下 Redis 記憶體不可控的增長。
- 降低系統使用swap優先級,如
echo10>/proc/sys/vm/swappiness