Redis 愈來愈慢?常見延遲問題定位與分析

image
來源:http://kaito-kidd.com/2020/07...redis

Redis做爲內存數據庫,擁有很是高的性能,單個實例的QPS可以達到10W左右。但咱們在使用Redis時,常常時不時會出現訪問延遲很大的狀況,若是你不知道Redis的內部實現原理,在排查問題時就會一頭霧水。數據庫

不少時候,Redis出現訪問延遲變大,都與咱們的使用不當或運維不合理致使的。緩存

這篇文章咱們就來分析一下Redis在使用過程當中,常常會遇到的延遲問題以及如何定位和分析。安全

使用複雜度高的命令

若是在使用Redis時,發現訪問延遲忽然增大,如何進行排查?網絡

首先,第一步,建議你去查看一下Redis的慢日誌。Redis提供了慢日誌命令的統計功能,咱們經過如下設置,就能夠查看有哪些命令在執行時延遲比較大。app

首先設置Redis的慢日誌閾值,只有超過閾值的命令纔會被記錄,這裏的單位是微秒,例如設置慢日誌的閾值爲5毫秒,同時設置只保留最近1000條慢日誌記錄:運維

# 命令執行超過5毫秒記錄慢日誌
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近1000條慢日誌
CONFIG SET slowlog-max-len 1000

設置完成以後,全部執行的命令若是延遲大於5毫秒,都會被Redis記錄下來,咱們執行SLOWLOG get 5查詢最近5條慢日誌dom

127.0.0.1:6379> SLOWLOG get5
1)1)(integer)32693# 慢日誌ID
2)(integer)1593763337# 執行時間
3)(integer)5299# 執行耗時(微秒)
4)1)"LRANGE"# 具體執行的命令和參數
2)"user_list_2000"
3)"0"
4)"-1"
2)1)(integer)32692
2)(integer)1593763337
3)(integer)5044
4)1)"GET"
2)"book_price_1000"
...

經過查看慢日誌記錄,咱們就能夠知道在什麼時間執行哪些命令比較耗時,若是你的業務常用O(n)以上覆雜度的命令,例如sort、sunion、zunionstore,或者在執行O(n)命令時操做的數據量比較大,這些狀況下Redis處理數據時就會很耗時。異步

若是你的服務請求量並不大,但Redis實例的CPU使用率很高,頗有多是使用了複雜度高的命令致使的。性能

解決方案就是,不使用這些複雜度較高的命令,而且一次不要獲取太多的數據,每次儘可能操做少許的數據,讓Redis能夠及時處理返回

存儲大key

若是查詢慢日誌發現,並非複雜度較高的命令致使的,例如都是SET、DELETE操做出如今慢日誌記錄中,那麼你就要懷疑是否存在Redis寫入了大key的狀況。

Redis在寫入數據時,須要爲新的數據分配內存,當從Redis中刪除數據時,它會釋放對應的內存空間。

若是一個key寫入的數據很是大,Redis在分配內存時也會比較耗時。一樣的,當刪除這個key的數據時,釋放內存也會耗時比較久

你須要檢查你的業務代碼,是否存在寫入大key的狀況,須要評估寫入數據量的大小,業務層應該避免一個key存入過大的數據量

那麼有沒有什麼辦法能夠掃描如今Redis中是否存在大key的數據嗎?

Redis也提供了掃描大key的方法:

redis-cli -h $host -p $port --bigkeys -i 0.01

使用上面的命令就能夠掃描出整個實例key大小的分佈狀況,它是以類型維度來展現的。

須要注意的是當咱們在線上實例進行大key掃描時,Redis的QPS會突增,爲了下降掃描過程當中對Redis的影響,咱們須要控制掃描的頻率,使用-i參數控制便可,它表示掃描過程當中每次掃描的時間間隔,單位是秒。

使用這個命令的原理,其實就是Redis在內部執行scan命令,遍歷全部key,而後針對不一樣類型的key執行strlen、llen、hlen、scard、zcard來獲取字符串的長度以及容器類型(list/dict/set/zset)的元素個數。

而對於容器類型的key,只能掃描出元素最多的key,但元素最多的key不必定佔用內存最多,這一點須要咱們注意下。不過使用這個命令通常咱們是能夠對整個實例中key的分佈狀況有比較清晰的瞭解。

