redis 優化和注意點(深入淺出Redis性能診斷系列文章)
2023-09-09 16:11:56 3
(本文首發於「資料庫架構師」公號,訂閱「資料庫架構師」公號,一起學習資料庫技術)
本篇為Redis性能問題診斷系列的第二篇,本文主要從應用發起的典型命令使用上進行講解,由於Redis為單線程服務架構,對於一些命令如果使用不當會極大的影響Redis的性能表現,這裡也會對不合理的使用方式給出優化解決方案。
一、Redis慢日誌功能
分析Redis訪問變慢,其中有個最基礎的方法就是先去看Redis是否有慢日誌【就像MySQL的慢SQL一樣】。Redis提供了一個簡單的慢命令統計記錄功能,它會記錄有哪些命令在執行時耗時較長。Redis慢日誌功能由兩個核心參數控制:
slowlog-log-slower-than 1000
#慢日誌命令執行閾值,這裡指超過1ms就會被記錄【單位為微秒】
slowlog-max-len 4096
#保留慢日誌命令的個數,類似一個先進先出的隊列,超過4096個最早的就會被清理
Redis的這個慢日誌功能比較粗糙簡單,有個嚴重的不足:沒有持久化記錄能力。
由於Redis的慢日誌記錄都在內存中,不像MySQL會持久化到文件裡,那麼如果慢日誌產生較快,即使設置的slowlog-max-len比較大也會很快被填滿,診斷問題時也就不能統計到那個時間段產生的所有慢命令詳情。
為了避免產生的慢日誌被清理,目前一個折中的解決方案是寫一個收集程序周期性的將新增慢命令查出並記錄到MySQL或者本地文件中,以備事後分析。但是這個頻率一般都是分鐘級,Redis處理的吞吐能力又太大,在慢命令較多的情況下往往也不能全部記錄下來。
配置好慢日誌相關閾值後,可以執行以下命令查詢最近的慢日誌記錄了:
127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 42343
2) (integer) 1653659194 #慢日誌產生的時間戳
3) (integer) 73536 #慢日誌執行的耗時
4) 1) "KEYS" #慢日誌命令詳情
2) "permission::userMenuList:*"
5) "192.168.1.11:20504" #慢日誌命令發起來源IP【4.0及以後版本支持】
6) ""2) 1) (integer) 42342
2) (integer) 1653659194
3) (integer) 73650
4) 1) "KEYS"
2) "userPermission:*"
5) "192.168.1.10:20362"
6) ""
3) 1) (integer) 42341
2) (integer) 1653659193
3) (integer) 81505
4) 1) "KEYS"
2) "userRole:*"
5) "192.168.1.13:19926"
6) ""
二、幾種典型導致Redis變慢的不合理使用方式
1.使用keys命令進行正則匹配
Keys的正則匹配是阻塞式的、全量掃描過濾,這對於單線程服務的Redis來說是致命的,僅僅幾十萬個Key的匹配查詢在高並發訪問下就有可能將Redis打崩潰!這其實就像MySQL的無索引查詢大表數據,全表掃描狀態下幾個並發查詢就可能會將資料庫堵死。
redis> SLOWLOG get 5
1) 1) (integer) 42343
2) (integer) 1653659194
3) (integer) 73536
4) 1) "KEYS"
2) "Testper::userList:*"
5) "192.168.1.10:20504"
6) ""
2) 1) (integer) 42342
2) (integer) 1653659194
3) (integer) 73650
4) 1) "KEYS"
2) "TestuserPermission:*"
5) "192.168.1.11:20362"
6) ""
3) 1) (integer) 42341
2) (integer) 1653659193
3) (integer) 81505
4) 1) "KEYS"
2) "TestuserRole:*"
5) "192.168.1.12:19926"
6) ""
上述示例中使用Keys來模糊查詢某些Key,每次的執行都在70ms以上,嚴重影響了正常的Redis響應時長和吞吐。
針對這種問題的一個解決方案是使用scan代替keys。這是一個查詢迭代命令,用於迭代當前資料庫中的緩存數據。它是一個基於遊標的迭代器,每次被調用之後, 都會向用戶返回一個新的遊標, 用戶在下次迭代時需要使用這個新遊標作為Scan命令的遊標參數, 以此來延續之前的迭代過程。具體的命令語法這裡不再詳述。
2.大量使用了複雜度較高的命令
(1)應用中高頻使用了 O(N) 及以上複雜度的命令,例如:SUNION、SORT、ZUNIONSTORE、ZINTERSTORE 聚合類命令。SORT命令的時間複雜度:O(N M*log(M)), N 為要排序的列表或集合內的元素數量, M 為要返回的元素數量。
這種導致Redis請求變慢的原因是,Redis 在操作數據排序時,時間複雜度過高,要花費更多的 CPU計算資源。
(2)使用 O(N) 複雜度的命令,但 N 的值非常大,比如hgetall、smembers、lrange、zrange等命令。
這種變慢的原因在於,Redis 一次需要返回給客戶端的數據過多,需要花費更多時間在數據組裝和網絡傳輸中。對於hgetall、smembers這種命令,需要警惕項目剛上線之初hash、set或者list存儲的成員個數較少,但是隨著業務發展成員數量極有可能會膨脹的非常大,如果仍然採用上述命令不加控制,會極大拖累整個Redis服務的響應時間。
針對這兩種情況還都可以從資源使用率層面來分析,如果應用程式訪問 Redis 的QPS不是很大,但 Redis 實例的 CPU 使用率卻很高,那麼很有可能是使用了複雜度過高的命令導致的。
因為Redis 是單線程處理請求的,如果你經常使用以上複雜度較高的命令,那麼當 Redis 處理程序請求時,一旦前面某個命令發生耗時較長,就會導致後面的請求發生阻塞排隊,對於應用程式來說,響應延遲也會變長。
3.存儲使用了bigkey
在分析慢日誌發現很多請求並不是複雜度高的命令,都是一些del、set、hset等的低複雜度命令,那麼就要評估是否寫入了大key。
在往Redis寫入數據時,需要為新數據分配內存塊,相對應的,當刪除數據時,Redis也會釋放對應的內存空間。如果一個 key 寫入Redis的值非常大,那麼在分配內存時就會相對比較耗時。同樣的當刪除這個 key 時,釋放內存也會比較耗時,這種被稱為bigKey。
當然這個描述仍然比較寬泛,因為Redis中的資料庫結構類型比較多,更完善的一些說法可以這麼定義:將含有較大數據或含有大量成員、列表數的Key定義為bigkey。
我們一般要求研發使用Redis時,對於String類型Value大小不要超過1KB。
大Key帶來的問題比較多,主要有下面幾種情況:
由於大Key的內存分配及釋放開銷變大,直接影響就是導致應用訪問Redis的響應變慢;刪除時會造成較長時間的阻塞並有可能造成集群主備節點切換【4.0之前的版本有這個問題】;內存佔用過多甚至達到maxmemory配置,會造成新寫入阻塞或一些不應該被提前刪除的Key被逐出,甚至導致OOM發生;並發讀請求因為Key過大會可能打滿伺服器帶寬,如果單機多實例部署則同時會影響到該伺服器上的其它服務【假設一個bigkey為1MB,客戶端每秒訪問量為1000,那麼每秒產生1000MB的流量】;運維麻煩,比如RedisCluster的數據跨節點均衡,因為均衡遷移原理是通過migrate命令來完成的,這個命令實際是通過dump restore del三個命令組合成原子命令完成,如果是bigkey,可能會使遷移失敗,而且較慢的migrate也會阻塞Redis正常請求;分片集群RedisCluster中的出現嚴重的數據傾斜,導致某個節點的內存使用過大;那麼對於已經寫入的數據,如何分析找出裡面的bigkey進行優化呢?可以通過Redis官方客戶端redis-cli的bigkeys參數來定位大Key分布。
shell> redis-cli -h 127.0.0.1 -p 18708 -a xxxx --bigkeys -i 0.01
[00.00%] Biggest string found so far 'urlcount:www.guprocessorSuccessMid' with 1 bytes
[00.01%] Biggest string found so far 'TestDomain:www:config:scheduler' with 3847 bytes
[00.03%] Biggest string found so far 'TestDomain:www:config:scheduler' with 211306 bytes
[00.88%] Biggest set found so far 'specialTestJobSet:www' with 20 members
[01.69%] Biggest list found so far 'TestDomain:www:urlList' with 9762 items
[07.13%] Biggest list found so far 'TestDomain:bx:urlList' with 457676 items
[07.39%] Biggest set found so far 'specialTestJobSet:www' with 100 members
[13.99%] Biggest string found so far 'TestDomain:wwwe:config:scheduler' with 540731 bytes
[18.74%] Biggest set found so far 'TestJobSet' with 300 members
[58.09%] Biggest string found so far 'TestDomain:wwwrt:config:scheduler' with 739024 bytes
[64.19%] Biggest string found so far 'TestDomain:bx:config:scheduler' with 1335468 bytes
-------- summary -------
Sampled 62522 keys in the keyspace!
Total key length in bytes is 2471471 (avg len 39.53)
Biggest list found 'TestDomain:bx:urlList' has 457676 items
Biggest string found 'TestDomain:bx:config:scheduler' has 1335468 bytes
Biggest set found 'TestJobSet' has 300 members
208 lists with 2408539 items (00.33% of keys, avg size 11579.51)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
62283 strings with 32642667 bytes (99.62% of keys, avg size 524.10)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
31 sets with 1354 members (00.05% of keys, avg size 43.68)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
從輸出結果我們可以看到,每種數據類型所佔用的最大長度或含有最多成員的 key 是哪一個,以及每種數據類型在整個實例中的佔比和平均大小及成員數量。
其實,使用這個命令的原理就是 Redis 在內部執行了 SCAN 命令,遍歷整個實例中所有的 key,然後針對 key 的類型,分別執行 STRLEN、HLEN、LLEN、SCARD、ZCARD 命令,來獲取 String 類型的長度、集合類型(Hash、List、Set、ZSet)的成員個數.
注意,使用該--bigkeys進行大key的統計時要注意:
對於集合類型的Hash、List、Set、ZSet僅僅統計的是包含的成員個數,個數多並代表佔用的內存大,僅僅是個參考;對於高並發訪問的集群,使用該命令會造成QPS增加,帶來額外的性能開銷,建議在業務低峰或者從節點進行掃描。那針對 bigkey 導致延遲的問題,有什麼好的解決方案呢?
1)對大Key進行拆分
如將一個含有數萬成員的HASH Key拆分為多個HASH Key,並確保每個Key的成員數量在合理範圍。特別是在RedisCluster架構下中,大Key的拆分對各節點間的內存平衡能夠起到顯著作用。
2)優化使用刪除Key的命令。
Redis自4.0起提供了UNLINK命令,該命令可以替換DEL,能夠以非阻塞的方式放到後臺線程中緩慢逐步的清理大Key所佔用的內存塊,從而減輕了對Redis的影響;
Redis 6.0 以上版本,建議開啟 lazy-free 機制(配置參數:lazyfree-lazy-user-del = yes,6.2版本之後默認開啟了),這樣在 DEL刪除大Key時,釋放內存的動作也是在後臺線程中執行的;
3)儘量不寫入大Key
首先評估使用其他的存儲形式,比如文檔性資料庫 MongoDB等;如果還無法避免使用BigKey,可以將大Key進行壓縮後存儲,並儘量根據業務精簡Value的內容;建議單個Key的大小不要超過1K;
4.不合理使用批處理命令
網上有不少關於批量處理的一些優化,使用mget、mset代替多次的get、set等,減少網絡IO開銷以此提高redis的處理效率,特別是對於一些php短連接效果尤其明顯。
但是對於這些批量處理命令原生的mget、mset,非原生命令如pipeline,一定要注意控制單次批量操作的元素個數,否則會阻塞其它請求命令!建議控制在500以內。針對該種場景的優化方案:
降低使用 O(N) 以上複雜度的命令,對於數據的計算聚合操作等可以適當的放在應用程式側處理;使用O(N) 複雜度的命令時,保證 N 儘量的小(推薦 N <= 500),每次處理的更小的數據量,降低阻塞的時長;對於Hgetall、Smembers操作的集合對象,應從應用層面保證單個集合的成員個數不要過大,可以進行適當的拆分等。5.大批量Key集中過期
經常遇見反饋我的應用沒有上線變更調整,但是訪問的Redis經常出現超時的問題。分析後現象大部分表現為:超時問題出現的時間點有規律,比如每隔一個小時出現一次,或者每天零點過後發生。
如果出現了這種情況,那麼需要從兩個方面排查一下:
是否有定時任務的腳本程序,定時或者間隔性的操作RedisRedis的Key數量出現集中過期清理第一種情況這裡不做過多解讀,重點分析下Redis的Key數量為什麼會出現集中過期,集中過期為什麼會造成Redis的訪問變慢。
這就需要我們了解 Redis 的Key過期策略是怎樣的。Redis 的過期數據採用被動過期 主動過期兩種策略:
被動過期:只有應用發起訪問某個key 時,才判斷這個key是否已過期,如果已過期,則從Redis中刪除主動過期:在Redis 內部維護了一個定時任務,默認每隔 100 毫秒(1秒10次)從全局的過期哈希表中隨機取出 20 個 key,判斷然後刪除其中過期的 key,如果過期 key 的比例超過了 25%,則繼續重複此過程,直到過期 key 的比例下降到 25% 以下,或者這次任務的執行耗時超過了 25 毫秒,才會退出循環注意:Redis的key主動過期清理的定時任務,是在 Redis 主線程中執行的,也就意味著會阻塞正常的請求命令。進一步說就是如果在執行主動過期的過程中,出現了需要大量刪除過期 key 的請求,那麼此時應用程式在訪問 Redis 時,必須要等待這個過期任務執行結束,Redis 才可以繼續處理新請求。此時現象就是上面說的應用訪問 Redis 延時突然變大了。
特別是由於批量清理Key這個操作的命令是內部發起的並不會記錄在慢日誌中,但我們的應用程式卻感知到了延遲變大,其實時間都花費在了刪除過期 key 上,這種情況就經常被忽略。
如果確實是集中過期 key 導致的訪問變慢,那麼可以採用如下處理方案:
業務Key設置過期時間時,預計的過期時間加上一個隨機過期時間段,比如5分鐘,將集中過期時間打散,降低 Redis批量清理時的壓力。
由於這種情況分析比較麻煩,強烈建議對過期key的數量進行監控,對於短時間過期較多key的情況進行預警,通過執行info命令獲取過期Key數量【expired_keys】的統計值:
# Stats
total_connections_received:1359356
total_commands_processed:2705619999
instantaneous_ops_per_sec:157
total_net_input_bytes:232498789314
total_net_output_bytes:279219680360
instantaneous_input_kbps:11.01
instantaneous_output_kbps:17.07
rejected_connections:0
sync_full:2
sync_partial_ok:1
sync_partial_err:0
expired_keys:215099347
evicted_keys:0
keyspace_hits:984222771
keyspace_misses:610235483
pubsub_channels:1
pubsub_patterns:0
latest_fork_usec:9484
說明:expired_keys為一個累計值,可以在監控系統中配置為1分鐘的增加值,當1分鐘過期的key超過一定閾值時進行預警。
6.預估內存不足,使用的數據內存達到了最大值
由於伺服器內存有限,一般使用Redis時都會配置當前實例可用的最大內存maxmemory,那麼當使用的內存達到了 maxmemory 後,雖然配置了數據的自動淘汰策略,但是在此之後每次寫入新數據,操作延遲都會變長。
核心原因在於,當 Redis 內存達到 maxmemory 後,每次寫入新的數據之前,Redis 必須先從實例中剔除一部分數據,讓整個實例的內存維持在 maxmemory 之下,然後才能把新數據寫進來。
這裡很多同學會有誤解,以為只要配置了maxmemory就可以了,實際上由於Redis特殊的清理策略,無法避免會對正常的使用造成影響!
為了降低內存自動清理對服務的影響,可以配置Redis的最大內存數據清理策略,主要有以下幾種:
allkeys-lru:清理最近最少使用(LRU)的Key,不管 key 是否設置了過期時間volatile-lru:清理最近最少使用(LRU)的Key,但是只回收有設置過期的Keyallkeys-random:隨機清理部分Key,不管 key 是否設置了過期時間allkeys-lfu:不管 key 是否設置了過期,清理訪問頻次最低的 key(4.0 版本支持)volatile-lfu:清理訪問頻次最低且設置了過期時間 key(4.0 版本支持)volatile-random:隨機清理部分設置了過期時間的部分Keyvolatile-ttl:清理有設置過期的Key,嘗試先回收離 TTL 最短時間的Keynoeviction:不清理任何Key,當到達內存最大限制時,當客戶端嘗試執行命令時會導致更多內存佔用時直接返回錯誤(大多數寫命令,除了 DEL 和一些例外)。使用哪種清理策略,我們需要根據實際的應用場景來選擇,比如有些業務用於存儲強調準確性,即使訪問有損了也不能逐出數據,那麼就要配置noeviction;還有些業務是緩存,有些清理那些早期寫入的Key,則可以選擇volatile-lru或allkeys-lru。
介紹下常使用的是 allkeys-lru / volatile-lru 的淘汰策略,它們的處理邏輯是,每次從實例中隨機取出一批 key(maxmemory-samples控制數量),然後淘汰一個最少訪問的key,然後把剩餘的 key 暫存到一個池子中,繼續隨機取一批 key,並與之前池子中的 key 比較,再淘汰一個最少訪問的 key。以此循環往復,直到實例內存降到 maxmemory值以下才停止。所以這段時間是會影響新的數據寫入的,應用層就會有超時或者請求響應變慢的問題發生。
針對內存達到上限的情況,可以採用如下優化方案:
合理預估內存佔用,避免達到內存的使用上限。這裡有兩種方法可以參考:(1)根據寫入Key的類型、數量及平均大小計算預估,不同的數據類型有不同的數據結構及編碼方式,後續開文專門介紹;
(2)寫入一小部分比例的真實業務數據,然後進行預估。
設置合理的Key過期時間,滿足業務的最小保留時間即可。數據量過大建議拆分成多套Redis或者使用RedisCluster分片集群,建議單集群最大內存不超過20G。數據清理策略改為隨機模式,隨機清理比 LRU 要快很多(不過這個要根據業務情況評定,業務優先滿足原則)。如果使用的是 Redis 4.0 及以上版本,開啟 layz-free 機制,把淘汰 key 釋放內存的操作放到後臺線程中執行(配置 lazyfree-lazy-eviction = yes)增加剩餘可用內存的監控,提前預警並進行最大內存上限的擴容或者提前清理釋放內存。7.實際請求量超過了Redis的處理能力
Redis處理速度再快,也有達到上限的時候。特別是一些大促活動時,業務流量往往出現暴漲,很容易就會達到Redis的處理瓶頸。這種在業務上的表現除了訪問Redis變慢,一些簡單的命令如get、set也開始出現在慢日誌中。
這時如果查看Redis的CPU使用情況,基本是100%的狀態,那麼大概率就是達到Redis的處理能力上限了。
為了解決這種問題,就需要評估當前集群的處理吞吐力,參考官方的測評結果QPS 10W行不行?我們在上一篇文章介紹基本的壓測有過說明,每個Redis所在的伺服器配置不一樣,處理能力就不一樣。
更進一步說,每個Redis承載的服務模型不同,比如使用的命令類型、訪問比例等,那麼處理的吞吐也會有很大不同。針對這種情況最好的方案就是業務上線前,可以模擬真實的業務進行壓力測評,給出一個大概的吞吐處理能力。如果評估單節點無法承載過多請求,建議進行讀寫分離架構或者拆分為多套集群擴容.
最後就是不要忽略運維監控,可以對使用的CPU使用率、訪問的QPS等進行有效監控,提前發現是否達到集群的處理瓶頸,並決定是否進行擴容或架構調整。
說明:Redis監控指標還是比較多的,不管是性能指標、內存使用、持久化、網絡連接等,後面會專門發文介紹,大家到時也可以關注下。
如果這篇文章對你有幫助,還請幫忙點讚、轉發 一下,你的支持會激勵我們輸出更多高質量的文章,非常感謝!
如果你還想看更多優質文章,歡迎關注我的公號「資料庫架構師」,提升資料庫技能。
,