Redis 忽然變慢了如何排查並解決?

碼哥位元組發表於2022-02-23

Redis 通常是我們業務系統中一個重要的元件,比如:快取、賬號登入資訊、排行榜等。

一旦 Redis 請求延遲增加,可能就會導致業務系統“雪崩”。

我在單身紅娘婚戀型別網際網路公司工作,在雙十一推出下單就送女朋友的活動。

誰曾想,凌晨 12 點之後,使用者量暴增,出現了一個技術故障,使用者無法下單,當時老大火冒三丈!

經過查詢發現 RedisCould not get a resource from the pool

獲取不到連線資源,並且叢集中的單臺 Redis 連線量很高。

大量的流量沒了 Redis 的快取響應,直接打到了 MySQL,最後資料庫也當機了……

於是各種更改最大連線數、連線等待數,雖然報錯資訊頻率有所緩解,但還是持續報錯

後來經過線下測試,發現存放 Redis 中的字元資料很大,平均 1s 返回資料

可以發現,一旦 Redis 延遲過高,會引發各種問題。

今天「碼哥」跟大家一起來分析下如何確定 Redis 有效能問題和解決方案。

[toc]

Redis 效能出問題了麼?

最大延遲是客戶端發出命令到客戶端收到命令的響應的時間,正常情況下 Redis 處理的時間極短,在微秒級別。

當 Redis 出現效能波動的時候,比如達到幾秒到十幾秒,這個很明顯我們可以認定 Redis 效能變慢了。

有的硬體配置比較高,當延遲 0.6ms,我們可能就認定變慢了。硬體比較差的可能 3 ms 我們才認為出現問題。

那我們該如何定義 Redis 真的變慢了呢?
所以,我們需要對當前環境的 Redis 基線效能做測量,也就是在一個系統在低壓力、無干擾情況下的基本效能。

當你發現 Redis 執行時時的延遲是基線效能的 2 倍以上,就可以判定 Redis 效能變慢了。

延遲基線測量

redis-cli 命令提供了–intrinsic-latency 選項,用來監測和統計測試期間內的最大延遲(以毫秒為單位),這個延遲可以作為 Redis 的基線效能。

redis-cli --latency -h `host` -p `port`

比如執行如下指令:

redis-cli --intrinsic-latency 100
Max latency so far: 4 microseconds.
Max latency so far: 18 microseconds.
Max latency so far: 41 microseconds.
Max latency so far: 57 microseconds.
Max latency so far: 78 microseconds.
Max latency so far: 170 microseconds.
Max latency so far: 342 microseconds.
Max latency so far: 3079 microseconds.
45026981 total runs (avg latency: 2.2209 microseconds / 2220.89 nanoseconds per run).
Worst run took 1386x longer than the average latency.

注意:引數100是測試將執行的秒數。我們執行測試的時間越長,我們就越有可能發現延遲峰值。

通常執行 100 秒通常是合適的,足以發現延遲問題了,當然我們可以選擇不同時間執行幾次,避免誤差。
「碼哥」執行的最大延遲是 3079 微秒,所以基線效能是 3079 (3 毫秒)微秒。

需要注意的是,我們要在 Redis 的服務端執行,而不是客戶端。這樣,可以避免網路對基線效能的影響

可以通過 -h host -p port 來連線服務端,如果想監測網路對 Redis 的效能影響,可以使用 Iperf 測量客戶端到服務端的網路延遲。

如果網路延遲幾百毫秒,說明網路可能有其他大流量的程式在執行導致網路擁塞,需要找運維協調網路的流量分配。

慢指令監控

如何判斷是否是慢指令呢?
看操作複雜度是否是O(N)官方文件對每個命令的複雜度都有介紹,儘可能使用O(1) 和 O(log N)命令。

涉及到集合操作的複雜度一般為O(N),比如集合全量查詢HGETALL、SMEMBERS,以及集合的聚合操作:SORTLREMSUNION等。

