簡單回顧
事故回溯總結一句話:
(1)因為大KEY呼叫量,隨著白天自然流量趨勢增長而增長,最終在業務高峰最高點期佔滿頻寬使用100%。
(2)從而引發redis的記憶體使用率,在5min之內從0%->100%。
(3)最終全面GET SET timeout崩潰(11點22分02秒)。
(4)最終導致頁面返回timeout。
未解之謎
疑問點:記憶體使用率100% 就等同於redis不可用嗎?
解答:正常使用情況下,不是。
redis有【快取淘汰機制】,Redis 在記憶體使用率達到 100% 時不會直接崩潰。相反,它依賴記憶體淘汰策略來釋放記憶體,確保系統的穩定性。
這個配置在哪裏?
大部分同學都是不會主動去調整這裏的引數的。
因此大機率預設的是:volatile-lru
-
行為: 使用 LRU(Least Recently Used,最近最少使用)演算法驅逐鍵。volatile-lru 僅驅逐設有過期時間的鍵,allkeys-lru 則驅逐所有鍵。 適用場景: 快取場景,不介意丟失一些資料。
確保你根據實際需求配置適當的記憶體淘汰策略,以便在記憶體達到上限時,系統能夠穩定地處理新請求,而不會出現寫操作失敗的情況(只要不是noeviction)。
也就是說,照理SET GET都應該沒啥問題纔對(先不考慮其他複雜命令)。
-
儘管 Redis 本身不會輕易崩潰,但如果記憶體耗盡且沒有淘汰策略或者淘汰策略未能生效,Redis 可能拒絕新的寫操作,並返回錯誤:OOM command not allowed when used memory > 'maxmemory' 如果系統的配置或者作業系統的記憶體管理不當,可能會導致 Redis 程序被作業系統殺死。
疑問點:但是事故現象就是:記憶體使用率100% 時,redis不可用,怎麼解釋?
猜測1:會是淘汰不及時導致的效能瓶頸嗎?
也就是說:寫入的速度>>淘汰的速度。
解答:如果是正常的業務寫入,不可能!
-
redis純記憶體,淘汰速度是非常快的; 這個業務特性,也並非高頻寫入;
這個redis例項其實裡面儲存的KEY很少,最終佔了整個例項的記憶體使用率<5%。
不太符合正常使用下KEY不斷增多,最終擠爆記憶體使用率的問題。
因此,初步結論:Redis 的崩潰一般不會是由於單純寫入速度超過淘汰速度引起的,尤其是使用了合理的記憶體淘汰策略時;如果寫入速度非常高,而淘汰策略無法及時清除舊資料,Redis 可能會非常頻繁地進行鍵的查詢和淘汰操作,從而導致效能下降。
具體機制如下:
過期 key 的自動刪除機制。它是 Redis 用來回收記憶體空間的常用機制,應用廣泛,本身就會引起 Redis 操作阻塞,導致效能變慢,所以,你必須要知道該機制對效能的影響。
Redis 鍵值對的 key 可以設定過期時間。預設情況下,Redis 每 100 毫秒會刪除一些過期 key,具體的演算法如下:
1.取樣:
2.如果超過 25% 的 key 過期了,則重複刪除的過程,直到過期 key 的比例降至 25% 以下。
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 是 Redis 的一個引數,預設是 20,那麼,一秒內基本有 200 個過期 key 會被刪除。這一策略對清除過期 key、釋放記憶體空間很有幫助。如果每秒鐘刪除 200 個過期 key,並不會對 Redis 造成太大影響。
但是,如果觸發了上面這個演算法的第二條,Redis 就會一直刪除以釋放記憶體空間。注意,刪除操作是阻塞的(Redis 4.0 後可以用非同步執行緒機制來減少阻塞影響)。所以,一旦該條件觸發,Redis 的執行緒就會一直執行刪除,這樣一來,就沒辦法正常服務其他的鍵值操作了,就會進一步引起其他鍵值操作的延遲增加,Redis 就會變慢。
那麼,演算法的第二條是怎麼被觸發的呢?其中一個重要來源,就是頻繁使用帶有相同時間引數的 EXPIREAT 命令設定過期 key,這就會導致,在同一秒內有大量的 key 同時過期。
可以類比JVM頻繁GC造成的效能影響。
猜測2:那就是寫入太兇猛,且是【非正常業務寫入】
那到底是什麼導致了記憶體使用率激增呢??
蛛絲馬跡
https://help.aliyun.com/zh/redis/support/how-to-solve-the-sudden-increase-in-redis-memory-usage?spm=a2c4g.11186623.0.i12
因此查閱了資料,發現最為貼近的答案。
證據支撐
真相大白
果然是這樣,說明記憶體是被【緩衝區】擠爆的。
https://learn.lianglianglee.com/專欄/Redis%20核心技術與實戰/21%20%20緩衝區:一個可能引發“慘案”的地方.md
知識點:Redis的記憶體佔用組成
使用info memory進行分析(我隨便模擬了一個緩衝區溢位的case,並非事故現場,因為當時不會)。
分析和解釋
從上面的 INFO memory 輸出中,我們可以看到一些關鍵資訊,這些資訊表明大部分記憶體被緩衝區佔用殆盡:
1.記憶體使用情況:
-
used_memory: 1072693248 (1.02 GB) maxmemory: 1073741824 (1.00 GB)
上面的輸出表明,當前記憶體使用幾乎達到了配置的最大記憶體限制,記憶體已接近耗盡。
2.緩衝區佔用:
used_memory_overhead: 1048576000 (1.00 GB)
這個值表示 Redis 開銷的記憶體,包括緩衝區、連線和其他後設資料。在這種情況下,大部分 used_memory (1.02 GB) 被 used_memory_overhead (1.00 GB) 佔用,這意味著大部分記憶體都被緩衝區等開銷佔據。
3.資料集佔用:
-
used_memory_dataset: 23929848 (23.93 MB) used_memory_dataset_perc: 2.23%
這裏顯示,實際儲存的資料只佔了非常少的一部分記憶體(約 23.93 MB),而絕大部分記憶體被緩衝區佔據。
4.客戶端緩衝區:
mem_clients_normal: 1048576000 (1.00 GB)
這表明普通客戶端連線佔用了約 1.00 GB 記憶體,這通常意味著輸出緩衝區可能已經接近或達到了設定的限制。
5.記憶體碎片:
-
allocator_frag_ratio: 1.02 mem_fragmentation_ratio: 1.02
碎片率不高,表明記憶體被合理使用但被緩衝區佔用過多。
總結
從上面的例子可以看出,Redis 的記憶體幾乎被緩衝區佔用殆盡。以下是具體的結論:
-
當前記憶體使用 (used_memory) 已經接近最大記憶體限制 (maxmemory),即 1.02 GB 接近 1.00 GB 的限制。 -
記憶體開銷 (used_memory_overhead) 很大,主要被客戶端普通連線使用(可能是輸出緩衝區),而實際的資料僅佔用了很少的記憶體。 分配器和 RSS 碎片率 (allocator_frag_ratio 和 mem_fragmentation_ratio) 較低,表明碎片不是問題。
緩衝記憶體的理論最大值推導
為什麼要有緩衝區
Redis工作原理-單客戶端視角簡單版:
Redis工作原理-單客戶端視角複雜版:
緩衝區的功能其實很簡單,主要就是用一塊記憶體空間來暫時存放命令資料,以免出現因為資料和命令的處理速度慢於傳送速度而導致的資料丟失和效能問題。
因此,Redis工作原理-多客戶端視角簡單版(含緩衝區)。
輸入緩衝區
定義
記憶體佔用
輸出緩衝區
定義
記憶體佔用
Redis Server 的輸出大小通常是不可控制的。存在bigkey的時候,就會產生體積龐大的返回資料。另外也有可能因為執行了太多命令,導致產生返回資料的速率超過了往客戶端傳送的速率,導致伺服器堆積大量訊息,從而導致輸出緩衝區越來越大,佔用過多記憶體,甚至導致系統崩潰。Redis 透過設定 client-output-buffer-limit來保護系統安全。
不出意外,預設是:32MB 8MB 60s
對於 Pub/Sub 連線:
-
硬限制:每個 Pub/Sub 客戶端連線的輸出緩衝區最大可以達到 32MB 。 連線池最大連線數:300 個連線,其中所有連線假設都在處理 Pub/Sub 訊息且達到了最大緩衝區限制。
故理論上,最大輸出緩衝區可以達到:
最大輸出緩衝區佔用=硬限制×連線池最大連線數最大輸出緩衝區佔用=硬限制×連線池最大連線數=32MB×300=9600MB=9.375GB
因此,在這種配置下,所有 Pub/Sub 連線的輸出緩衝區理論上的最大佔用記憶體為 9.375 GB。
因此,在client-output-buffer-limit是預設的情況下,最大佔用記憶體為 9.375 GB。
所以,MAX(輸出緩衝區+輸入緩衝區)是否會造成記憶體使用率100%?
當然!
MAX(輸出緩衝區+輸入緩衝區)=10.375GB >> 一個例項的記憶體規格(在本case中,是2G)
最後的效果就是:
物件儲存的部分因為是有過期時間的,過期了自然被清理了。
-
【緩衝記憶體】↑ (涌入) -
【物件記憶體】↓ (定時清理) 並受MAX記憶體掣肘(上限)
最終的結局:Redis 的記憶體完全被緩衝區佔據。
自然,每當有SET請求進來的時候,SET不進來——因為「記憶體淘汰策略」(maxmemory-policy) 淘汰的是【物件記憶體】,壓根起不到作用!!!
結論:
Redis 的記憶體完全被緩衝區佔據,實際上 Redis 將無法正常工作,包括資料儲存(SET 操作)和資料讀取(GET 操作)。
分析:為何緩衝區激增(Redis不可用的時間點11:22:02,之前都發生了什麼)
知道了緩衝區擠爆Redis記憶體會導致Redis不工作之後,接下來就是分析為何當時出現了緩衝區激增並最終導致redis不可用。
例項資訊
相關程式碼
案件還原
1.自然增長導致流出頻寬不斷變大直至96MB/s。
2.流出頻寬超過96MB/s,輸出緩衝區記憶體佔用激增甚至溢位 (300setMaxTotal*10機器ip數量個客戶端,之前推導過可以到9G)。
6.SET流量飆升,且因都是大KEY,導致流入頻寬激增(別看寫QPS只有50,但是如果每個寫都是2MB,就可以做到瞬間佔滿流入頻寬)。
8.輸入緩衝區記憶體隨即激增。
10.後續的SET GET命令甚至都進不了輸入緩衝區,阻塞持續到客戶端配置的SoTimeout時間;但是流入流出頻寬依然佔據並持續,總頻寬到達了216MB/s > 例項最大頻寬192MB/s。
11.造成最終的不可用(後續的命令想進場,要依賴當前輸入緩衝區裡的命令被執行給你騰出來位置,但是還是那句話Redis主執行緒處理消化的速度,實在是太慢了;從圖中,可以看到Redis的QPS驟降。
11:35分之後,我把redis降級了,全使用db來抗流量。
開發運維規範
可以看到Redis的效能是有邊界的,不能盲目相信所謂的高效能。
真正理解效能須使用benchmark。
它也是有問題能造成他的效能瓶頸的。
計算資源 |
使用萬用字元、Lua併發、1對多的PUBSUB、熱點Key等會大量消耗計算資源,叢集架構下還會導致訪問傾斜,無法有效利用所有資料分片。 |
儲存資源 |
Streaming慢消費、大Key等會佔用大量儲存資源,叢集架構下還會導致資料傾斜,無法有效利用所有資料分片。 |
網路資源 |
掃描全庫(KEYS命令)、大Value、大Key的範圍查詢(如HGETALL命令)等會消耗大量的網路資源,且極易引發執行緒阻塞。 重要 Redis的高併發能力不等同於高吞吐能力,例如將大Value存在Redis裡以期望提升訪問效能,此類場景往往不會有特別大的收益,反而會影響Redis整體的服務能力。 |
在上述的事故中,就佔了【網路資源消耗高】【儲存資源消耗高】兩大問題
因此也到了本文的方法論環節:從業務部署、Key的設計、SDK、命令、運維管理等維度展示雲資料庫Redis開發運維規範:
-
業務部署規範:https://help.aliyun.com/zh/redis/use-cases/development-and-o-and-m-standards-for-apsaradb-for-redis -
Key設計規範:https://help.aliyun.com/zh/redis/use-cases/development-and-o-and-m-standards-for-apsaradb-for-redis -
SDK使用規範:https://help.aliyun.com/zh/redis/use-cases/development-and-o-and-m-standards-for-apsaradb-for-redis -
命令使用規範:https://help.aliyun.com/zh/redis/use-cases/development-and-o-and-m-standards-for-apsaradb-for-redis 運維管理規範:https://help.aliyun.com/zh/redis/use-cases/development-and-o-and-m-standards-for-apsaradb-for-redis
業務部署規範
重要程度 |
規範 |
說明 |
★★★★★ |
確定使用場景為快取記憶體或記憶體資料庫。 |
重要 如需使用透過資料閃回按時間點恢復資料功能,AOF功能需保持開啟狀態。
|
★★★★★ |
就近部署業務,例如將業務部署在同一個專有網路VPC下的ECS例項中。 |
Redis具備極強的效能,如果部署位置過遠(例如業務伺服器與Redis例項透過公網連線),網路延遲將極大影響讀寫效能。 說明 針對多地部署應用的場景,您可以透過全球多活功能,藉助其提供的跨域複製(Geo-replication)能力,快速實現資料異地災備和多活,降低網路延遲和業務設計的複雜度。更多資訊,請參見Redis全球多活簡介。 |
★★★★☆ |
為每個業務提供單獨的Redis例項。 |
避免業務混用,尤其需要避免將同一Redis例項同時用作快取記憶體和記憶體資料庫業務。帶來的影響例如針對某個業務淘汰策略設定、產生的慢請求或執行FLUSHDB命令影響將擴散至其他業務。 |
★★★★☆ |
設定合理的過期淘汰策略。 |
雲資料庫Redis預設的預設逐出策略為volatile-lru,關於各逐出策略的說明,請參見Redis配置引數列表。 |
★★★☆☆ |
合理控制壓測的資料和壓測時間。 |
雲資料庫Redis不會對您壓測的資料執行自動刪除操作,您需要自行控制壓測資料的資料量和壓測時間,避免對業務造成影響。 |
Key設計規範
重要程度 |
規範 |
說明 |
★★★★★ |
設計合理的Key中Value的大小,推薦小於10 KB。 |
過大的Value會引發資料傾斜、熱點Key、例項流量或CPU效能被佔滿等問題,應從設計源頭上避免此類問題帶來的影響。 |
★★★★★ |
設計合理的Key名稱與長度。 |
說明 叢集架構下執行同時操作多個Key的命令時(例如RENAME命令),如果被操作的Key未使用hash tag讓其處於相同的資料分片,則命令無法正常執行。
|
★★★★★ |
對於支援子Key的複雜數據結構,應避免一個Key中包含過多的子Key(推薦低於1,000)。 說明 常見的複雜數據結構例如Hash、Set、Zset、Geo、Stream及Tair(Redis企業版)特有的exHash、Bloom、TairGIS等。 |
由於某些命令(例如HGETALL)的時間複雜度直接與Key中的子Key數量相關。如果頻繁執行時間複雜度為O(N)及以上的命令,且Key中的子Key數量過多容易引發慢請求、資料傾斜或熱點Key問題。 |
★★★★☆ |
推薦使用序列化方法將Value轉變為可讀的結構。 |
由於程式語言的位元組碼隨著版本可能會變化,如果儲存裸物件(例如Java Object、C#物件)會導致整個軟體棧升級困難,推薦使用序列化方法將Value變成可讀的結構。 |
SDK使用規範
重要程度 |
規範 |
說明 |
★★★★★ |
推薦使用JedisPool或者JedisCluster連線例項。 說明 企業版(記憶體型)例項推薦使用TairJedis客戶端,支援新數據結構的封裝類。使用方法,請參見透過客戶端程式連線Redis。 |
如果使用單連線的方式,一旦遇到單次超時則無法自動恢復。關於JedisPool的連線方法,請參見透過客戶端程式連線Redis、JedisPool資源池最佳化和JedisCluster。 |
★★★★☆ |
程式客戶端需要對超時和慢請求做容錯處理。 |
由於Redis服務可能因網路波動或資源佔滿引發超時或慢請求,您需要在程式客戶端上設計合理的容錯機制。 |
★★★★☆ |
程式客戶端應設定相對寬鬆的超時重試時間。 |
如果超時重試時間設定的非常短(例如200毫秒以下),可能引發重試風暴,極易引發業務層雪崩。更多資訊,請參見Redis客戶端重連指南。 |
命令使用規範
重要程度 |
規範 |
說明 |
★★★★★ |
避免執行範圍查詢(例如KEYS *),使用多次單點查詢或SCAN命令來獲取延遲優勢。 |
執行範圍查詢可能導致服務發生抖動、引發慢請求或產生阻塞。 |
★★★★★ |
推薦使用擴充套件數據結構(數據結構模組整合)實現複雜功能,避免使用Lua指令碼。 |
Lua指令碼會佔用較多的計算和記憶體資源,且無法被多執行緒加速,過於複雜或不合理的Lua指令碼可能導致資源被佔滿的情況。 |
★★★★☆ |
合理使用管道(pipeline)降低鏈路的往返時延RTT(Round-trip time)。 |
如果有多個操作命令需要被迅速提交至伺服器端,且客戶端不依賴每個操作返回的結果,那麼可以透過管道來作為最佳化效能的批處理工具,注意事項如下:
|
★★★★☆ |
正確使用Redis社羣版命令支援。 |
使用事務(Transaction)時,需要注意其限制:
|
★★★★☆ |
避免使用Redis社羣版命令支援執行大量的訊息分發工作。 |
由於Pub和Sub不支援資料持久化,且不支援ACK應答機制無法實現資料可靠性,當執行大量訊息分發工作時(例如訂閱客戶端數量超過100且Value超過1 KB),訂閱客戶端可能因服務端資源被佔滿而無法接收到資料。 說明 為提升效能和均衡性,雲資料庫Redis對Pub和Sub類命令進行了最佳化,叢集架構下,代理節點會根據channel name進行Hash計算,並分配至對應資料節點。 |
運維管理規範
重要程度 |
規範 |
說明 |
★★★★★ |
充分了解不同的例項管理操作帶來的影響。 |
在對Redis例項執行變更配置、重啟等操作時,例項的狀態將發生變化併產生某些影響(例如產生秒級的連線閃斷),在操作前您需要充分了解。更多資訊,請參見例項狀態與影響。 |
★★★★★ |
驗證客戶端程式的差錯處理能力或容災邏輯。 |
雲資料庫Redis支援節點健康狀態監測,當監測到例項中的主節點不可用時,會自動觸發主備切換,保障例項的高可用性。在客戶端程式正式上線前,推薦手動觸發主備切換,可幫助您驗證客戶端程式的差錯處理能力或容災邏輯。具體操作,請參見手動執行主備切換。 |
★★★★★ |
禁用高耗時或高風險的命令。 |
生產環境中,無限制地使用命令可能帶來諸多問題,例如執行FLUSHALL會直接清空全部資料;執行KEYS會阻塞Redis服務。為保障業務穩定、高效率地執行,您可以根據實際情況禁用特定的命令,具體操作,請參見禁用高風險命令。 |
★★★★☆ |
及時處理阿里雲發起的計劃內運維操作(即待處理事件)。 |
為提供更優質的服務,持續提升產品效能和穩定性,阿里雲會不定期地發起計劃內運維操作(即待處理事件),對部分例項所屬的機器執行軟硬體或網路換代升級(例如資料庫小版本升級)。當您收到來自阿里雲的事件通知後,您可以檢視本次事件的影響,根據業務需求評估是否需要調整執行時間。更多資訊,請參見檢視並管理待處理事件。 |
★★★★☆ |
為核心指標配置監控報警,幫助掌握例項執行狀態。 |
為CPU使用率、記憶體使用率、頻寬使用率等核心指標配置監控報警,實時掌握例項執行狀態。具體操作,請參見報警設定。 |
★★★★☆ |
透過雲資料庫Redis提供的豐富的運維功能,定期檢查例項狀態或輔助排查資源消耗異常問題。 |
|
★★★☆☆ |
評估並開啟審計日誌功能。 |
開通審計日誌功能後,可記錄寫操作的審計資訊,為您提供日誌的查詢、線上分析、匯出等功能,助您時刻掌握產品安全及效能情況。更多資訊,請參見開通審計日誌。 重要 開通審計日誌後,視寫入量或審計量可能會對Redis例項造成5%~15%的效能損失。如果業務對Redis例項的寫入量非常大,建議僅在運維需要(例如故障排查)期間開通審計功能,以免帶來效能損失。 |
重點行動項
大KEY
大KEY其實並不是長度過長的KEY,而是存放了慢查詢命令的KEY。
對於String型別,慢查詢的本質在於value的大小。
對於其他型別,慢查詢的本質在於集合的大小(時間複雜度帶來)。
如何找到大key?
https://help.aliyun.com/zh/redis/user-guide/identify-and-handle-large-keys-and-hotkeys?spm=a2c4g.11186623.0.i1
如何解決大key?
其實就是一個字 "拆"。
1.對於字串型別的key,我們通常要在業務層面將value的大小控制在10KB左右,如果value確實很大,可以考慮採用序列化演算法和壓縮演算法來處理,推薦常用的幾種序列化演算法:Protostuff、Kryo或者Fst。
2.對於集合型別的key,我們通常要透過控制集合內元素數量來避免bigKey,通常的做法是將一個大的集合型別的key拆分成若干小集合型別的key來達到目的。
壓測關注點
1.資料是否傾斜(不能只看聚合資訊,要切換到分片上,看資料節點);
4.如果可以,開啟審計日誌,看寫入日誌是否符合程式碼邏輯。
常用運維命令
出線上事故的時候,用於快速分析和保留現場。