Redis key 消失之謎
作者:vivo 互聯網存儲團隊 - Lin Haiwen、Xu Xingbao
本文從一次生產環境業務服務報錯,逐步對問題進行定位,深入分析之后發現導致問題的原因,給出相應的優化方法,提升業務可用性。
1分鐘看圖掌握核心觀點??

一、問題描述
1.1 報錯信息
應用服務報錯,通過監控日志發現凌晨2點的時候,應用報錯獲取不到Redis key。
1.2 告警與監控信息
首先想到是否由于內存滿導致的key淘汰,生產的所有Redis都有設置內存告警,但沒有收到內存告警信息;(內存告警需要每隔10秒,連續3次觸發才會告警。)
從監控圖中,可以看到Redis內存稍有增長,但使用率一直偏低,并沒有達到使用率告警。

查看監控信息:在問題時間點,發生了大量的key過期,初步懷疑是由于key批量設置了過期時間,正好到期了導致大量key失效。

查看Redis錯誤日志,沒有發現異常。
二、問題定位
-
基于前面的監控,初步懷疑是key設置了過期時間導致失效。
-
是否有上線其他新功能導致?
但是業務反饋不是由于設置過期時間導致;并且第二天問題復現,因此繼續深入定位。
2.1 key是否過期
-
查看淘汰策略
-
查看key過期時間
初步判斷確實不是因為key過期導致的大量淘汰,這里TTL接近5天,但是連著2天出現問題,因此不應該是過期時間到了導致。
xxx:xxx> config get'maxmemory-policy'
1)"maxmemory-policy"
2)"volatile-lru"
xxx:xxx> ttl finance:xxxx_cms_version_10000
-> Redirected to slot [9229] located at xxx:xxx
(integer)387585
xxx:xxx> ttl finance:xxxcms_basic_data_10423
(integer)387574
key是被刪除還是淘汰?查看監控,發現存在key確實被淘汰,說明接下來需要考慮內存問題。

2.2 是否內存滿了
發現確實短時間內存寫滿了,一開始查看監控由于時間拉的比較長,查看增長趨勢沒發現內存寫滿情況,但是由于沒有達到告警條件,沒有滿足連著3次觸發閾值,故沒有觸發告警。
同時內存用滿問題持續時間較短,約10分鐘左右。

其他指標檢查,發現出現問題時client_longest_output_list指標存在明顯突刺,該指標非常可疑。

請求量的大漲,懷疑是請求堆積導致buffer增長使得內存寫滿。但是此時還有疑點:寫入也上漲,是否是由于讀請求積壓導致,還是因為寫入數據導致內存滿?
2.3 找出內存漲的來源
設置定時任務,對出問題時間點前后20分鐘這段時間進行抓包分析。
對比出問題前后幾分鐘的請求,對應時間點請求量飆升,并且請求量來源基本是get請求,set請求也少量增長。
4860 get finance:xxxx__10122
4925 get finance:xxxx__10032
4945set finance:xxxx_data_10013-0
4947 get finance:xxxx_data_10013_cms_version_10000
4976 get finance:xxxx__10251
5054set finance:xxxx__undefined
5098 get finance:xxxx__10018
8729 get finance:xxxx_data_10415_cms_version_10000
9152 get finance:xxxx_data_10401_cms_version_10000
9553 get finance:xxxx_data_10228_cms_version_10000
9597 get finance:xxxx_data_10213_cms_version_10000
9622 get finance:xxxx_data_10032_cms_version_10000
9647 get finance:xxxx_data_10347_cms_version_10000
9674 get finance:xxxx_data_10182_cms_version_10000
9742 get finance:xxxx_data_10251_cms_version_10000
10085 get finance:xxxx_data_10019_cms_version_10000
23064 get finance:xxxx__10423
45176 get finance:xxxx_data_10423_cms_version_10000
[root@db-prd-xxx.v-bj-3.vivo.lan:/data/redis/scpdir]
# cat 0807.cap | grep '2024-08-07 01:59'|awk '{print $8,$10}'|sort |uniq -c |sort -k 1 -n
同時也對set的內容進行分析,發現set的數據并不足使內存寫滿;且上面監控可以看到,內存寫滿問題持續時間很短,因此應該不是數據增長導致。
進一步對get請求來源分析:

結合 IP來源以及 keyname;跟業務同學溝通確認:
由于業務讀請求量大漲導致,業務請求從每分鐘27w左右上漲到70w左右,主要有:

