Redis大集群擴(kuò)容性能優(yōu)化實(shí)踐
作者:vivo互聯(lián)網(wǎng)數(shù)據(jù)庫(kù)團(tuán)隊(duì)—Yuan Jianwei
一、背景
在現(xiàn)網(wǎng)環(huán)境,一些使用Redis集群的業(yè)務(wù)隨著業(yè)務(wù)量的上漲,往往需要進(jìn)行節(jié)點(diǎn)擴(kuò)容操作。
之前有了解到運(yùn)維同學(xué)對(duì)一些節(jié)點(diǎn)數(shù)比較大的Redis集群進(jìn)行擴(kuò)容操作后,業(yè)務(wù)側(cè)反映集群性能下降,具體表現(xiàn)在訪問(wèn)時(shí)延增長(zhǎng)明顯。
某些業(yè)務(wù)對(duì)Redis集群訪問(wèn)時(shí)延比較敏感,例如現(xiàn)網(wǎng)環(huán)境對(duì)模型實(shí)時(shí)讀取,或者一些業(yè)務(wù)依賴讀取Redis集群的同步流程,會(huì)影響業(yè)務(wù)的實(shí)時(shí)流程時(shí)延。業(yè)務(wù)側(cè)可能無(wú)法接受。
為了找到這個(gè)問(wèn)題的根因,我們對(duì)某一次的Redis集群遷移操作后的集群性能下降問(wèn)題進(jìn)行排查。
1.1 問(wèn)題描述
這一次具體的Redis集群?jiǎn)栴}的場(chǎng)景是:某一個(gè)Redis集群進(jìn)行過(guò)擴(kuò)容操作。業(yè)務(wù)側(cè)使用Hiredis-vip進(jìn)行Redis集群訪問(wèn),進(jìn)行MGET操作。
業(yè)務(wù)側(cè)感知到訪問(wèn)Redis集群的時(shí)延變高。
1.2 現(xiàn)網(wǎng)環(huán)境說(shuō)明
目前現(xiàn)網(wǎng)環(huán)境部署的Redis版本多數(shù)是3.x或者4.x版本;
業(yè)務(wù)訪問(wèn)Redis集群的客戶端品類繁多,較多的使用Jedis。本次問(wèn)題排查的業(yè)務(wù)使用客戶端Hiredis-vip進(jìn)行訪問(wèn);
Redis集群的節(jié)點(diǎn)數(shù)比較大,規(guī)模是100+;
集群之前存在擴(kuò)容操作。
1.3 觀察現(xiàn)象
因?yàn)闀r(shí)延變高,我們從幾個(gè)方面進(jìn)行排查:
帶寬是否打滿;
CPU是否占用過(guò)高;
OPS是否很高;
通過(guò)簡(jiǎn)單的監(jiān)控排查,帶寬負(fù)載不高。但是發(fā)現(xiàn)CPU表現(xiàn)異常:

1.3.1 對(duì)比ops和CPU負(fù)載
觀察業(yè)務(wù)反饋使用的MGET和CPU負(fù)載,我們找到了對(duì)應(yīng)的監(jiān)控曲線。
從時(shí)間上分析,MGET和CPU負(fù)載高并沒(méi)有直接關(guān)聯(lián)。業(yè)務(wù)側(cè)反饋的是MGET的時(shí)延普遍增高。此處看到MGET的OPS和CPU負(fù)載是錯(cuò)峰的。
此處可以暫時(shí)確定業(yè)務(wù)請(qǐng)求和CPU負(fù)載暫時(shí)沒(méi)有直接關(guān)系,但是從曲線上可以看出:在同一個(gè)時(shí)間軸上,業(yè)務(wù)請(qǐng)求和cpu負(fù)載存在錯(cuò)峰的情況,兩者間應(yīng)該有間接關(guān)系。
1.3.2 對(duì)比Cluster指令OPS和CPU負(fù)載
由于之前有運(yùn)維側(cè)同事有反饋集群進(jìn)行過(guò)擴(kuò)容操作,必然存在slot的遷移。
考慮到業(yè)務(wù)的客戶端一般都會(huì)使用緩存存放Redis集群的slot拓?fù)湫畔?,因此懷疑Cluster指令會(huì)和CPU負(fù)載存在一定聯(lián)系。
我們找到了當(dāng)中確實(shí)有一些聯(lián)系:

此處可以明顯看到:某個(gè)實(shí)例在執(zhí)行Cluster指令的時(shí)候,CPU的使用會(huì)明顯上漲。
根據(jù)上述現(xiàn)象,大致可以進(jìn)行一個(gè)簡(jiǎn)單的聚焦:
業(yè)務(wù)側(cè)執(zhí)行MGET,因?yàn)橐恍┰驁?zhí)行了Cluster指令;
Cluster指令因?yàn)橐恍┰驅(qū)е翪PU占用較高影響其他操作;
懷疑Cluster指令是性能瓶頸。
同時(shí),引申幾個(gè)需要關(guān)注的問(wèn)題:
為什么會(huì)有較多的Cluster指令被執(zhí)行?
為什么Cluster指令執(zhí)行的時(shí)候CPU資源比較高?
為什么節(jié)點(diǎn)規(guī)模大的集群遷移slot操作容易“中招”?
二、問(wèn)題排查
2.1 Redis熱點(diǎn)排查
我們對(duì)一臺(tái)現(xiàn)場(chǎng)出現(xiàn)了CPU負(fù)載高的Redis實(shí)例使用perf top進(jìn)行簡(jiǎn)單的分析:

從上圖可以看出來(lái),函數(shù)(ClusterReplyMultiBulkSlots)占用的CPU資源高達(dá) 51.84%,存在異常。
2.1.1 ClusterReplyMultiBulkSlots實(shí)現(xiàn)原理
我們對(duì)clusterReplyMultiBulkSlots函數(shù)進(jìn)行分析:
void clusterReplyMultiBulkSlots(client *c) {/* Format: 1) 1) start slot* 2) end slot* 3) 1) master IP* 2) master port* 3) node ID* 4) 1) replica IP* 2) replica port* 3) node ID* ... continued until done*/int num_masters = 0;void *slot_replylen = addDeferredMultiBulkLength(c);dictEntry *de;dictIterator *di = dictGetSafeIterator(server.cluster->nodes);while((de = dictNext(di)) != NULL) {/*注意:此處是對(duì)當(dāng)前Redis節(jié)點(diǎn)記錄的集群所有主節(jié)點(diǎn)都進(jìn)行了遍歷*/clusterNode *node = dictGetVal(de);int j = 0, start = -1;/* Skip slaves (that are iterated when producing the output of their* master) and masters not serving any slot. *//*跳過(guò)備節(jié)點(diǎn)。備節(jié)點(diǎn)的信息會(huì)從主節(jié)點(diǎn)側(cè)獲取。*/if (!nodeIsMaster(node) || node->numslots == 0) continue;for (j = 0; j < CLUSTER_SLOTS; j++) {/*注意:此處是對(duì)當(dāng)前節(jié)點(diǎn)中記錄的所有slot進(jìn)行了遍歷*/int bit, i;/*確認(rèn)當(dāng)前節(jié)點(diǎn)是不是占有循環(huán)終端的slot*/if ((bit = clusterNodeGetSlotBit(node,j)) != 0) {if (start == -1) start = j;}/*簡(jiǎn)單分析,此處的邏輯大概就是找出連續(xù)的區(qū)間,是的話放到返回中;不是的話繼續(xù)往下遞歸slot。如果是開(kāi)始的話,開(kāi)始一個(gè)連續(xù)區(qū)間,直到和當(dāng)前的不連續(xù)。*/if (start != -1 && (!bit || j == CLUSTER_SLOTS-1)) {int nested_elements = 3; /* slots (2) + master addr (1). */void *nested_replylen = addDeferredMultiBulkLength(c);if (bit && j == CLUSTER_SLOTS-1) j++;/* If slot exists in output map, add to it's list.* else, create a new output map for this slot */if (start == j-1) {addReplyLongLong(c, start); /* only one slot; low==high */addReplyLongLong(c, start);} else {addReplyLongLong(c, start); /* low */addReplyLongLong(c, j-1); /* high */}start = -1;/* First node reply position is always the master */addReplyMultiBulkLen(c, 3);addReplyBulkCString(c, node->ip);addReplyLongLong(c, node->port);addReplyBulkCBuffer(c, node->name, CLUSTER_NAMELEN);/* Remaining nodes in reply are replicas for slot range */for (i = 0; i < node->numslaves; i++) {/*注意:此處遍歷了節(jié)點(diǎn)下面的備節(jié)點(diǎn)信息,用于返回*//* This loop is copy/pasted from clusterGenNodeDescription()* with modifications for per-slot node aggregation */if (nodeFailed(node->slaves[i])) continue;addReplyMultiBulkLen(c, 3);addReplyBulkCString(c, node->slaves[i]->ip);addReplyLongLong(c, node->slaves[i]->port);addReplyBulkCBuffer(c, node->slaves[i]->name, CLUSTER_NAMELEN);nested_elements++;}setDeferredMultiBulkLength(c, nested_replylen, nested_elements);num_masters++;}}}dictReleaseIterator(di);setDeferredMultiBulkLength(c, slot_replylen, num_masters);}/* Return the slot bit from the cluster node structure. *//*該函數(shù)用于判斷指定的slot是否屬于當(dāng)前clusterNodes節(jié)點(diǎn)*/int clusterNodeGetSlotBit(clusterNode *n, int slot) {return bitmapTestBit(n->slots,slot);}/* Test bit 'pos' in a generic bitmap. Return 1 if the bit is set,* otherwise 0. *//*此處流程用于判斷指定的的位置在bitmap上是否為1*/int bitmapTestBit(unsigned char *bitmap, int pos) {off_t byte = pos/8;int bit = pos&7;return (bitmap[byte] & (1<0 ;}typedef struct clusterNode {.../*使用一個(gè)長(zhǎng)度為CLUSTER_SLOTS/8的char數(shù)組對(duì)當(dāng)前分配的slot進(jìn)行記錄*/unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */...} clusterNode;
每一個(gè)節(jié)點(diǎn)(ClusterNode)使用位圖(char slots[CLUSTER_SLOTS/8])存放slot的分配信息。
簡(jiǎn)要說(shuō)一下BitmapTestBit的邏輯:clusterNode->slots是一個(gè)長(zhǎng)度為CLUSTER_SLOTS/8的數(shù)組。CLUSTER_SLOTS是固定值16384。數(shù)組上的每一個(gè)位分別代表一個(gè)slot。此處的bitmap數(shù)組下標(biāo)則是0到2047,slot的范圍是0到16383。
因?yàn)橐袛鄍os這個(gè)位置的bit上是否是1,因此:
off_t byte = pos/8:拿到在bitmap上對(duì)應(yīng)的哪一個(gè)字節(jié)(Byte)上存放這個(gè)pos位置的信息。因?yàn)橐粋€(gè)Byte有8個(gè)bit。使用pos/8可以指導(dǎo)需要找的Byte在哪一個(gè)。此處把bitmap當(dāng)成數(shù)組處理,這里對(duì)應(yīng)的便是對(duì)應(yīng)下標(biāo)的Byte。
int bit = pos&7:拿到是在這個(gè)字節(jié)上對(duì)應(yīng)哪一個(gè)bit表示這個(gè)pos位置的信息。&7其實(shí)就是%8??梢韵胂髮?duì)pos每8個(gè)一組進(jìn)行分組,最后一組(不滿足8)的個(gè)數(shù)對(duì)應(yīng)的便是在bitmap對(duì)應(yīng)的Byte上對(duì)應(yīng)的bit數(shù)組下標(biāo)位置。
(bitmap[byte] & (1<
:判斷對(duì)應(yīng)的那個(gè)bit在bitmap[byte]上是否存在。
以slot為10001進(jìn)行舉例:

因此10001這個(gè)slot對(duì)應(yīng)的是下標(biāo)1250的Byte,要校驗(yàn)的是下標(biāo)1的bit。
對(duì)應(yīng)在ClusterNode->slots上的對(duì)應(yīng)位置:

圖示綠色的方塊表示bitmap[1250],也就是對(duì)應(yīng)存放slot 10001的Byte;紅框標(biāo)識(shí)(bit[1])對(duì)應(yīng)的就是1< 總結(jié)ClusterNodeGetSlotBit的概要邏輯是:判斷當(dāng)前的這個(gè)slot是否分配在當(dāng)前node上。因此ClusterReplyMultiBulkSlots大概邏輯表示如下:

大概步驟如下:
對(duì)每一個(gè)節(jié)點(diǎn)進(jìn)行遍歷;
對(duì)于每一個(gè)節(jié)點(diǎn),遍歷所有的slots,使用ClusterNodeGetSlotBit判斷遍歷中的slot是否分配于當(dāng)前節(jié)點(diǎn);
從獲取CLUSTER SLOTS指令的結(jié)果來(lái)看,可以看到,復(fù)雜度是<集群主節(jié)點(diǎn)個(gè)數(shù)> *
2.1.2 Redis熱點(diǎn)排查總結(jié)
就目前來(lái)看,CLUSTER SLOTS指令時(shí)延隨著Redis集群的主節(jié)點(diǎn)個(gè)數(shù),線性增長(zhǎng)。而這次我們排查的集群主節(jié)點(diǎn)數(shù)比較大,可以解釋這次排查的現(xiàn)網(wǎng)現(xiàn)象中CLUSTER SLOTS指令時(shí)延為何較大。
2.2 客戶端排查
了解到運(yùn)維同學(xué)們存在擴(kuò)容操作,擴(kuò)容完成后必然涉及到一些key在訪問(wèn)的時(shí)候存在MOVED的錯(cuò)誤。
當(dāng)前使用的Hiredis-vip客戶端代碼進(jìn)行簡(jiǎn)單的瀏覽,簡(jiǎn)要分析以下當(dāng)前業(yè)務(wù)使用的Hiredis-vip客戶端在遇到MOVED的時(shí)候會(huì)怎樣處理。由于其他的大部分業(yè)務(wù)常用的Jedis客戶端,此處也對(duì)Jedis客戶端對(duì)應(yīng)流程進(jìn)行簡(jiǎn)單分析。
2.2.1 Hiredis-vip對(duì)MOVED處理實(shí)現(xiàn)原理
Hiredis-vip針對(duì)MOVED的操作:
查看Cluster_update_route的調(diào)用過(guò)程:
此處的cluster_update_route_by_addr進(jìn)行了CLUSTER SLOT操作??梢钥吹剑?dāng)獲取到MOVED報(bào)錯(cuò)的時(shí)候,Hiredis-vip會(huì)重新更新Redis集群拓?fù)浣Y(jié)構(gòu),有下面的特性:
因?yàn)楣?jié)點(diǎn)通過(guò)ip:port作為key,哈希方式一樣,如果集群拓?fù)漕愃?,多個(gè)客戶端很容易同時(shí)到同一個(gè)節(jié)點(diǎn)進(jìn)行訪問(wèn);
如果某個(gè)節(jié)點(diǎn)訪問(wèn)失敗,會(huì)通過(guò)迭代器找下一個(gè)節(jié)點(diǎn),由于上述的原因,多個(gè)客戶端很容易同時(shí)到下一個(gè)節(jié)點(diǎn)進(jìn)行訪問(wèn)。
2.2.2 Jedis對(duì)MOVED處理實(shí)現(xiàn)原理
對(duì)Jedis客戶端代碼進(jìn)行簡(jiǎn)單瀏覽,發(fā)現(xiàn)如果存在MOVED錯(cuò)誤,會(huì)調(diào)用renewSlotCache。
繼續(xù)看renewSlotCache的調(diào)用,此處可以確認(rèn):Jedis在集群模式下在遇到MOVED的報(bào)錯(cuò)時(shí)候,會(huì)發(fā)送Redis命令CLUSTER SLOTS,重新拉取Redis集群的slot拓?fù)浣Y(jié)構(gòu)。
2.2.3 客戶端實(shí)現(xiàn)原理小結(jié)
由于Jedis是Java的Redis客戶端,Hiredis-vip是c++的Redis客戶端,可以簡(jiǎn)單認(rèn)為這種異常處理機(jī)制是共性操作。
對(duì)客戶端集群模式下對(duì)MOVED的流程梳理大概如下:

總的來(lái)說(shuō):
1)使用客戶端緩存的slot拓?fù)溥M(jìn)行對(duì)key的訪問(wèn);
2)Redis節(jié)點(diǎn)返回正常:
訪問(wèn)正常,繼續(xù)后續(xù)操作
3)Redis節(jié)點(diǎn)返回MOVED:
對(duì)Redis節(jié)點(diǎn)進(jìn)行CLUSTER SLOTS指令執(zhí)行,更新拓?fù)洌?/p>
使用新的拓?fù)鋵?duì)key重新訪問(wèn)。
2.2.3 客戶端排查小結(jié)
Redis集群正在擴(kuò)容,也就是必然存在一些Redis客戶端在訪問(wèn)Redis集群遇到MOVED,執(zhí)行Redis指令CLUSTER SLOTS進(jìn)行拓?fù)浣Y(jié)構(gòu)更新。
如果遷移的key命中率高,CLUSTER SLOTS指令會(huì)更加頻繁的執(zhí)行。這樣導(dǎo)致的結(jié)果是遷移過(guò)程中Redis集群會(huì)持續(xù)被客戶端執(zhí)行CLUSTER SLOTS指令。
2.3 排查小結(jié)
此處,結(jié)合Redis側(cè)的CLUSTER SLOTS機(jī)制以及客戶端對(duì)MOVED的處理邏輯,可以解答之前的幾個(gè)個(gè)問(wèn)題:
為什么會(huì)有較多的Cluster指令被執(zhí)行?
因?yàn)榘l(fā)生過(guò)遷移操作,業(yè)務(wù)訪問(wèn)一些遷移過(guò)的key會(huì)拿到MOVED返回,客戶端會(huì)對(duì)該返回重新拉取slot拓?fù)湫畔?,?zhí)行CLUSTER SLOTS。
為什么Cluster指令執(zhí)行的時(shí)候CPU資源比較高?
分析Redis源碼,發(fā)現(xiàn)CLUSTER SLOT指令的時(shí)間復(fù)雜度和主節(jié)點(diǎn)個(gè)數(shù)成正比。業(yè)務(wù)當(dāng)前的Redis集群主節(jié)點(diǎn)個(gè)數(shù)比較多,自然耗時(shí)高,占用CPU資源高。
為什么節(jié)點(diǎn)規(guī)模大的集群遷移slot操作容易“中招”?
遷移操作必然帶來(lái)一些客戶端訪問(wèn)key的時(shí)候返回MOVED;
客戶端對(duì)于MOVED的返回會(huì)執(zhí)行CLUSTER SLOTS指令;
CLUSTER SLOTS指令隨著集群主節(jié)點(diǎn)個(gè)數(shù)的增加,時(shí)延會(huì)上升;
業(yè)務(wù)的訪問(wèn)在slot的遷移期間會(huì)因?yàn)镃LUSTER SLOTS的時(shí)延上升,在外部的感知是執(zhí)行指令的時(shí)延升高。
此段與本文無(wú)關(guān)(webflux 是兼容Spring MVC 基于@Controller,@RequestMapping等注解的編程開(kāi)發(fā)方式的,可以做到平滑切換)
三、優(yōu)化
3.1 現(xiàn)狀分析
根據(jù)目前的情況來(lái)看,客戶端遇到MOVED進(jìn)行CLUSTER SLOTS執(zhí)行是正常的流程,因?yàn)樾枰录旱膕lot拓?fù)浣Y(jié)構(gòu)提高后續(xù)的集群訪問(wèn)效率。
此處流程除了Jedis,Hiredis-vip,其他的客戶端應(yīng)該也會(huì)進(jìn)行類似的slot信息緩存優(yōu)化。此處流程優(yōu)化空間不大,是Redis的集群訪問(wèn)機(jī)制決定。
因此對(duì)Redis的集群信息記錄進(jìn)行分析。
3.1.1 Redis集群元數(shù)據(jù)分析
集群中每一個(gè)Redis節(jié)點(diǎn)都會(huì)有一些集群的元數(shù)據(jù)記錄,記錄于server.cluster,內(nèi)容如下:
typedef struct clusterState {...dict *nodes; /* Hash table of name -> clusterNode structures *//*nodes記錄的是所有的節(jié)點(diǎn),使用dict記錄*/...clusterNode *slots[CLUSTER_SLOTS];/*slots記錄的是slot數(shù)組,內(nèi)容是node的指針*/...} clusterState;
如2.1所述,原有邏輯通過(guò)遍歷每個(gè)節(jié)點(diǎn)的slot信息獲得拓?fù)浣Y(jié)構(gòu)。
3.1.2 Redis集群元數(shù)據(jù)分析
觀察CLUSTER SLOTS的返回結(jié)果:
/* Format: 1) 1) start slot* 2) end slot* 3) 1) master IP* 2) master port* 3) node ID* 4) 1) replica IP* 2) replica port* 3) node ID* ... continued until done*/
結(jié)合server.cluster中存放的集群信息,筆者認(rèn)為此處可以使用server.cluster->slots進(jìn)行遍歷。因?yàn)閟erver.cluster->slots已經(jīng)在每一次集群的拓?fù)渥兓玫搅烁?,保存的是?jié)點(diǎn)指針。
3.2 優(yōu)化方案
簡(jiǎn)單的優(yōu)化思路如下:
對(duì)slot進(jìn)行遍歷,找出slot中節(jié)點(diǎn)是連續(xù)的塊;
當(dāng)前遍歷的slot的節(jié)點(diǎn)如果和之前遍歷的節(jié)點(diǎn)一致,說(shuō)明目前訪問(wèn)的slot和前面的是在同一個(gè)節(jié)點(diǎn)下,也就是是在某個(gè)節(jié)點(diǎn)下的“連續(xù)”的slot區(qū)域內(nèi);
當(dāng)前遍歷的slot的節(jié)點(diǎn)如果和之前遍歷的節(jié)點(diǎn)不一致,說(shuō)明目前訪問(wèn)的slot和前面的不同,前面的“連續(xù)”slot區(qū)域可以進(jìn)行輸出;而當(dāng)前slot作為下一個(gè)新的“連續(xù)”slot區(qū)域的開(kāi)始。
因此只要對(duì)server.cluster->slots進(jìn)行遍歷,可以滿足需求。簡(jiǎn)單表示大概如下:

這樣的時(shí)間復(fù)雜度降低到
3.3 實(shí)現(xiàn)
優(yōu)化邏輯如下:
void clusterReplyMultiBulkSlots(client * c) {/* Format: 1) 1) start slot* 2) end slot* 3) 1) master IP* 2) master port* 3) node ID* 4) 1) replica IP* 2) replica port* 3) node ID* ... continued until done*/clusterNode *n = NULL;int num_masters = 0, start = -1;void *slot_replylen = addReplyDeferredLen(c);for (int i = 0; i <= CLUSTER_SLOTS; i++) {/*對(duì)所有slot進(jìn)行遍歷*//* Find start node and slot id. */if (n == NULL) {if (i == CLUSTER_SLOTS) break;n = server.cluster->slots[i];start = i;continue;}/* Add cluster slots info when occur different node with start* or end of slot. */if (i == CLUSTER_SLOTS || n != server.cluster->slots[i]) {/*遍歷主節(jié)點(diǎn)下面的備節(jié)點(diǎn),添加返回客戶端的信息*/addNodeReplyForClusterSlot(c, n, start, i-1);num_masters++;if (i == CLUSTER_SLOTS) break;n = server.cluster->slots[i];start = i;}}setDeferredArrayLen(c, slot_replylen, num_masters);}
通過(guò)對(duì)server.cluster->slots進(jìn)行遍歷,找到某個(gè)節(jié)點(diǎn)下的“連續(xù)”的slot區(qū)域,一旦后續(xù)不連續(xù),把之前的“連續(xù)”slot區(qū)域的節(jié)點(diǎn)信息以及其備節(jié)點(diǎn)信息進(jìn)行輸出,然后繼續(xù)下一個(gè)“連續(xù)”slot區(qū)域的查找于輸出。
四、優(yōu)化結(jié)果對(duì)比
對(duì)兩個(gè)版本的Redis的CLUSTER SLOTS指令進(jìn)行橫向?qū)Ρ取?/p>
4.1 測(cè)試環(huán)境&壓測(cè)場(chǎng)景
操作系統(tǒng):manjaro 20.2
硬件配置:
CPU:AMD Ryzen 7 4800H
DRAM:DDR4 3200MHz 8G*2
Redis集群信息:
1)持久化配置
關(guān)閉aof
關(guān)閉bgsave
2)集群節(jié)點(diǎn)信息:
節(jié)點(diǎn)個(gè)數(shù):100
所有節(jié)點(diǎn)都是主節(jié)點(diǎn)
壓測(cè)場(chǎng)景:
使用benchmark工具對(duì)集群?jiǎn)蝹€(gè)節(jié)點(diǎn)持續(xù)發(fā)送CLUSTER SLOTS指令;
對(duì)其中一個(gè)版本壓測(cè)完后,回收集群,重新部署后再進(jìn)行下一輪壓測(cè)。
4.2 CPU資源占用對(duì)比
perf導(dǎo)出火焰圖。原有版本:

優(yōu)化后:

可以明顯看到,優(yōu)化后的占比大幅度下降?;痉项A(yù)期。
4.3 耗時(shí)對(duì)比
在上進(jìn)行測(cè)試,嵌入耗時(shí)測(cè)試代碼:
else if (!strcasecmp(c->argv[1]->ptr,"slots") && c->argc == 2) {/* CLUSTER SLOTS */long long now = ustime();clusterReplyMultiBulkSlots(c);serverLog(LL_NOTICE,"cluster slots cost time:%lld us", ustime() - now);}
輸入日志進(jìn)行對(duì)比;
原版的日志輸出:
37351:M 06 Mar 2021 16:11:39.313 * cluster slots cost time:2061 us。
優(yōu)化后版本日志輸出:
35562:M 06 Mar 2021 16:11:27.862 * cluster slots cost time:168 us。
從耗時(shí)上看下降明顯:從2000+us 下降到200-us;在100個(gè)主節(jié)點(diǎn)的集群中的耗時(shí)縮減到原來(lái)的8.2%;優(yōu)化結(jié)果基本符合預(yù)期。
五、總結(jié)
這里可以簡(jiǎn)單描述下文章上述的動(dòng)作從而得出的這樣的一個(gè)結(jié)論:性能缺陷。
簡(jiǎn)單總結(jié)下上述的排查以及優(yōu)化過(guò)程:
Redis大集群因?yàn)镃LUSTER命令導(dǎo)致某些節(jié)點(diǎn)的訪問(wèn)延遲明顯;
使用perf top指令對(duì)Redis實(shí)例進(jìn)行排查,發(fā)現(xiàn)clusterReplyMultiBulkSlots命令占用CPU資源異常;
對(duì)clusterReplyMultiBulkSlots進(jìn)行分析,該函數(shù)存在明顯的性能問(wèn)題;
對(duì)clusterReplyMultiBulkSlots進(jìn)行優(yōu)化,性能提升明顯。
從上述的排查以及優(yōu)化過(guò)程可以得出一個(gè)結(jié)論:目前的Redis在CLUSTER SLOT指令存在性能缺陷。
因?yàn)镽edis的數(shù)據(jù)分片機(jī)制,決定了Redis集群模式下的key訪問(wèn)方法是緩存slot的拓?fù)湫畔?。?yōu)化點(diǎn)也只能在CLUSTER SLOTS入手。而Redis的集群節(jié)點(diǎn)個(gè)數(shù)一般沒(méi)有這么大,問(wèn)題暴露的不明顯。
其實(shí)Hiredis-vip的邏輯也存在一定問(wèn)題。如2.2.1所說(shuō),Hiredis-vip的slot拓?fù)涓路椒ㄊ潜闅v所有的節(jié)點(diǎn)挨個(gè)進(jìn)行CLUSTER SLOTS。如果Redis集群規(guī)模較大而且業(yè)務(wù)側(cè)的客戶端規(guī)模較多,會(huì)出現(xiàn)連鎖反應(yīng):
1)如果Redis集群較大,CLUSTER SLOTS響應(yīng)比較慢;
2)如果某個(gè)節(jié)點(diǎn)沒(méi)有響應(yīng)或者返回報(bào)錯(cuò),Hiredis-vip客戶端會(huì)對(duì)下一個(gè)節(jié)點(diǎn)繼續(xù)進(jìn)行請(qǐng)求;
3)Hiredis-vip客戶端中對(duì)Redis集群節(jié)點(diǎn)迭代遍歷的方法相同(因?yàn)榧旱男畔⒃诟鱾€(gè)客戶端基本一致),此時(shí)當(dāng)客戶端規(guī)模較大的時(shí)候,某個(gè)Redis節(jié)點(diǎn)可能存在阻塞,就會(huì)導(dǎo)致hiredis-vip客戶端遍歷下一個(gè)Redis節(jié)點(diǎn);
4)大量Hiredis-vip客戶端挨個(gè)地對(duì)一些Redis節(jié)點(diǎn)進(jìn)行訪問(wèn),如果Redis節(jié)點(diǎn)無(wú)法負(fù)擔(dān)這樣的請(qǐng)求,這樣會(huì)導(dǎo)致Redis節(jié)點(diǎn)在大量Hiredis-vip客戶端的“遍歷”下挨個(gè)請(qǐng)求:
結(jié)合上述第3點(diǎn),可以想象一下:有1w個(gè)客戶端對(duì)該Redis集群進(jìn)行訪問(wèn)。因?yàn)槟硞€(gè)命中率較高的key存在遷移操作,所有的客戶端都需要更新slot拓?fù)?。由于所有客戶端緩存的集群?jié)點(diǎn)信息相同,因此遍歷各個(gè)節(jié)點(diǎn)的順序是一致的。這1w個(gè)客戶端都使用同樣的順序?qū)焊鱾€(gè)節(jié)點(diǎn)進(jìn)行遍歷地操作CLUSTER SLOTS。由于CLUSTER SLOTS在大集群中性能較差,Redis節(jié)點(diǎn)很容易會(huì)被大量客戶端請(qǐng)求導(dǎo)致不可訪問(wèn)。Redis節(jié)點(diǎn)會(huì)根據(jù)遍歷順序依次被大部分的客戶端(例如9k+個(gè)客戶端)訪問(wèn),執(zhí)行CLUSTER SLOTS指令,導(dǎo)致Redis節(jié)點(diǎn)挨個(gè)被阻塞。
5)最終的表現(xiàn)是大部分Redis節(jié)點(diǎn)的CPU負(fù)載暴漲,很多Hiredis-vip客戶端則繼續(xù)無(wú)法更新slot拓?fù)洹?/p>
最終結(jié)果是大規(guī)模的Redis集群在進(jìn)行slot遷移操作后,在大規(guī)模的Hiredis-vip客戶端訪問(wèn)下業(yè)務(wù)側(cè)感知是普通指令時(shí)延變高,而Redis實(shí)例CPU資源占用高漲。這個(gè)邏輯可以進(jìn)行一定優(yōu)化。
目前上述分節(jié)3的優(yōu)化已經(jīng)提交并合并到Redis 6.2.2版本中。
六、參考資料
1、Hiredis-vip: https://github.com
2、Jedis: https://github.com/redis/jedis
3、Redis: https://github.com/redis/redis
4、Perf:https://perf.wiki.kernel.org
