Redis大Key問題

Redis—大key問題討論及解決方案

一、問題背景

所謂的big key就是存儲本身的key值空間太大,或者hash,list,set等存儲中value值過多。主要包括:

  • 1、單個簡單的key存儲的value很大
  • 2、hash, set,zset,list 中存儲過多的元素
  • 3、一個集羣存儲了上億的key

bigkey 會帶來一些問題

  • 1.讀寫bigkey會導致超時嚴重,甚至阻塞服務。
  • 2.大key相關的刪除或者自動過期時,會出現qps突降或者突升的情況,極端情況下,會造成主從複製異常,Redis服務阻塞無法響應請求。bigkey的體積與刪除耗時可參考下表:
key類型 field數量 耗時
Hash 100萬 1000ms
List 100萬 1000ms
Set 100萬 1000ms
ZSet 100萬 1000ms

redis 是單線程,操作 bigkey 比較耗時,那麼阻塞 redis 的可能性增大。每次獲取 bigKey 的網絡流量較大,假設一個 bigkey 爲 1MB,每秒訪問量爲 1000,那麼每秒產生 1000MB 的流量,對於普通千兆網卡,按照字節算 128M/S 的服務器來說可能扛不住。而且一般服務器採用單機多實例方式來部署,所以還可能對其他實例造成影響。

二、redis單線程模型

1、redis單線程操作過程

  • redis 會將每個客戶端都關聯一個指令隊列。客戶端的指令通過隊列來按順序處理,先到先服務。
  • 在一個客戶端的指令隊列中的指令是順序執行的,但是多個指令隊列中的指令是無法保證順序的。
  • redis 同樣也會爲每個客戶端關聯一個響應隊列,通過響應隊列來順序地將指令的返回結果回覆給客戶端。
  • 一個響應隊列中的消息可以順序的回覆給客戶端,多個響應隊列之間是無法保證順序的。
  • 所有的客戶端的隊列中的指令或者響應,redis 每次都只能處理一個,同一時間絕對不會處理超過一個指令或者響應。

2、redis內部操作過程

redis基於Reactor模式開發了自己的網絡事件處理器: Redis通過socket與客戶端進行連接,並將服務器對socket的操作抽象爲文件事件。redis通過單線程,並通過I/O多路複用來處理來自客戶端的多個連接請求,當產生連接後,i/o多路複用程序,會將產生事件的套接字放置一個隊列,通過隊列以有序、同步的、每次一個套接字的方式向文件事件分派發器傳送套接字。當上一個套接字的事件被處理完畢後,I/O多路複用纔會向文件分派器傳送下一個套接字。服務端通過監聽這些事件,並完成相應的處理。被監聽的套接字準備好執行連接應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作,與操作相關的文件事件就會產生,這時文件事件處理器就會調用套接字之前關聯好的事件處理器來處理這些事件。

三、對於bigkey常用的解決辦法

1、單個簡單的key存儲的value很大時

  • 對象需要每次都整存整取時, 可以嘗試將對象分拆成幾個key-value, 使用multiGet獲取值,這樣分拆的意義在於分拆單次操作的壓力,將操作壓力平攤到多個redis實例中,降低對單個redis的IO影響;
  • 該對象每次只需要存取部分數據時, 可以像第一種做法一樣,分拆成幾個key-value, 也可以將這個存儲在一個hash中,每個field代表一個具體的屬性,使用hget,hmget來獲取部分的value,使用hset,hmset來更新部分屬性

2、 hash, set,zset,list 中存儲過多的元素時

  • 可以對存儲元素按一定規則進行分類,分散存儲到多個redis實例中。
  • 對於一些榜單類的場景,用戶一般只會訪問前幾百及後幾百條數據,可以只緩存前幾百條以及後幾百條,即對用戶經常訪問的數據做緩存(正序倒序的前幾頁),而不是全部都做,對於獲取中間的數據則可以直接從數據庫獲取

3、一個集羣存儲了上億的key時