針對大key的問題,Redis官方在4.0版本推出了lazy-free的機制,用於異步釋放大key的內存,下降對Redis性能的影響。即便這樣,咱們也不建議使用大key,大key在集羣的遷移過程當中,也會影響到遷移的性能,這個後面在介紹集羣相關的文章時,會再詳細介紹到。

集中過時

有時你會發現,平時在使用Redis時沒有延時比較大的狀況,但在某個時間點忽然出現一波延時,並且報慢的時間點頗有規律,例如某個整點,或者間隔多久就會發生一次。

若是出現這種狀況,就須要考慮是否存在大量key集中過時的狀況。

若是有大量的key在某個固定時間點集中過時,在這個時間點訪問Redis時,就有可能致使延遲增長。

Redis的過時策略採用主動過時+懶惰過時兩種策略:

•主動過時:Redis內部維護一個定時任務,默認每隔100毫秒會從過時字典中隨機取出20個key,刪除過時的key,若是過時key的比例超過了25%,則繼續獲取20個key,刪除過時的key,循環往復,直到過時key的比例降低到25%或者此次任務的執行耗時超過了25毫秒,纔會退出循環

•懶惰過時:只有當訪問某個key時,才判斷這個key是否已過時,若是已通過期,則從實例中刪除

注意,Redis的主動過時的定時任務,也是在Redis**主線程**中執行的,也就是說若是在執行主動過時的過程當中,出現了須要大量刪除過時key的狀況,那麼在業務訪問時,必須等這個過時任務執行結束,才能夠處理業務請求。此時就會出現,業務訪問延時增大的問題,最大延遲爲25毫秒。

並且這個訪問延遲的狀況,不會記錄在慢日誌裏。慢日誌中只記錄真正執行某個命令的耗時,Redis主動過時策略執行在操做命令以前,若是操做命令耗時達不到慢日誌閾值,它是不會計算在慢日誌統計中的,但咱們的業務卻感到了延遲增大。

此時你須要檢查你的業務,是否真的存在集中過時的代碼,通常集中過時使用的命令是expireatpexpireat命令,在代碼中搜索這個關鍵字就能夠了。

若是你的業務確實須要集中過時掉某些key,又不想致使Redis發生抖動,有什麼優化方案?

解決方案是,在集中過時時增長一個`隨機時間`,把這些須要過時的key的時間打散便可

僞代碼能夠這麼寫:

# 在過時時間點以後的5分鐘內隨機過時掉
redis.expireat(key, expire_time + random(300))

這樣Redis在處理過時時,不會由於集中刪除key致使壓力過大,阻塞主線程。

另外,除了業務使用須要注意此問題以外,還能夠經過運維手段來及時發現這種狀況。

作法是咱們須要把Redis的各項運行數據監控起來,執行info能夠拿到全部的運行數據,在這裏咱們須要重點關注expired_keys這一項,它表明整個實例到目前爲止,累計刪除過時key的數量。

咱們須要對這個指標監控,當在很短期內這個指標出現突增時,須要及時報警出來,而後與業務報慢的時間點對比分析,確認時間是否一致,若是一致,則能夠認爲確實是由於這個緣由致使的延遲增大。

實例內存達到上限

有時咱們把Redis當作純緩存使用,就會給實例設置一個內存上限maxmemory,而後開啓LRU淘汰策略。

當實例的內存達到了maxmemory後,你會發現以後的每次寫入新的數據,有可能變慢了。

致使變慢的緣由是,當Redis內存達到maxmemory後,每次寫入新的數據以前,必須先踢出一部分數據,讓內存維持在maxmemory之下。

這個踢出舊數據的邏輯也是須要消耗時間的,而具體耗時的長短,要取決於配置的淘汰策略:

•allkeys-lru:無論key是否設置了過時,淘汰最近最少訪問的key

•volatile-lru:只淘汰最近最少訪問並設置過時的key

•allkeys-random:無論key是否設置了過時,隨機淘汰

•volatile-random:只隨機淘汰有設置過時的key

•allkeys-ttl:無論key是否設置了過時,淘汰即將過時的key

•noeviction:不淘汰任何key,滿容後再寫入直接報錯

•allkeys-lfu:無論key是否設置了過時,淘汰訪問頻率最低的key(4.0+支持)

•volatile-lfu:只淘汰訪問頻率最低的過時key(4.0+支持)

備註: allkeys-xxx表示從 全部的鍵值中淘汰數據,而 volatile-xxx表示從設置了 過時鍵的鍵值中淘汰數據。