2.4 機制分析
內存用量上漲超限會觸發Redis節點基于已經配置的volatile-lru策略進行過期數據淘汰,所以需要找到內存上漲的來源。基于監控指標排查分析,基本確定了發生異常期間沒有集中的大量數據寫入,反而發現大量網絡數據的輸出,抓包也印證了此時節點主要是在處理get命令讀請求。進一步地發現了client-output-buffer-limit這個指標出現異常上漲,才最終鎖定到Redis的回包積壓問題。
關于Redis的結果回包邏輯,首先要了解Redis的結果存儲空間設計。Redis針對每一個連接客戶端都會初始化一個大小為16K的靜態的buf區域和一個空的鏈表結果,相關聲明代碼如下:
#define REDIS_REPLY_CHUNK_BYTES (16*1024) /* 16k output buffer */
char buf[REDIS_REPLY_CHUNK_BYTES];
list *reply;
c->reply = listCreate();
對于Redis而言正常執行的命令都會有數據回包,而回包結果都需要在客戶端中做暫存。Redis是如何結合以上兩個數據進行結果存儲的呢?主要邏輯是如果靜態buf區域能夠滿足回包結果存儲,即結果不大于16k,那么結果就會存儲靜態buf中,將結果不斷插入reply鏈表中;同時我們都知道Redis支持豐富的數據類型和操作命令,有些批量數據讀取命令的結果可能會有很多字段,這些字段也會作為一個個鏈表元素追加到reply鏈表中。正常情況下,我們在Redis節點上執行info clients命令可以獲得如下客戶端的統計信息:
> info clients
# Clients
connected_clients: 1
client_longest_output_list: 0
client_biggest_input_buf: 0
blocked_clients:
其中的client_longest_output_list字段代表此時節點的所有連接客戶端中回包結果的緩存情況。
但是按照之前服務監控和抓包結果分析,具體執行的指令都是get,實際數據也沒有超過16k大小,并沒有滿足將結果存儲到鏈表的條件。這里有個經常被忽略的場景,就是Redis其實支持pipeline命令執行方式的,簡單來說就是Redis支持一次性接收一個客戶端的多個指令,具體執行過程中會把這些指令的結果不斷暫存,然后在后續流程中集中回包,如果這時候不能及時地把數據通過網絡發出去,就有可能出現reply鏈表長度激增的現象,進而導致客戶端占用內存激增,這正是我們本次遇到的場景。

Redis的maxmemory參數限制的是Redis實例可以使用的最大內存,這部分內存主要包括以下幾個部分:業務數據占用的內存、客戶端連接的輸入/輸出緩沖區、復制積壓緩沖區、AOF 緩沖區以及其他一些內部開銷。
具體來說,Redis 的maxmemory 限制包括:
-
業務數據占用的內存,這部分是用戶在Redis中存儲的數據。
-
客戶端連接的輸入/輸出緩沖區,用于暫存客戶端發送的命令和Redis 返回給客戶端的數據。
-
復制積壓緩沖區:用于主從復制,當從節點斷線重連時,可以從這個緩沖區拉取丟失的數據。
-
AOF 緩沖區:用于持久化,當開啟AOF 持久化時,Redis 會將寫操作追加到AOF 緩沖區,然后異步地寫入AOF 文件。
-
其他內部開銷:包括Redis 進程本身的一些數據結構、對象、碎片內存等。
因此,在設置Redis 的maxmemory 參數時,需要綜合考慮業務數據的內存占用、各個緩沖區的大小以及內存碎片率等因素,合理地分配內存,避免出現內存溢出或性能下降的問題。
三、問題解決
3.1 緊急修復
-
臨時擴大Redis集群內存,避免內存寫滿。
-
限制client-output-buffer-limit大小,避免由于請求過大導致內存突增。
-
業務限流&削峰,避免請求量突增。
-
增加兜底機制,如果由于Redis key被淘汰,則去MySQL查詢,避免業務直接報錯。
3.2 根本解決
業務進行業務邏輯優化,將請求打散,避免同一時間集中大量請求Redis。
四、總結
本次問題是由于業務集中請求Redis,導致緩存積壓內存增長達到最大內存限制,觸發Redis淘汰策略對key進行驅逐。key被淘汰丟失后,需要增加兜底機制去DB側請求避免業務報錯。雖然Redis性能比較好,但是也要盡量打散請求,合理評估存儲側的性能。
同時,對于Redis淘汰策略,對于數據比較重要的集群,可以考慮使用不驅逐的方式。合理設置TTL保留時間,把Redis的內存使用率保持在安全的區域。

浙公網安備 33010602011771號