如果key的個數過多會帶來更多的內存空間佔用,

  • key本身的佔用。
  • 集羣模式中,服務端有時需要建立一些slot2key的映射關係,這其中的指針佔用在key多的情況下也是浪費巨大空間。
  • 減少key的個數可以減少內存消耗,可以參考的方案是轉Hash結構存儲,即原先是直接使用Redis String 的結構存儲,現在將多個key存儲在一個Hash結構中

對緩存操作的改善可以利用pipeline管道

對於一些指令進行拆分之後可以考慮採用pipeline去取,由於redis是單線程的,一次只能執行一個命令,這裏採用Pipeline模式,一次發送多個命令,無需等待服務端返回。這樣就大大的減少了網絡往返時間,提高了系統性能。

Redis 大key的發現與刪除方法

  • 1、redis-rdb-tools工具。redis實例上執行bgsave,然後對dump出來的rdb文件進行分析,找到其中的大KEY。
  • 2、redis-cli --bigkeys命令。可以找到某個實例5種數據類型(String、hash、list、set、zset)的最大key。
  • 3、自定義的掃描腳本,以Python腳本居多,方法與redis-cli --bigkeys類似。
  • 4、debug object key命令。可以查看某個key序列化後的長度,每次只能查找單個key的信息。官方不推薦。
  • 5、Redis 4.0引入了memory usage命令和lazyfree機制,不管是對大key的發現,還是解決大key刪除或者過期造成的阻塞問題都有明顯的提升。

redis-rdb-tools工具

關於rdb工具的詳細介紹請查看鏈接https://github.com/sripathikrishnan/redis-rdb-tools,在此只介紹內存相關的使用方法。基本的命令爲 rdb -c memory dump.rdb (其中dump.rdb爲Redis實例的rdb文件,可通過bgsave生成)。輸出結果如下:

database,type,key,size_in_bytes,encoding,num_elements,len_largest_element

0,hash,hello1,1050,ziplist,86,22,

0,hash,hello2,2517,ziplist,222,8,

0,hash,hello3,2523,ziplist,156,12,

0,hash,hello4,62020,hashtable,776,32,

0,hash,hello5,71420,hashtable,1168,12,

可以看到輸出的信息包括數據類型,key、內存大小、編碼類型等。

Rdb工具優點在於獲取的key信息詳細、可選參數多、支持定製化需求,結果信息可選擇json或csv格式,後續處理方便,其缺點是需要離線操作,獲取結果時間較長

redis-cli --bigkeys命令

Redis-cli --bigkeys是redis-cli自帶的一個命令。它對整個redis進行掃描,尋找較大的key,並打印統計結果。

例如redis-cli -p 6379 --bigkeys

#Scanning the entire keyspace to find biggest keys as well as

#average sizes per key type. You can use -i 0.1 to sleep 0.1 sec

#per 100 SCAN commands (not usually needed).

[00.72%] Biggest hash found so far 'hello6' with 43 fields

[02.81%] Biggest string found so far 'hello7' with 31 bytes

[05.15%] Biggest string found so far 'hello8' with 32 bytes

[26.94%] Biggest hash found so far 'hello9' with 1795 fields

[32.00%] Biggest hash found so far 'hello10' with 4671 fields

[35.55%] Biggest string found so far 'hello11' with 36 bytes

-------- summary -------

Sampled 293070 keys in the keyspace!

Total key length in bytes is 8731143 (avg len 29.79)

Biggest string found 'hello11' has 36 bytes

Biggest hash found 'hello10' has 4671 fields

238027 strings with 2300436 bytes (81.22% of keys, avg size 9.66)

0 lists with 0 items (00.00% of keys, avg size 0.00)

0 sets with 0 members (00.00% of keys, avg size 0.00)

55043 hashs with 289965 fields (18.78% of keys, avg size 5.27)

0 zsets with 0 members (00.00% of keys, avg size 0.00)

我們可以看到打印結果分爲兩部分,掃描過程部分,只顯示了掃描到當前階段裏最大的key。summary部分給出了每種數據結構中最大的Key以及統計信息。

