Redis系列23:效能最佳化指南

Brand發表於2023-09-26

Redis系列1:深刻理解高效能Redis的本質
Redis系列2:資料持久化提高可用性
Redis系列3:高可用之主從架構
Redis系列4:高可用之Sentinel(哨兵模式)
Redis系列5:深入分析Cluster 叢集模式
追求效能極致:Redis6.0的多執行緒模型
追求效能極致:客戶端快取帶來的革命
Redis系列8:Bitmap實現億萬級資料計算
Redis系列9:Geo 型別賦能億級地圖位置計算
Redis系列10:HyperLogLog實現海量資料基數統計
Redis系列11:記憶體淘汰策略
Redis系列12:Redis 的事務機制
Redis系列13:分散式鎖實現
Redis系列14:使用List實現訊息佇列
Redis系列15:使用Stream實現訊息佇列
Redis系列16:聊聊布隆過濾器(原理篇)
Redis系列17:聊聊布隆過濾器(實踐篇)
Redis系列18:過期資料的刪除策略
Redis系列19:LRU記憶體淘汰演算法分析
Redis系列20:LFU記憶體淘汰演算法分析
Redis系列21:快取與資料庫的資料一致性討論
Redis系列22:Redis 的Pub/Sub能力

1 介紹

Redis是我們在業務開發中很重要的一個輔助,能夠極大提高我們系統的執行效率,為後端的儲存服務減少壓力,提升使用者使用體驗。
但是作為一個輔助提升速度的元件,如果自己存在請求延遲的情況,那將是一個巨大的災難,可能引起整條業務鏈路的雪崩。
在我以往的部落格裡面,也有過相應的案例,比如 《架構與思維:一次快取雪崩的災難覆盤》。
但在實際業務場景中,可能有更加複雜的原因導致Redis訪問效率變慢,下面我們詳細來分析下。

2 發現和監測Redis的慢執行

2.1 如何判斷Redis變慢了

在之前的章節中,我們根據官網的資料有過這樣的結論:
在較高的配置基準下(比如 8C 16G +),在連線數為0~10000的時候,最高QPS可達到120000。Redis以超過60000個連線為基準,仍然能夠在這些條件下維持50000個q/s,體現了超高的效能。下圖中橫軸是連線數,縱軸是QPS。
image

可見,Redis的效能和流量可抗性是極其高的,從客戶端request發出到接受到response,這個處理過程時間是極短的,一般是微秒級別。
而對比Redis出現效能瓶頸的時候,就會有比較異常的表現,如達到幾秒甚至幾十秒,這時候我們就可以認定 Redis 效能變差了,需要去最佳化。

感知到 Redis的變慢了,接下來我們要做的就是驗證和確認,這樣才能有針對性的進行最佳化。
我們透過以下方法進行驗證和分析。

2.1 基線延遲測試

redis-cli 提供了一個指令選項 --intrinsic-latency,用於監測和統計某個時間段內Redis的最大延遲。這個選項可以用來評估Redis本身的效能,並且可以作為Redis的基準效能。
使用--intrinsic-latency選項需要指定時長,例如120秒。在指定的時間段內,redis-cli會記錄每個秒級的最大延遲。這個最大延遲可以反映出Redis本身的效能,不受網路或其他外部因素的影響。
你可以透過以下命令使用--intrinsic-latency選項:

redis-cli --intrinsic-latency 120

執行該命令後,redis-cli會輸出在120秒內的最大延遲統計資訊。
需要注意的是,--intrinsic-latency選項從Redis的2.8.7版本開始支援。如果你使用的是較早的版本,可能無法使用該選項。

redis-cli --intrinsic-latency 120
Max latency so far: 5 microseconds.
Max latency so far: 14 microseconds.
Max latency so far: 33 microseconds.
Max latency so far: 52 microseconds.
Max latency so far: 70 microseconds.
Max latency so far: 130 microseconds.
Max latency so far: 272 microseconds.
Max latency so far: 879 microseconds.
Max latency so far: 1079 microseconds.
Max latency so far: 1665 microseconds.
Max latency so far: 1665 microseconds.

