天天看點

Redis常見延遲問題排查手冊!附優化建議

作者:呆萌小可萘日常

Redis作為記憶體資料庫,擁有非常高的性能,單個執行個體的QPS能夠達到10W左右。但我們在使用Redis時,經常時不時會出現通路延遲很大的情況,如果你不知道Redis的内部實作原理,在排查問題時就會一頭霧水。

很多時候,Redis出現通路延遲變大,都與我們的使用不當或運維不合理導緻的。

Redis變慢了?常見延遲問題定位與分析

下面我們就來分析一下Redis在使用過程中,經常會遇到的延遲問題以及如何定位和分析。

使用複雜度高的指令

如果在使用Redis時,發現通路延遲突然增大,如何進行排查?

首先,第一步,建議你去檢視一下Redis的慢日志。Redis提供了慢日志指令的統計功能,我們通過以下設定,就可以檢視有哪些指令在執行時延遲比較大。

首先設定Redis的慢日志門檻值,隻有超過門檻值的指令才會被記錄,這裡的機關是微妙,例如設定慢日志的門檻值為5毫秒,同時設定隻保留最近1000條慢日志記錄:

# 指令執行超過5毫秒記錄慢日志

CONFIG SET slowlog-log-slower-than 5000

# 隻保留最近1000條慢日志

CONFIG SET slowlog-max-len 1000

設定完成之後,所有執行的指令如果延遲大于5毫秒,都會被Redis記錄下來,我們執行SLOWLOG get 5查詢最近5條慢日志:

127.0.0.1:6379> SLOWLOG get 5

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主動過期政策執行在操作指令之前,如果操作指令耗時達不到慢日志門檻值,它是不會計算在慢日志統計中的,但我們的業務卻感到了延遲增大。

此時你需要檢查你的業務,是否真的存在集中過期的代碼,一般集中過期使用的指令是expireat或pexpireat指令,在代碼中搜尋這個關鍵字就可以了。

如果你的業務确實需要集中過期掉某些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-lru或volatile-lru政策,它們的處理邏輯是,每次從執行個體中随機取出一批key(可配置),然後淘汰一個最少通路的key,之後把剩下的key暫存到一個池子中,繼續随機取出一批key,并與之前池子中的key比較,再淘汰一個最少通路的key。以此循環,直到記憶體降到maxmemory之下。

如果使用的是allkeys-random或volatile-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的性能。

具體原理也可以參考我之前寫的文章:Redis持久化是如何做的?RDB和AOF對比分析。

我們可以執行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的穩定運作。

Redis的最佳實踐方式:業務層面和運維層面

在上文中,主要講解了 Redis 常見的導緻變慢的場景以及問題定位和分析,主要是由業務使用不合理和運維不當導緻的。

我們在了解了導緻Redis變慢的原因之後,針對性地優化,就可以讓Redis穩定發揮出更高性能。

接着就來總結一下,在使用Redis時的最佳實踐方式,主要包含兩個層面:業務層面、運維層面。

由于我之前寫過很多UGC後端服務,在大量場景下用到了Redis,這個過程中也踩過很多坑,是以在使用過程中也總結了一套合理的使用方法。

後來做基礎架構,開發Codis、Redis相關的中間件,在這個階段關注領域從使用層面下沉到Redis的開發和運維,更多聚焦在Redis的内部實作和運維過程中産生的各種問題,在這塊也積累了一些經驗。

下面就針對這兩塊,某公司我認為比較合理的Redis使用和運維方法,不一定最全面,也可能與你使用Redis的方法不同,但以下這些方法都是我在踩坑之後總結的實際經驗,供你參考。

業務層面

業務層面主要是開發人員需要關注,也就是開發人員在寫業務代碼時,如何合理地使用Redis。開發人員需要對Redis有基本的了解,才能在合适的業務場景使用Redis,進而避免業務層面導緻的延遲問題。

在開發過程中,業務層面的優化建議如下:

key的長度盡量要短,在資料量非常大時,過長的key名會占用更多的記憶體;

一定避免存儲過大的資料(大value),過大的資料在配置設定記憶體和釋放記憶體時耗時嚴重,會阻塞主線程;

Redis 4.0以上建議開啟lazy-free機制,釋放大value時異步操作,不阻塞主線程;

建議設定過期時間,把Redis當做緩存使用,尤其在數量很大的時,不設定過期時間會導緻記憶體的無限增長;

不使用複雜度過高的指令,例如SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE,使用這些指令耗時較久,會阻塞主線程;

查詢資料時,一次盡量擷取較少的資料,在不确定容器元素個數的情況下,避免使用LRANGE key 0 -1,ZRANGE key 0 -1這類操作,應該設定具體查詢的元素個數,推薦一次查詢100個以下元素;