具體使用哪一種策略,須要根據業務場景來決定。

咱們最常使用的通常是allkeys-lruvolatile-lru策略,它們的處理邏輯是,每次從實例中隨機取出一批key(可配置),而後淘汰一個最少訪問的key,以後把剩下的key暫存到一個池子中,繼續隨機取出一批key,並與以前池子中的key比較,再淘汰一個最少訪問的key。以此循環,直到內存降到maxmemory之下。

若是使用的是allkeys-randomvolatile-random策略,那麼就會快不少,由於是隨機淘汰,那麼就少了比較key訪問頻率時間的消耗了,隨機拿出一批key後直接淘汰便可,所以這個策略要比上面的LRU策略執行快一些。

但以上這些邏輯都是在訪問Redis時,真正命令執行以前執行的,也就是它會影響咱們訪問Redis時執行的命令。

另外,若是此時Redis實例中有存儲大key,那麼在淘汰大key釋放內存時,這個耗時會更加久,延遲更大,這須要咱們格外注意。

若是你的業務訪問量很是大,而且必須設置maxmemory限制實例的內存上限,同時面臨淘汰key致使延遲增大的的狀況,要想緩解這種狀況,除了上面說的避免存儲大key、使用隨機淘汰策略以外,也能夠考慮拆分實例的方法來緩解,拆分實例能夠把一個實例淘汰key的壓力分攤到多個實例上,能夠在必定程度下降延遲。

fork耗時嚴重

若是你的Redis開啓了自動生成RDB和AOF重寫功能,那麼有可能在後臺生成RDB和AOF重寫時致使Redis的訪問延遲增大,而等這些任務執行完畢後,延遲狀況消失。

遇到這種狀況,通常就是執行生成RDB和AOF重寫任務致使的。

生成RDB和AOF都須要父進程fork出一個子進程進行數據的持久化,在fork執行過程當中,父進程須要拷貝**內存頁表**給子進程,若是整個實例內存佔用很大,那麼須要拷貝的內存頁表會比較耗時,此過程會消耗大量的CPU資源,在完成fork以前,整個實例會被阻塞住,沒法處理任何請求,若是此時CPU資源緊張,那麼fork的時間會更長,甚至達到秒級。這會嚴重影響Redis的性能

咱們能夠執行info命令,查看最後一次fork執行的耗時latest_fork_usec,單位微秒。這個時間就是整個實例阻塞沒法處理請求的時間。

除了由於備份的緣由生成RDB以外,在主從節點第一次創建數據同步時,主節點也會生成RDB文件給從節點進行一次全量同步,這時也會對Redis產生性能影響。

要想避免這種狀況,咱們須要規劃好數據備份的週期,建議在從節點上執行備份,並且最好放在**低峯期**執行若是對於丟失數據不敏感的業務,那麼不建議開啓AOF和AOF重寫功能。

另外,fork的耗時也與系統有關,若是把Redis部署在虛擬機上,那麼這個時間也會增大。因此使用Redis時建議部署在物理機上,下降fork的影響。

綁定CPU

不少時候,咱們在部署服務時,爲了提升性能,下降程序在使用多個CPU時上下文切換的性能損耗,通常會採用進程綁定CPU的操做。

但在使用Redis時,咱們不建議這麼幹,緣由以下:

綁定CPU的Redis,在進行數據持久化時,fork出的子進程,子進程會繼承父進程的CPU使用偏好,而此時子進程會消耗大量的CPU資源進行數據持久化, 子進程會與主進程發生CPU爭搶,這也會致使主進程的CPU資源不足訪問延遲增大。

因此在部署Redis進程時,若是須要開啓RDB和AOF重寫機制,必定不能進行CPU綁定操做!

開啓AOF

上面提到了,當執行AOF文件重寫時會由於fork執行耗時致使Redis延遲增大,除了這個以外,若是開啓AOF機制,設置的策略不合理,也會致使性能問題。

開啓AOF後,Redis會把寫入的命令實時寫入到文件中,但寫入文件的過程是先寫入內存,等內存中的數據超過必定閾值或達到必定時間後,內存中的內容纔會被真正寫入到磁盤中。

AOF爲了保證文件寫入磁盤的安全性,提供了3種刷盤機制:

•appendfsync always:每次寫入都刷盤,對性能影響最大,佔用磁盤IO比較高,數據安全性最高

•appendfsync everysec:1秒刷一次盤,對性能影響相對較小,節點宕機時最多丟失1秒的數據