可以看到,當前執行的最大延遲是 1665 微秒,所以基線延遲的效能是 1665 (約 1.6 毫秒)微秒。
可以在終端上連線Redis的服務端進行測試,避免客戶端測試因為網路的影響導致差異較大。
可以透過 -h host -p port 來連線到服務端。

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

2.2 監控慢指令

在演算法設計中,最優的時間複雜度是O(1) 和 O(log N)。
一樣的 ,Redis官方也儘量自身的操作指令儘量的高效,儘量節省時間複雜度。但是涉及到集合操作、全量的複雜度一般為O(N),

  • 集合全量查詢:HGETALL、SMEMBERS
  • 集合的聚合操作:SORT、LREM、 SUNION
  • SORTEDSET型別命令:ZDELRANGEBYSCORE

排查是否使用了慢指令,可以用如下幾種方式:

  • 使用一些工具進行操作的延時監控,如 latency-monitor
  • 透過查詢 Redis 的慢日誌來分析執行慢查詢的操作(下面會詳細說到)

如果只是簡單判斷是否使用了慢速查詢,還可以使用命令 top、htop、prstat 等檢查 Redis 主程式的 CPU 消耗即可

2.3 監控慢日誌

在 Redis 中,slowlog 是一個用於查詢和記錄執行時間較長的命令的命令。它可以幫助你找出哪些命令的執行時間超過了設定的閾值,並且將這些命令記錄下來,以便後續分析和最佳化。
預設情況下命令執行時間超過 10ms 就會被記錄到日誌,這個只記錄執行時間,摒棄了 io 往返或者網路延遲引起的響應慢部分。
如果你想修改慢查詢的時間閾值,自定義慢查詢的耗時標準,可以執行如下命令:

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

這邊為什麼使用 3330 ,是因為我們採用上面基線延遲測試值的double。

slowlog 命令的基本語法如下:

slowlog get [count]

其中,count 是一個可選引數,表示要返回的 slow log 的數量。如果不指定 count,則預設返回最近的 slow log。
當執行 slowlog get 命令時,Redis 會返回最近的一些 slow log,每個 slow log 包含以下資訊:

  • unique ID:該 slow log 的唯一 ID。
  • 時間戳:該 slow log 記錄的時間戳。
  • 命令:執行了哪些命令。
  • 執行時間:執行該命令所花費的時間(以微秒為單位)。
    你可以透過 slowlog get 命令來檢視最近的 slow log,以便找出需要最佳化的命令。此外,你還可以透過 slowlog-max-len 引數來設定 slow log 的最大長度,以避免日誌過多佔用過多記憶體。
# 舉例:讀取最近2個慢查詢
127.0.0.1:6381> SLOWLOG get 2
1) 1) (integer) 17
   2) (integer) 1693641198
   3) (integer) 5427
   4) 1) "hgetall"
      2) "all.uer_info"
1) 1) (integer) 18
   2) (integer) 1693641217 
   3) (string) "GET"  
   4) (integer) 3771

第一個 HGET 命令,共 4 個欄位:

  • 欄位 1:代表 slowlog 出現的序號,服務啟動後遞增加碼,當前為 17。
  • 欄位 2:查詢時的 Unix 時間戳。
  • 欄位 3:查詢執行的時間數(微秒),當前值5472(5.472微秒) 比 3330多,所以被記錄下來。
  • 欄位 4: 表示查詢的命令和引數, 如 hgetall all.user_info。
    這樣的做法是輸出慢查詢,提供開發同學排查的方向。

3 解決Redis慢執行問題