有監控資料可以觀測呢?程式碼不是我寫的,不知道有沒有人用了慢指令。
有兩種方式可以排查到:
  • 使用 Redis 慢日誌功能查出慢命令;
  • latency-monitor(延遲監控)工具。

此外,可以使用自己(top、htop、prstat 等)快速檢查 Redis 主程式的 CPU 消耗。如果 CPU 使用率很高而流量不高,通常表明使用了慢速命令。

慢日誌功能

Redis 中的 slowlog 命令可以讓我們快速定位到那些超出指定執行時間的慢命令,預設情況下命令若是執行時間超過 10ms 就會被記錄到日誌。

slowlog 只會記錄其命令執行的時間,不包含 io 往返操作,也不記錄單由網路延遲引起的響應慢。

我們可以根據基線效能來自定義慢命令的標準(配置成基線效能最大延遲的 2 倍),調整觸發記錄慢命令的閾值。

可以在 redis-cli 中輸入以下命令配置記錄 6 毫秒以上的指令:

redis-cli CONFIG SET slowlog-log-slower-than 6000

也可以在 Redis.config 配置檔案中設定,以微秒為單位。

想要檢視所有執行時間比較慢的命令,可以通過使用 Redis-cli 工具,輸入 slowlog get 命令檢視,返回結果的第三個欄位以微秒位單位顯示命令的執行時間。

假如只需要檢視最後 2 個慢命令,輸入 slowlog get 2 即可。

示例:獲取最近2個慢查詢命令
127.0.0.1:6381> SLOWLOG get 2
1) 1) (integer) 6
   2) (integer) 1458734263
   3) (integer) 74372
   4) 1) "hgetall"
      2) "max.dsp.blacklist"
2) 1) (integer) 5
   2) (integer) 1458734258
   3) (integer) 5411075
   4) 1) "keys"
      2) "max.dsp.blacklist"

以第一個 HGET 命令為例分析,每個 slowlog 實體共 4 個欄位:

  • 欄位 1:1 個整數,表示這個 slowlog 出現的序號,server 啟動後遞增,當前為 6。
  • 欄位 2:表示查詢執行時的 Unix 時間戳。
  • 欄位 3:表示查詢執行微秒數,當前是 74372 微秒,約 74ms。
  • 欄位 4: 表示查詢的命令和引數,如果引數很多或很大,只會顯示部分並給數引數個數。
    當前命令是hgetall max.dsp.blacklist

Latency Monitoring

Redis 在 2.8.13 版本引入了 Latency Monitoring 功能,用於以秒為粒度監控各種事件的發生頻率。

啟用延遲監視器的第一步是設定延遲閾值(單位毫秒)。只有超過該閾值的時間才會被記錄,比如我們根據基線效能(3ms)的 3 倍設定閾值為 9 ms。

可以用 redis-cli 設定也可以在 Redis.config 中設定;

CONFIG SET latency-monitor-threshold 9

工具記錄的相關事件的詳情可檢視官方文件:https://redis.io/topics/laten...

如獲取最近的 latency

127.0.0.1:6379> debug sleep 2
OK
(2.00s)
127.0.0.1:6379> latency latest
1) 1) "command"
   2) (integer) 1645330616
   3) (integer) 2003
   4) (integer) 2003
  1. 事件的名稱;
  2. 事件發生的最新延遲的 Unix 時間戳;
  3. 毫秒為單位的時間延遲;
  4. 該事件的最大延遲。

如何解決 Redis 變慢?

Redis 的資料讀寫由單執行緒執行,如果主執行緒執行的操作時間太長,就會導致主執行緒阻塞。

一起分析下都有哪些操作會阻塞主執行緒,我們又該如何解決?

網路通訊導致的延遲

客戶端使用 TCP/IP 連線或 Unix 域連線連線到 Redis。1 Gbit/s 網路的典型延遲約為 200 us。

