個推做爲國內第三方推送市場的早期進入者,專一於爲開發者提供高效穩定的推送服務,通過9年的積累和發展,服務了包括新浪、滴滴在內的數十萬APP。因爲咱們推送業務對併發量、速度要求很高,爲此,咱們選擇了高性能的內存數據庫Redis。然而,在實際業務場景中咱們也遇到了一些Redis大key形成的服務阻塞問題,所以積累了一些應對經驗。本文將對大key的發現、解決大key刪除形成的阻塞作相應的介紹。git
Redis大key的一些場景及問題github
大key場景redis
Redis使用者應該都遇到過大key相關的場景,好比: 一、熱門話題下評論、答案排序場景。 二、大V的粉絲列表。 三、使用不恰當,或者對業務預估不許確、不及時進行處理垃圾數據等。算法
大key問題數據庫
因爲Redis主線程爲單線程模型,大key也會帶來一些問題,如: 一、集羣模式在slot分片均勻狀況下,會出現數據和查詢傾斜狀況,部分有大key的Redis節點佔用內存多,QPS高。json
二、大key相關的刪除或者自動過時時,會出現qps突降或者突升的狀況,極端狀況下,會形成主從複製異常,Redis服務阻塞沒法響應請求。大key的體積與刪除耗時可參考下表:網絡
key類型 field數量 耗時 Hash ~100萬 ~1000ms List ~100萬 ~1000ms Set ~100萬 ~1000ms Sorted Set ~100萬 ~1000ms數據結構
Redis 4.0以前的大key的發現與刪除方法 一、redis-rdb-tools工具。redis實例上執行bgsave,而後對dump出來的rdb文件進行分析,找到其中的大KEY。 二、redis-cli --bigkeys命令。能夠找到某個實例5種數據類型(String、hash、list、set、zset)的最大key。 三、自定義的掃描腳本,以Python腳本居多,方法與redis-cli --bigkeys相似。 四、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。
參考文檔: 一、http://antirez.com/news/93 二、http://antirez.com/news/126