根據我們之前的知識,Redis 的資料讀寫這個主操作是由單執行緒執行,如果被影響,導致阻塞,那麼效能就會大大降低。
所以,如何避免主執行緒阻塞,是我們解決Redis慢執行的一個關鍵因素。以下從一個方向

3.1 網路通訊延遲

網際網路時代,客戶端訪問Redis服務端的時候很可能回到網路延遲較高,這樣,客戶端連線並請求的的效率就會變低。
如果是多個資料中心或者多網路分割槽的場景(兩地三中心、異地多活),這種情況就更明顯,畢竟資料在網路中傳輸都是視距離時延的。
透過 TCP/IP 連線或 Unix 域連線連線到 Redis,1 Gbit/s 的網路延遲大約為 200 us。
從下面這個圖看到往返時間RTT(Round trip time),執行過程為:

  • 傳送命令
  • 命令進入佇列,待執行
  • 執行命令
  • 返回執行結果
    image
    上面的圖明顯有效節約了RTT的次數,提高了效率。類似MGET和MSET等命令是Redis中用於批次操作多個key-value的命令,而Redis的很多其他命令,
    如 hgetall,不支援批次操作,需要消耗 N 次 RTT ,這個時候需要 pipeline 來解決這個問題。
    Redis pipeline 將多個命令連線在一起來減少網路響應往返次數。

3.2 慢指令導致的延遲

我們上面與分析過慢指令,在演算法設計中,最優的時間複雜度是O(1) 和 O(log N)。所以當我們確認了有慢查詢指令。可以透過以下兩種方式解決:

  • 在 Cluster 叢集中,複雜度高於O(1) 和 O(log N)的操作使用Slava從庫執行,大部分複雜執行都是非寫的,所以應該要避免阻塞主執行緒。當然,在Client執行也是可以的。
  • 使用高效命令代替慢執行的命令,同時避免單次查詢大量資料,採用增量獲取的辦法(參考 SCAN、SSCAN、HSCAN、ZSCAN)。
  • 生產環境中禁用KEYS 命令,它在除錯的時候會遍歷所有的鍵值對,操作延時較高。

3.3 Fork 生成 RDB 導致的延遲

我們在資料持久化的篇章中,說過資料持久化的一些辦法,包括RDB記憶體快照和AOF日誌。使用的不合理,也會照成Redis的效能大打折扣。
生成 RDB 快照(參考這篇 《Redis系列2:資料持久化提高可用性》),Redis 必須 fork 後臺程式,
做了fork操作之後,實際是拆分了執行出去,因為在主執行緒中執行,所以會導致效能降低,操作延遲。
在Redis 中可以使用作業系統的 COW(Copy On Write,多程式寫時複製技術) 來快速持久化,減少記憶體佔用。
image
Redis在執行 bgsave 時,涉及到記憶體和磁碟的分配和複製,並且從庫載入 RDB 期間無法提供讀寫服務,為了保障高效率,
主庫的資料量大小盡量控制在 2~4G 左右,超過4G,會讓從庫載入效率變慢,從而影響業務操作。

3.4 AOF 檔案系統或 RDB 大記憶體頁問題,包括 AOF 持久化阻塞、大記憶體頁等

Linux 2.6.38 版本之後開始支援記憶體大頁,最大可以支援2MB的記憶體頁分配,而之前的常規頁是按照4KB來分配的。
這樣的話,會導致Redis本身的一些持久化操作的問題,(參考這篇 《Redis系列2:資料持久化提高可用性》)。

  • Redis 使用了 fork 生成 RDB 持久化, COW在資料被修改的時候,會複製一份資料。記憶體頁太大的話,即使客戶端修改的量很小,也會複製2MB的大頁記憶體,導致效能變慢。
  • AOF 日誌儲存了 Redis 伺服器的順序指令序列,AOF 日誌只記錄對記憶體進行修改的指令記錄。如果記憶體頁那太大,單次重放(replay)的內容過多,也會導致效能變慢