redis 客戶端執行一條命令分 4 個過程:

傳送命令-〉 命令排隊 -〉 命令執行-〉 返回結果
這個過程稱為 Round trip time(簡稱 RTT, 往返時間),mget mset 有效節約了 RTT,但大部分命令(如 hgetall,並沒有 mhgetall)不支援批量操作,需要消耗 N 次 RTT ,這個時候需要 pipeline 來解決這個問題。

Redis pipeline 將多個命令連線在一起來減少網路響應往返次數。

redis-pipeline

慢指令導致的延遲

根據上文的慢指令監控查詢文件,查詢到慢查詢指令。可以通過以下兩種方式解決:

  • 比如在 Cluster 叢集中,將聚合運算等 O(N) 操作執行在 slave 上,或者在客戶端完成。
  • 使用高效的命令代替。使用增量迭代的方式,避免一次查詢大量資料,具體請檢視SCANSSCANHSCANZSCAN命令。

除此之外,生產中禁用KEYS 命令,它只適用於除錯。因為它會遍歷所有的鍵值對,所以操作延時高。

Fork 生成 RDB 導致的延遲

生成 RDB 快照,Redis 必須 fork 後臺程式。fork 操作(在主執行緒中執行)本身會導致延遲。

Redis 使用作業系統的多程式寫時複製技術 COW(Copy On Write) 來實現快照持久化,減少記憶體佔用。

寫時複製技術保證快照期間資料客修改

但 fork 會涉及到複製大量連結物件,一個 24 GB 的大型 Redis 例項需要 24 GB / 4 kB * 8 = 48 MB 的頁表。

執行 bgsave 時,這將涉及分配和複製 48 MB 記憶體。

此外,從庫載入 RDB 期間無法提供讀寫服務,所以主庫的資料量大小控制在 2~4G 左右,讓從庫快速的載入完成

記憶體大頁(transparent huge pages)

常規的記憶體頁是按照 4 KB 來分配,Linux 核心從 2.6.38 開始支援記憶體大頁機制,該機制支援 2MB 大小的記憶體頁分配。

Redis 使用了 fork 生成 RDB 做持久化提供了資料可靠性保證

當生成 RDB 快照的過程中,Redis 採用寫時複製技術使得主執行緒依然可以接收客戶端的寫請求。

也就是當資料被修改的時候,Redis 會複製一份這個資料,再進行修改。

採用了記憶體大頁,生成 RDB 期間,即使客戶端修改的資料只有 50B 的資料,Redis 需要複製 2MB 的大頁。當寫的指令比較多的時候就會導致大量的拷貝,導致效能變慢。

使用以下指令禁用 Linux 記憶體大頁即可:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

swap:作業系統分頁

當實體記憶體(記憶體條)不夠用的時候,將部分記憶體上的資料交換到 swap 空間上,以便讓系統不會因記憶體不夠用而導致 oom 或者更致命的情況出現。

當某程式向 OS 請求記憶體發現不足時,OS 會把記憶體中暫時不用的資料交換出去,放在 SWAP 分割槽中,這個過程稱為 SWAP OUT。

當某程式又需要這些資料且 OS 發現還有空閒實體記憶體時,又會把 SWAP 分割槽中的資料交換回實體記憶體中,這個過程稱為 SWAP IN。

記憶體 swap 是作業系統裡將記憶體資料在記憶體和磁碟間來回換入和換出的機制,涉及到磁碟的讀寫。

觸發 swap 的情況有哪些呢?
對於 Redis 而言,有兩種常見的情況:
  • Redis 使用了比可用記憶體更多的記憶體;
  • 與 Redis 在同一機器執行的其他程式在執行大量的檔案讀寫 I/O 操作(包括生成大檔案的 RDB 檔案和 AOF 後臺執行緒),檔案讀寫佔用記憶體,導致 Redis 獲得的記憶體減少,觸發了 swap。