•appendfsync no:按照操做系統的機制刷盤,對性能影響最小,數據安全性低,節點宕機丟失數據取決於操做系統刷盤機制

當使用第一種機制appendfsync always時,Redis每處理一次寫命令,都會把這個命令寫入磁盤,並且這個操做是在主線程中執行的

內存中的的數據寫入磁盤,這個會加劇磁盤的IO負擔,操做磁盤成本要比操做內存的代價大得多。若是寫入量很大,那麼每次更新都會寫入磁盤,此時機器的磁盤IO就會很是高,拖慢Redis的性能,所以咱們不建議使用這種機制。

與第一種機制對比,appendfsync everysec會每隔1秒刷盤,而appendfsync no取決於操做系統的刷盤時間,安全性不高。所以咱們推薦使用appendfsync everysec這種方式,在最壞的狀況下,只會丟失1秒的數據,但它能保持較好的訪問性能。

固然,對於有些業務場景,對丟失數據並不敏感,也能夠不開啓AOF。

使用Swap

若是你發現Redis忽然變得很是慢,每次訪問的耗時都達到了幾百毫秒甚至秒級,那此時就檢查Redis是否使用到了Swap,這種狀況下Redis基本上已經沒法提供高性能的服務。

咱們知道,操做系統提供了Swap機制,目的是爲了當內存不足時,能夠把一部份內存中的數據換到磁盤上,以達到對內存使用的緩衝。

但當內存中的數據被換到磁盤上後,訪問這些數據就須要從磁盤中讀取,這個速度要比內存慢太多!

尤爲是針對Redis這種高性能的內存數據庫來講,若是Redis中的內存被換到磁盤上,對於Redis這種性能極其敏感的數據庫,這個操做時間是沒法接受的

咱們須要檢查機器的內存使用狀況,確認是否確實是由於內存不足致使使用到了Swap。

若是確實使用到了Swap,要及時整理內存空間,釋放出足夠的內存供Redis使用,而後釋放Redis的Swap,讓Redis從新使用內存。

釋放Redis的Swap過程一般要重啓實例,爲了不重啓實例對業務的影響,通常先進行主從切換,而後釋放舊主節點的Swap,從新啓動服務,待數據同步完成後,再切換回主節點便可。

可見,當Redis使用到Swap後,此時的Redis的高性能基本被廢掉,因此咱們須要提早預防這種狀況。

咱們須要對Redis機器的內存和Swap使用狀況進行監控,在內存不足和使用到Swap時及時報警出來,及時進行相應的處理

網卡負載太高

若是以上產生性能問題的場景,你都規避掉了,並且Redis也穩定運行了很長時間,但在某個時間點以後開始,訪問Redis開始變慢了,並且一直持續到如今,這種狀況是什麼緣由致使的?

以前咱們就遇到這種問題,特色就是從某個時間點以後就開始變慢,而且一直持續。這時你須要檢查一下機器的網卡流量,是否存在網卡流量被跑滿的狀況。

網卡負載太高,在網絡層和TCP層就會出現數據發送延遲、數據丟包等狀況。Redis的高性能除了內存以外,就在於網絡IO,請求量突增會致使網卡負載變高。

若是出現這種狀況,你須要排查這個機器上的哪一個Redis實例的流量過大佔滿了網絡帶寬,而後確認流量突增是否屬於業務正常狀況,若是屬於那就須要及時擴容或遷移實例,避免這個機器的其餘實例受到影響。

運維層面,咱們須要對機器的各項指標增長監控,包括網絡流量,在達到閾值時提早報警,及時與業務確認並擴容。

總結

以上咱們總結了Redis中常見的可能致使延遲增大甚至阻塞的場景,這其中既涉及到了業務的使用問題,也涉及到Redis的運維問題。

可見,要想保證Redis高性能的運行,其中涉及到CPU、內存、網絡,甚至磁盤的方方面面,其中還包括操做系統的相關特性的使用。

做爲開發人員,咱們須要瞭解Redis的運行機制,例如各個命令的執行時間複雜度、數據過時策略、數據淘汰策略等,使用合理的命令,並結合業務場景進行優化。

做爲DBA運維人員,須要瞭解數據持久化、操做系統fork原理、Swap機制等,並對Redis的容量進行合理規劃,預留足夠的機器資源,對機器作好完善的監控,才能保證Redis的穩定運行。

image

相關文章
相關標籤/搜索