可以使用指令來disable Linux 記憶體大頁:

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

3.5 作業系統 Swap 操作問題,包括記憶體磁碟資料轉換、記憶體清理等

Swap是作業系統中的一種虛擬記憶體技術,用於在實體記憶體不足時將一部分資料從實體記憶體中移動到硬碟上,以釋放實體記憶體空間,避免系統因為記憶體不夠而導致的 oom 情況。Swap操作包括“換出”(swap out)和“換入”(swap in)兩個過程。

  • Swap out是指將一部分資料從實體記憶體中移動到交換空間(swap space)中,以釋放實體記憶體空間。當作業系統需要更多的記憶體空間時,它會根據一定的演算法來決定哪些資料應該被移動到交換空間中。這些資料通常是最近最少使用的或者最不可能被再次使用的。
  • Swap in是指將一部分資料從交換空間中移動到實體記憶體中。當作業系統需要訪問被移動到交換空間中的資料時,它會根據一定的演算法來決定哪些資料應該被移動到實體記憶體中。這些資料通常是最近被訪問的或者最可能被再次訪問的。

Swap操作可以幫助作業系統在實體記憶體不足時繼續執行程式,但是過度的swap操作可能會導致系統效能下降,因為硬碟訪問速度比實體記憶體慢得多。因此,合理配置和管理swap空間對於系統效能最佳化非常重要。
通常建議將swap空間設定為實體記憶體的1.5到2倍,並且應該避免在swap空間中頻繁進行大量的讀寫操作。

既然swap的操作引發是由於記憶體空間不夠導致的操作,那看看我們的Redis裡面有哪些操作會引起這類行為:

  • 使用超額的記憶體,比可用記憶體更多
  • 再比如上面說的那幾點:RDB Fork操作以及大檔案生成,AOF日誌記錄和指令同步。都可能導致大量的記憶體佔用,觸發了 swap。

3.5.1 對swap導致的效能問題進行排查

#  獲取Redis 例項的程式ID(pid):
$ redis-cli info | grep process_id
process_id:12893

#  根據程式進入 /proc 檔案系統目錄:
cd /proc/12893

# 開啟 smaps 的檔案,查詢所有檔案中所有的 swap 操作:
# 這邊可以看到,當使用記憶體達到810554 kb 時,swap記憶體達到37kb。
$ cat smaps | egrep '^(Swap|Size)'
Size:                241 kB
Swap:                  0 kB
Size:                172 kB
Swap:                  0 kB
Size:           810554 kB
Swap:                37 kB

如果 Swap 是 0 kb,或者小量的kb,應該都是正常的。但當出現百 M,甚至 GB 級別的 swap 大小時,記憶體就明顯吃緊了,大機率會導致線上請求變慢。
可以用如下辦法進行解決:

  • 資源補充:記憶體進行擴容。
  • 單一職責原則:Redis在獨佔的主機上執行,避免其他高記憶體佔用服務的資源搶佔。
  • 分治理念:增加 Cluster 叢集的數量進行分擔,減少單個例項所需的記憶體消耗。

3.6 AOF 的寫回策略為 always,導致每個操作都要同步刷回磁碟

為了保證資料可靠性,Redis 使用 AOF 和 RDB 快照實現快速恢復和持久化。
(參考這篇 《Redis系列2:資料持久化提高可用性》)

AOF(Append-Only File)作為Redis用於快取持久化的一種方式,當你選擇 "always" 作為 AOF 的寫回策略時,這意味著每一個寫操作都會被立即同步到磁碟中。
雖然這種策略可以最大限度地保證資料的安全性,但是它對效能的影響也是最大的,因為每次寫操作都需要等待磁碟 I/O 完成。如果你的應用可以接受一點點資料丟失的風險,或者你的應用主要是讀操作,你可能會想要調整 AOF 的寫回策略以提高效能。