碼哥,我要如何排查是否因為 swap 導致的效能變慢呢?
Linux 提供了很好的工具來排查這個問題,所以當懷疑由於交換導致的延遲時,只需按照以下步驟排查。

獲取 Redis 例項 pid

$ redis-cli info | grep process_id
process_id:13160

進入此程式的 /proc 檔案系統目錄:

cd /proc/13160

在這裡有一個 smaps 的檔案,該檔案描述了 Redis 程式的記憶體佈局,執行以下指令,用 grep 查詢所有檔案中的 Swap 欄位。

$ cat smaps | egrep '^(Swap|Size)'
Size:                316 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                  8 kB
Swap:                  0 kB
Size:                 40 kB
Swap:                  0 kB
Size:                132 kB
Swap:                  0 kB
Size:             720896 kB
Swap:                 12 kB

每行 Size 表示 Redis 例項所用的一塊記憶體大小,和 Size 下方的 Swap 對應這塊 Size 大小的記憶體區域有多少資料已經被換出到磁碟上了。

如果 Size == Swap 則說明資料被完全換出了。

可以看到有一個 720896 kB 的記憶體大小有 12 kb 被換出到了磁碟上(僅交換了 12 kB),這就沒什麼問題。

Redis 本身會使用很多大小不一的記憶體塊,所以,你可以看到有很多 Size 行,有的很小,就是 4KB,而有的很大,例如 720896KB。不同記憶體塊被換出到磁碟上的大小也不一樣。

敲重點了

如果 Swap 一切都是 0 kb,或者零星的 4k ,那麼一切正常。

當出現百 MB,甚至 GB 級別的 swap 大小時,就表明,此時,Redis 例項的記憶體壓力很大,很有可能會變慢。

解決方案

  1. 增加機器記憶體;
  2. 將 Redis 放在單獨的機器上執行,避免在同一機器上執行需要大量記憶體的程式,從而滿足 Redis 的記憶體需求;
  3. 增加 Cluster 叢集的數量分擔資料量,減少每個例項所需的記憶體。

AOF 和磁碟 I/O 導致的延遲

為了保證資料可靠性,Redis 使用 AOF 和 RDB 快照實現當即快速恢復和持久化

可以使用 appendfsync 配置將 AOF 配置為以三種不同的方式在磁碟上執行 write 或者 fsync (可以在執行時使用 CONFIG SET命令修改此設定,比如:redis-cli CONFIG SET appendfsync no)。

  • no:Redis 不執行 fsync,唯一的延遲來自於 write 呼叫,write 只需要把日誌記錄寫到核心緩衝區就可以返回。
  • everysec:Redis 每秒執行一次 fsync。使用後臺子執行緒非同步完成 fsync 操作。最多丟失 1s 的資料。
  • always:每次寫入操作都會執行 fsync,然後用 OK 程式碼回覆客戶端(實際上 Redis 會嘗試將同時執行的許多命令聚集到單個 fsync 中),沒有資料丟失。在這種模式下,效能通常非常低,強烈建議使用快速磁碟和可以在短時間內執行 fsync 的檔案系統實現。

我們通常將 Redis 用於快取,資料丟失完全惡意從資料獲取,並不需要很高的資料可靠性,建議設定成 no 或者 everysec。

除此之外,避免 AOF 檔案過大, Redis 會進行 AOF 重寫,生成縮小的 AOF 檔案。

可以把配置項 no-appendfsync-on-rewrite設定為 yes,表示在 AOF 重寫時,不進行 fsync 操作。

也就是說,Redis 例項把寫命令寫到記憶體後,不呼叫後臺執行緒進行 fsync 操作,就直接返回了。

expires 淘汰過期資料

Redis 有兩種方式淘汰過期資料:

  • 惰性刪除:當接收請求的時候發現 key 已經過期,才執行刪除;
  • 定時刪除:每 100 毫秒刪除一些過期的 key。

