1. 程式人生 > >學會這幾個技巧,讓Redis大key問題遠離你

學會這幾個技巧,讓Redis大key問題遠離你

Redis大key的一些場景及問題

大key場景

Redis使用者應該都遇到過大key相關的場景,比如: 1、熱門話題下評論、答案排序場景。 2、大V的粉絲列表。 3、使用不恰當,或者對業務預估不準確、不及時進行處理垃圾資料等。

大key問題

由於Redis主執行緒為單執行緒模型,大key也會帶來一些問題,如: 1、叢集模式在slot分片均勻情況下,會出現資料和查詢傾斜情況,部分有大key的Redis節點佔用記憶體多,QPS高。

2、大key相關的刪除或者自動過期時,會出現qps突降或者突升的情況,極端情況下,會造成主從複製異常,Redis服務阻塞無法響應請求。大key的體積與刪除耗時可參考下表:

key型別 field數量 耗時 Hash ~100萬 ~1000ms List ~100萬 ~1000ms Set ~100萬 ~1000ms Sorted Set ~100萬 ~1000ms

Redis 4.0之前的大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的資訊。官方不推薦。

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一樣,不贅述。

總之,之前的方法要麼是用時較長離線解析,或者是不夠詳細的抽樣掃描,離理想的以記憶體為維度的線上掃描獲取詳細資訊有一定距離。由於在redis4.0前,沒有lazy free機制;針對掃描出來的大key,DBA只能通過hscan、sscan、zscan方式漸進刪除若干個元素;但面對過期刪除鍵的場景,這種取巧的刪除就無能為力。我們只能祈禱自動清理過期key剛好在系統低峰時,降低對業務的影響。

Redis 4.0之後的大key的發現與刪除方法 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.c 285⾏)

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

從上述原始碼看到memory usage是通過呼叫objectComputeSize來計算key的大小。我們來看objectComputeSize函式的邏輯。

#define OBJ_COMPUTE_SIZE_DEF_SAMPLES 5 /* Default sample size. / size_t objectComputeSize(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(struct dictEntry); samples++; } dictReleaseIterator(di); /根據上一步計算的抽樣樣本記憶體值除以樣本量,再乘以總的filed個數計算總記憶體值/ if (samples) asize += (double)elesize/samplesdictSize(d); /...*/ } (object.c 779⾏)

由此,我們發現memory usage預設抽樣5個field來迴圈累加計算整個key的記憶體大小,樣本的數量決定了key的記憶體大小的準確性和計算成本,樣本越大,迴圈次數越多,計算結果更精確,效能消耗也越多。

我們可以通過Python指令碼在叢集低峰時掃描Redis,用較小的代價去獲取所有key的記憶體大小。以下為部分虛擬碼,可根據實際情況設定大key閾值進行預警。

for key in r.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 / #define BIO_LAZY_FREE 2 / Deferred objects freeing. */ (bio.h 38⾏)

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

{"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0}, (server.c 137⾏)

void unlinkCommand(client *c) { delGenericCommand(c,1); } (db.c 490⾏)

通過這幾段原始碼可以看出del命令和unlink命令都是呼叫delGenericCommand,唯一的差別在於第二個引數不一樣。這個引數就是非同步刪除引數。

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

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

#define LAZYFREE_THRESHOLD 64 /定義後臺刪除的閾值,key的元素大於該閾值時才真正丟給後臺執行緒去刪除/ int dbAsyncDelete(redisDb db, robj key) { /.../ /lazyfreeGetFreeEffort來獲取val物件所包含的元素個數/ size_t free_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.c 53⾏)

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

/.../ else if (type == BIO_LAZY_FREE) { if (job->arg1) /* 後臺刪除物件函式,呼叫decrRefCount減少key的引用計數,引用計數為0時會真正的釋放資源 / lazyfreeFreeObjectFromBioThread(job->arg1); else if (job->arg2 && job->arg3) / 後臺清空資料庫字典,呼叫dictRelease迴圈遍歷資料庫字典刪除所有key / lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3); else if (job->arg3) / 後臺刪除key-slots對映表,在Redis叢集模式下會用*/ lazyfreeFreeSlotsMapFromBioThread(job->arg3); } (bio.c 197⾏)

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進行非同步刪除。

總結 在某些業務場景下,Redis大key的問題是難以避免的,但是,memory usage命令和lazyfree機制分別提供了記憶體維度的抽樣演算法和非同步刪除優化功能,這些特性有助於我們在實際業務中更好的預防大key的產生和解決大key造成的阻塞。關於Redis核心的優化思路也可從Redis作者Antirez的部落格中窺測一二,他提出"Lazy Redis is better Redis"、"Slow commands threading"(允許在不同的執行緒中執行慢操作命令),非同步化應該是Redis優化的主要方向。

Redis作為個推訊息推送的一項重要的基礎服務,效能的好壞至關重要。個推將Redis版本從2.8升級到5.0後,有效地解決了部分大key刪除或過期造成的阻塞問題。未來,個推將會持續關注Redis 5.0及後續的Redis 6.0,與大家共同探討如何更好地使用Redis。

參考文件: 1、http://antirez.com/news/93 2、http://ant