以下是幾種可能的解決方案:

  1. 調整 AOF 寫回策略:Redis 提供了幾種 AOF 寫回策略,包括 "always"(每個操作都立即寫回)、"everysec"(每秒寫回一次)、"no"(由作業系統決定何時寫回)。如果你的應用可以接受一些資料丟失的風險,你可以嘗試將 AOF 寫回策略調整為 "everysec" 或 "no"。
  2. 使用 RDB 持久化:與 AOF 不同,RDB(Redis DataBase)持久化是透過生成資料快照來實現的。你可以配置 Redis 在指定的時間間隔內生成 RDB 快照,這種方式對效能的影響相對較小。
  3. 最佳化硬體:如果你的硬體(例如磁碟)效能較低,那麼 I/O 操作可能會成為瓶頸。在這種情況下,你可能需要升級你的硬體。
  4. 使用快取:如果你的應用主要是讀操作,你可以考慮使用快取來減輕資料庫的負載。例如,你可以使用 Redis 的記憶體儲存功能,或者使用其他的快取系統。

注意,調整持久化策略或使用快取都可能會增加資料丟失的風險,因此你需要根據你的應用需求和風險承受能力來選擇合適的策略。

3.7 expires 淘汰過期資料

參考這篇文章《Redis系列18:過期資料的刪除策略》,我們有詳細的描述
Redis 有兩種方式淘汰過期資料:

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

定時任務的發起的頻率由redis.conf配置檔案中的hz來進行配置

# 代表每1s 執行 10次
hz 10

Redis 預設每 1 秒執行 10 次,也就是每 100 ms 執行一次,每次隨機抽取一些設定了過期時間的 key(這邊注意不是檢查所有設定過期時間的key,而是隨機抽取部分),檢查是否過期,如果發現過期了就直接刪除。
該定時任務的具體流程如下:

  • 定時serverCron方法去執行清理,執行頻率根據redis.conf中的hz配置的值
  • 執行清理的時候,不是去掃描所有的key,而是去掃描所有設定了過期時間的key(redisDb.expires)
  • 如果每次去把所有過期的key都拿過來,那麼假如過期的key很多,就會很慢,所以也不是一次性拿取所有的key
  • 根據hash桶的維度去掃描key,掃到20(可配)個key為止。假如第一個桶是15個key ,沒有滿足20,繼續掃描第二個桶,第二個桶20個key,由於是以hash桶的維度掃描的,所以第二個掃到了就會全掃,總共掃描35個key
  • 找到掃描的key裡面過期的key,並進行刪除
  • 刪除完檢查過期的 key 超過 25%,繼續執行4、5步

大家看最後一步,如果一致有超過25%的過期key,就會導致 Redis 一直去刪除來釋放記憶體,而刪除是阻塞的。
所以,需要避免大量 key 過期早同一時期過期,這樣可能需要重複刪除多次才能降低到 25% 以下。

解決方案:
可以給快取設定過期時間時加上一個隨機值時間(在 EXPIREAT 和 EXPIRE 的過期時間引數上,加上一個一定大小範圍內的隨機數),使得每個key的過期時間分佈開來,不會集中在同一時刻失效。
隨機值我們團隊的做法是:n * 3/4 + n * random() 。所以,比如你原本計劃對一個快取建立的過期時間為8小時,那就是6小時 + 0~2小時的隨機值。
這樣保證了均勻分佈在 6~8小時之間。如圖:
image

3.8 最佳化 bigkey

bigkey 是指含有較大資料或含有大量成員、列表數的 Key。以下是一些實際的例子:

  • 一個 STRING Key 的Value過大,比如超過 5MB
  • 一個 LIST 型別的 Key,它的List Size太大,比如10000 個
  • 一個 ZSET 型別的 Key,它的Member Size太大,比如 10000 個
  • 一個 HASH 格式的 Key,它的Member Size太大,比如 10000個,或者Value值總量過大,比如 100MB