定時刪除的演算法如下:

  1. 隨機取樣 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP個數的 key,刪除所有過期的 key;
  2. 如果發現還有超過 25% 的 key 已過期,則執行步驟一。

ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP預設設定為 20,每秒執行 10 次,刪除 200 個 key 問題不大主。

如果觸發了第二條,就會導致 Redis 一致在刪除過期資料取釋放記憶體。而刪除是阻塞的。

碼哥,觸發條件是什麼呀?
也就是大量的 key 設定了相同的時間引數。同一秒內,大量 key 過期,需要重複刪除多次才能降低到 25% 以下。

簡而言之:大量同時到期的 key 可能會導致效能波動。

解決方案

如果一批 key 的確是同時過期,可以在 EXPIREATEXPIRE 的過期時間引數上,加上一個一定大小範圍內的隨機數,這樣,既保證了 key 在一個鄰近時間範圍內被刪除,又避免了同時過期造成的壓力。

bigkey

通常我們會將含有較大資料或含有大量成員、列表數的 Key 稱之為大 Key,下面我們將用幾個實際的例子對大 Key 的特徵進行描述:

  • 一個 STRING 型別的 Key,它的值為 5MB(資料過大)
  • 一個 LIST 型別的 Key,它的列表數量為 10000 個(列表數量過多)
  • 一個 ZSET 型別的 Key,它的成員數量為 10000 個(成員數量過多)
  • 一個 HASH 格式的 Key,它的成員數量雖然只有 1000 個但這些成員的 value 總大小為 10MB(成員體積過大)

bigkey 帶來一問題如下:

  1. Redis 記憶體不斷變大引發 OOM,或者達到 maxmemory 設 置值引發寫阻塞或重要 Key 被逐出;
  2. Redis Cluster 中的某個 node 記憶體遠超其餘 node,但因 Redis Cluster 的資料遷移最小粒度為 Key 而無法將 node 上的記憶體均衡化;
  3. bigkey 的讀請求佔用過大頻寬,自身變慢的同時影響到該伺服器上的其它服務;
  4. 刪除一個 bigkey 造成主庫較長時間的阻塞並引發同步中斷或主從切換;

查詢 bigkey

使用 redis-rdb-tools 工具以定製化方式找出大 Key。

解決方案

對大 key 拆分

如將一個含有數萬成員的 HASH Key 拆分為多個 HASH Key,並確保每個 Key 的成員數量在合理範圍,在 Redis Cluster 結構中,大 Key 的拆分對 node 間的記憶體平衡能夠起到顯著作用。

非同步清理大 key

Redis 自 4.0 起提供了 UNLINK 命令,該命令能夠以非阻塞的方式緩慢逐步的清理傳入的 Key,通過 UNLINK,你可以安全的刪除大 Key 甚至特大 Key。

總結

如下檢查清單,幫助你在遇到 Redis 效能變慢的時候能高效解決問題。

  1. 獲取當前 Redis 的基線效能;
  2. 開啟慢指令監控,定位慢指令導致的問題;
  3. 找到慢指令,使用 scan 的方式;
  4. 將例項的資料大小控制在 2-4GB,避免主從複製載入過大 RDB 檔案而阻塞;
  5. 禁用記憶體大頁,採用了記憶體大頁,生成 RDB 期間,即使客戶端修改的資料只有 50B 的資料,Redis 需要複製 2MB 的大頁。當寫的指令比較多的時候就會導致大量的拷貝,導致效能變慢。
  6. Redis 使用的記憶體是否過大導致 swap;
  7. AOF 配置是否合理,可以將配置項 no-appendfsync-on-rewrite 設定為 yes,避免 AOF 重寫和 fsync 競爭磁碟 IO 資源,導致 Redis 延遲增加。
  8. bigkey 會帶來一些列問題,我們需要進行拆分防止出現 bigkey,並通過 UNLINK 非同步刪除。

相關文章