**redis-cli --bigkeys的優點是可以在線掃描,不阻塞服務;缺點是信息較少,內容不夠精確。**掃描結果中只有string類型是以字節長度爲衡量標準的。List、set、zset等都是以元素個數作爲衡量標準,元素個數多不能說明佔用內存就一定多。

自定義Python掃描腳本

通過strlen、hlen、scard等命令獲取字節大小或者元素個數,掃描結果比redis-cli --keys更精細,但是缺點和redis-cli --keys一樣。

Redis 4.0引入了memory usage命令和lazyfree機制,不管是對大key的發現,還是解決大key刪除或者過期造成的阻塞問題都有明顯的提升。

下面我們從源碼(摘自Redis 5.0.4版本)來理解memory usage和lazyfree的特點。

  • memory usage

{"memory",memoryCommand,-2,"rR",0,NULL,0,0,0,0,0}(server.c285⾏)void memoryCommand(client c) {/...//計算key大小是通過抽樣部分field來估算總大小。/elseif(!strcasecmp(c->argv[1]->ptr,"usage") &&c->argc >=3) { size_t usage = objectComputeSize(dictGetVal(de),samples);/...*/ }}(object.c1299⾏)

從上述源碼看到memory usage是通過調用objectComputeSize來計算key的大小。我們來看objectComputeSize函數的邏輯。

#defineOBJ_COMPUTE_SIZE_DEF_SAMPLES 5 /* Default sample size. /size_tobjectComputeSize(robj o, size_t sample_size){/...代碼對數據類型進行了分類,此處只取hash類型說明//...//循環抽樣個field,累加獲取抽樣樣本內存值,默認抽樣樣本爲5/while((de = dictNext(di)) != NULL && samples < sample_size) { ele = dictGetKey(de); ele2 = dictGetVal(de); elesize += sdsAllocSize(ele) + sdsAllocSize(ele2);elesize +=sizeof(structdictEntry); samples++; } dictReleaseIterator(di);/根據上一步計算的抽樣樣本內存值除以樣本量,再乘以總的filed個數計算總內存值/if(samples) asize += (double)elesize/samplesdictSize(d);/...*/ }(object.c779⾏)

由此,我們發現memory usage默認抽樣5個field來循環累加計算整個key的內存大小,樣本的數量決定了key的內存大小的準確性和計算成本,樣本越大,循環次數越多,計算結果更精確,性能消耗也越多。

我們可以通過Python腳本在集羣低峯時掃描Redis,用較小的代價去獲取所有key的內存大小。以下爲部分僞代碼,可根據實際情況設置大key閾值進行預警。

forkeyinr.scan_iter(count=1000):redis-cli ='/usr/bin/redis-cli'configcmd ='%s -h %s -p %s memory usage %s'% (redis-cli, rip,rport,key) keymemory = commands.getoutput(configcmd)

  • lazyfree機制

**Lazyfree的原理是在刪除的時候只進行邏輯刪除,把key釋放操作放在bio(Background I/O)單獨的子線程處理中,減少刪除大key對redis主線程的阻塞,有效地避免因刪除大key帶來的性能問題。**在此提一下bio線程,很多人把Redis通常理解爲單線程內存數據庫, 其實不然。Redis將最主要的網絡收發和執行命令等操作都放在了主工作線程,然而除此之外還有幾個bio後臺線程,從源碼中可以看到有處理關閉文件和刷盤的後臺線程,以及Redis4.0新增加的lazyfree線程。

/* Background job opcodes /#defineBIO_LAZY_FREE 2/ Deferred objects freeing. */(bio.h38⾏)

下面我們以unlink命令爲例,來理解lazyfree的實現原理。

{"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0},(server.c137⾏)void unlinkCommand(client *c) {delGenericCommand(c,1);}(db.c490⾏)

通過這幾段源碼可以看出del命令和unlink命令都是調用delGenericCommand,唯一的差別在於第二個參數不一樣。這個參數就是異步刪除參數。

/* This command implements DEL and LAZYDEL. /void delGenericCommand(client c, intlazy) {/.../int deleted =lazy? dbAsyncDelete(c->db,c->argv[j]) :dbSyncDelete(c->db,c->argv[j]);/.../}(db.c468⾏)

可以看到delGenericCommand函數根據lazy參數來決定是同步刪除還是異步刪除。當執行unlink命令時,傳入lazy參數值1,調用異步刪除函數dbAsyncDelete。否則執行del命令傳入參數值0,調用同步刪除函數dbSyncDelete。我們重點來看異步刪除dbAsyncDelete的實現邏輯:

#defineLAZYFREE_THRESHOLD 64/定義後臺刪除的閾值,key的元素大於該閾值時才真正丟給後臺線程去刪除/intdbAsyncDelete(redisDb db, robj key){/...//lazyfreeGetFreeEffort來獲取val對象所包含的元素個數/size_tfree_effort = lazyfreeGetFreeEffort(val);/* 對刪除key進行判斷,滿足閾值條件時進行後臺刪除 /if(free_effort > LAZYFREE_THRESHOLD && val->refcount ==1) {atomicIncr(lazyfree_objects,1);bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);/將刪除對象放入BIO_LAZY_FREE後臺線程任務隊列/dictSetVal(db->dict,de,NULL);/將第一步獲取到的val值設置爲null/ }/...*/}(lazyfree.c53⾏)

上面提到了當刪除key滿足閾值條件時,會將key放入BIO_LAZY_FREE後臺線程任務隊列。接下來我們來看BIO_LAZY_FREE後臺線程。

/.../elseif(type == BIO_LAZY_FREE) {if(job->arg1)/* 後臺刪除對象函數,調用decrRefCount減少key的引用計數,引用計數爲0時會真正的釋放資源 / lazyfreeFreeObjectFromBioThread(job->arg1);elseif(job->arg2 && job->arg3)/ 後臺清空數據庫字典,調用dictRelease循環遍歷數據庫字典刪除所有key / lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);elseif(job->arg3)/ 後臺刪除key-slots映射表,在Redis集羣模式下會用*/ lazyfreeFreeSlotsMapFromBioThread(job->arg3);}(bio.c197⾏)

unlink命令的邏輯可以總結爲:執行unlink調用delGenericCommand函數傳入lazy參數值1,來調用異步刪除函數dbAsyncDelete,將滿足閾值的大key放入BIO_LAZY_FREE後臺線程任務隊列進行異步刪除。類似的後臺刪除命令還有flushdb async、flushall async。它們的原理都是獲取刪除標識進行判斷,然後調用異步刪除函數emptyDbAsnyc來清空數據庫。這些命令具體的實現邏輯可自行查看flushdbCommand部分源碼,在此不做贅述。

**除了主動的大key刪除和數據庫清空操作外,過期key驅逐引發的刪除操作也會阻塞Redis服務。**因此Redis4.0除了增加上述三個後臺刪除的命令外,還增加了4個後臺刪除配置項,分別爲slave-lazy-flush、lazyfree-lazy-eviction、lazyfree-lazy-expire和lazyfree-lazy-server-del。

  • slave-lazy-flush:slave接收完RDB文件後清空數據選項。建議大家開啓slave-lazy-flush,這樣可減少slave節點flush操作時間,從而降低主從全量同步耗時的可能性。
  • lazyfree-lazy-eviction:內存用滿逐出選項。若開啓此選項可能導致淘汰key的內存釋放不夠及時,內存超用。
  • lazyfree-lazy-expire:過期key刪除選項。建議開啓。
  • lazyfree-lazy-server-del:內部刪除選項,比如rename命令將oldkey修改爲一個已存在的newkey時,會先將newkey刪除掉。如果newkey是一個大key,可能會引起阻塞刪除。建議開啓。

上述四個後臺刪除相關的參數實現邏輯差異不大,都是通過參數選項進行判斷,從而選擇是否採用dbAsyncDelete或者emptyDbAsync進行異步刪除。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章