bigkey 帶來問題如下:

  1. OOM,或者達到 maxmemory 閾值導致請求阻塞或者key被驅逐。
  2. Redis Cluster 的資料負載最小粒度為 Key,這樣如果某個node上有一個bigkey,就可能導致記憶體不均衡。
  3. bigkey 的讀寫都有有較大的頻寬佔用、記憶體佔用,混合使用雲主機情況下,會影響其他服務的資源存量。
  4. 刪除一個 bigkey 造成主庫較長時間的阻塞甚至引發同步中斷或主從切換。

最佳化 Redis 的 bigkey 可以從以下幾個方面入手:

  1. 拆分 bigkey:將一個含有大量成員、列表數或資料大小的 Key 拆分成多個小 Key,每個 Key 的成員數量或資料大小在合理範圍內。這樣可以避免單個 Key 佔用過多記憶體,並且可以提高 Redis 的讀寫效能。
  2. 非同步清理 bigkey:Redis 4.0 版本提供了 UNLINK 命令,可以用來安全地刪除 bigkey,避免對 Redis 例項造成過大的負載。可以透過將 UNLINK 命令放在非同步任務中執行,以避免對主執行緒造成阻塞。
  3. 使用更合適的資料結構:根據實際需求選擇更合適的資料結構,比如使用雜湊表(hash)代替字串(string),使用有序集合(sorted set)代替列表(list)等。這樣可以減少資料的記憶體佔用,提高讀寫效能。
  4. 控制 key 的數量:儘量避免使用過多的 Key,尤其是在使用 Redis Cluster 時,過多的 Key 會導致資料分佈不均,影響叢集的效能和穩定性。
  5. 使用壓縮功能:Redis 提供了壓縮功能,可以將 bigkey 的資料進行壓縮後再儲存,減少記憶體佔用和網路傳輸開銷。但是需要注意的是,壓縮和解壓操作會增加 CPU 的負載,需要根據實際情況權衡利弊。
  6. 差異化過期時間:如果一批 key 的確是同時過期,可以在 EXPIREAT 和 EXPIRE 的過期時間引數上,加上一個一定大小範圍內的隨機數

3.9 其他原因

  • 複雜度過高的命令或查詢全量資料。
  • 記憶體達到 maxmemory。
  • 客戶端使用短連線和 Redis 相連。
  • Redis 例項的資料量大,導致生成 RDB 或 AOF 重寫耗時嚴重。
  • Redis 例項執行機器的記憶體不足,導致 swap 發生,Redis 需要到 swap 分割槽讀取資料。
  • 程式繫結 CPU 不合理。
  • Redis 例項執行機器上開啟了透明記憶體大頁機制。
  • 網路卡壓力過大。
  • Redis 例項之間以及內部資料傳輸阻塞,包括客戶端、磁碟、主從通訊、切片叢集通訊等問題。
  • 多 CPU 多核架構問題,包括綁核、綁 CPU 等。
  • sql 語句執行阻塞,包括慢查詢、過期 key 等問題。

4 總結

最佳化步驟如下:

  • 獲取 Redis 的基線延遲情況
  • 開啟慢指令監控,定位慢指令導致的問題,分析並最佳化。
  • 開啟慢請求日誌,分析執行時間超時情況,以便分析和最佳化。
  • 解決常見的Redis慢執行問題
    • 網路通訊延遲
    • 慢指令導致的延遲
    • Fork 生成 RDB 導致的延遲
    • AOF 檔案系統或 RDB 大記憶體頁問題,包括 AOF 持久化阻塞、大記憶體頁等
    • 作業系統 Swap 操作問題,包括記憶體磁碟資料轉換、記憶體清理等
    • 對swap導致的效能問題進行排查
    • AOF 的寫回策略為 always,導致每個操作都要同步刷回磁碟
    • expires 淘汰過期資料
    • 最佳化 bigkey

相關文章