寫入資料時,一次盡量寫入較少的資料,例如HSET key value1 value2 value3...,控制一次寫入元素的數量,推薦在100以下,大資料量分多個批次寫入;

批量操作資料時,用MGET/MSET替換GET/SET、HMGET/MHSET替換HGET/HSET,減少請求來回的網絡IO次數,降低延遲,對于沒有批量操作的指令,推薦使用pipeline,一次性發送多個指令到服務端;

禁止使用KEYS指令,需要掃描執行個體時,建議使用SCAN,線上操作一定要控制掃描的頻率,避免對Redis産生性能抖動

避免某個時間點集中過期大量的key,集中過期時推薦增加一個随機時間,把過期時間打散,降低集中過期key時Redis的壓力,避免阻塞主線程;

根據業務場景,選擇合适的淘汰政策,通常随機過期要比LRU過期淘汰資料更快;

使用連接配接池通路Redis,并配置合理的連接配接池參數,避免短連接配接,TCP三向交握和四次揮手的耗時也很高;

隻使用db0,不推薦使用多個db,使用多個db會增加Redis的負擔,每次通路不同的db都需要執行SELECT指令,如果業務線不同,建議拆分多個執行個體,還能提高單個執行個體的性能;

讀的請求量很大時,推薦使用讀寫分離,前提是可以容忍從節資料更新不及時的問題;

寫請求量很大時,推薦使用叢集,部署多個執行個體分攤寫壓力。

運維層面

運維層面主要是DBA需要關注的,目的是合理規劃Redis的部署和保障Redis的穩定運作,主要優化如下:

不同業務線部署不同的執行個體,各自獨立,避免混用,推薦不同業務線使用不同的機器,根據業務重要程度劃分不同的分組來部署,避免某一個業務線出現問題影響其他業務線;

保證機器有足夠的CPU、記憶體、帶寬、磁盤資源,防止負載過高影響Redis性能;

以master-slave叢集方式部署執行個體,并分布在不同機器上,避免單點,slave必須設定為readonly;

master和slave節點所在機器,各自獨立,不要交叉部署執行個體,通常備份工作會在slave上做,做備份時會消耗機器資源,交叉部署會影響到master的性能;

推薦部署哨兵節點增加可用性,節點數量至少3個,并分布在不同機器上,實作故障自動故障轉移;

提前做好容量規劃,一台機器部署執行個體的記憶體上限,最好是機器記憶體的一半,主從全量同步時會占用最多額外一倍的記憶體空間,防止網絡大面積故障引發所有master-slave的全量同步導緻機器記憶體被吃光;

做好機器的CPU、記憶體、帶寬、磁盤監控,在資源不足時及時報警處理,Redis使用Swap後性能急劇下降,網絡帶寬負載過高通路延遲明顯增大,磁盤IO過高時開啟AOF會拖慢Redis的性能;

設定最大連接配接數上限,防止過多的用戶端連接配接導緻服務負載過高;

單個執行個體的使用記憶體建議控制在10G以下,過大的執行個體會導緻備份時間久、資源消耗多,主從全量同步資料時間阻塞時間更長;

設定合理的slowlog門檻值,推薦10毫秒,并對其進行監控,産生過多的慢日志需要及時報警;

設定合理的複制緩沖區repl-backlog大小,适當調大repl-backlog可以降低主從全量複制的機率;

設定合理的slave節點client-output-buffer-limit大小,對于寫入量很大的執行個體,适當調大可以避免主從複制中斷問題;

備份時推薦在slave節點上做,不影響master性能;

不開啟AOF或開啟AOF配置為每秒刷盤,避免磁盤IO消耗降低Redis性能;

當執行個體設定了記憶體上限,需要調大記憶體上限時,先調整slave再調整master,否則會導緻主從節點資料不一緻;

對Redis增加監控,監控采集info資訊時,使用長連接配接,頻繁的短連接配接也會影響Redis性能;

線上掃描整個執行個體數時,記得設定休眠時間,避免掃描時QPS突增對Redis産生性能抖動;

做好Redis的運作時監控,尤其是expired_keys、evicted_keys、latest_fork_usec名額,短時間内這些名額值突增可能會阻塞整個執行個體,引發性能問題。

以上就是我在使用Redis和開發Redis相關中間件時,總結出來Redis推薦的實踐方法,以上提出的這些方面,都或多或少在實際使用中遇到過。

可見,要想穩定發揮Redis的高性能,需要在各個方面做好工作,但凡某一個方面出現問題,必然會影響到Redis的性能,這對我們使用和運維提出了更高的要求。

如果你在使用Redis過程中,遇到更多的問題或者有更好的使用經驗,可以留言一起探讨!

繼續閱讀