Redis 集群中如何處理非本節(jié)點(diǎn)的 slot
我們都知道redis集群有16384個(gè)槽,它會(huì)因?yàn)槲覀兗簜€(gè)數(shù)配置的不同而分配不同的slot給各個(gè)節(jié)點(diǎn),而這篇文章就來(lái)聊聊當(dāng)某個(gè)節(jié)點(diǎn)處理到非它所負(fù)責(zé)的slot時(shí)是如何處理的,這一點(diǎn)很好的體現(xiàn)了redis對(duì)于raft協(xié)議良好的設(shè)計(jì)與實(shí)現(xiàn)。
一、詳解redis集群指令處理
1. 整體流程
假設(shè)我們現(xiàn)在集群中有個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)各自負(fù)責(zé)一部分槽,此時(shí)我們的客戶端向節(jié)點(diǎn)2發(fā)起一個(gè)set指令,而該指令對(duì)應(yīng)的key應(yīng)該是要存放到節(jié)點(diǎn)1中,對(duì)此節(jié)點(diǎn)2的做法是查看自己所維護(hù)的節(jié)點(diǎn)列表是否有負(fù)責(zé)該slot的節(jié)點(diǎn),如果發(fā)現(xiàn)了而回復(fù)給客戶端move指令,告知客戶端到指令的ip端口的節(jié)點(diǎn)進(jìn)行鍵值對(duì)存儲(chǔ):
了解完整體流程之后,我們通過(guò)源碼的方式來(lái)印證這些實(shí)現(xiàn)上的細(xì)節(jié),我們都知道redis客戶端發(fā)送的指令都會(huì)被redis的processCommand處理,該函數(shù)如果發(fā)現(xiàn)當(dāng)前是以集群的方式啟動(dòng)并且符合以下兩個(gè)條件則以集群的邏輯解析這條指令:
- 發(fā)送指令的不是master服務(wù)器。
- 參數(shù)中帶有key。
那么redis就會(huì)調(diào)用getNodeByQuery查詢重定向的節(jié)點(diǎn),如果發(fā)現(xiàn)查詢到的節(jié)點(diǎn)不是自己或者為空則調(diào)用clusterRedirectClient進(jìn)行重定向處理:
int processCommand(redisClient *c) {
//......
//如果開(kāi)啟了集群,且發(fā)送者不是master且參數(shù)帶key則步入邏輯
if (server.cluster_enabled &&
!(c->flags & REDIS_MASTER) &&
!(c->flags & REDIS_LUA_CLIENT &&
server.lua_caller->flags & REDIS_MASTER) &&
!(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0))
{
int hashslot;
if (server.cluster->state != REDIS_CLUSTER_OK) {
//......
} else {
int error_code;
//查找可以處理的節(jié)點(diǎn)
clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc,&hashslot,&error_code);
//如果為空且或者非自己,則調(diào)用clusterRedirectClient進(jìn)行重定向
if (n == NULL || n != server.cluster->myself) {
flagTransaction(c);
clusterRedirectClient(c,n,hashslot,error_code);
return REDIS_OK;
}
}
}
//......
//處理當(dāng)前請(qǐng)求指令并返回
}
2. 詳解節(jié)點(diǎn)定位步驟getNodeByQuery
步入getNodeByQuery即可看到查詢的核心流程,無(wú)論是單條還是多條客戶端指令,他都會(huì)封裝成multiState結(jié)構(gòu)體交由后續(xù)邏輯處理,而后續(xù)邏輯就會(huì)遍歷這些指令并計(jì)算出對(duì)應(yīng)的slot,然后執(zhí)行如下邏輯:
- 如果發(fā)現(xiàn)定位到的節(jié)點(diǎn)是自己,且當(dāng)前節(jié)點(diǎn)正在做遷移,則做個(gè)遷移標(biāo)記,然后檢查當(dāng)前節(jié)點(diǎn)是否有這個(gè)槽,如果沒(méi)有則發(fā)送ASK指令告知客戶端重定向到另一個(gè)遷移的目標(biāo)槽試試看。
- 如果對(duì)應(yīng)的key沒(méi)有找到對(duì)應(yīng)的槽,則直接返回當(dāng)前節(jié)點(diǎn)。
- 找到目標(biāo)槽,直接返回MOVE指令和目標(biāo)槽的信息。
對(duì)應(yīng)我們給出getNodeByQuery的核心代碼段:
clusterNode *getNodeByQuery(redisClient *c, struct redisCommand *cmd, robj **argv, int argc, int *hashslot, int *error_code) {
//......
//如果是exec命令則用客戶端的multiState封裝這些命令
if (cmd->proc == execCommand) {
/* If REDIS_MULTI flag is not set EXEC is just going to return an
* error. */
if (!(c->flags & REDIS_MULTI)) return myself;
ms = &c->mstate;
} else {
//如果不是exec則自己創(chuàng)建一個(gè)multiState封裝這單條指令保證后續(xù)邏輯一致
ms = &_ms;
_ms.commands = &mc;
//命令個(gè)數(shù)1
_ms.count = 1;
//命令參數(shù)
mc.argv = argv;
//命令參數(shù)個(gè)數(shù)
mc.argc = argc;
//對(duì)應(yīng)的命令
mc.cmd = cmd;
}
//遍歷multiState中的命令
for (i = 0; i < ms->count; i++) {
struct redisCommand *mcmd;
robj **margv;
int margc, *keyindex, numkeys, j;
//解析出命令、參數(shù)個(gè)數(shù)、參數(shù)
mcmd = ms->commands[i].cmd;
margc = ms->commands[i].argc;
margv = ms->commands[i].argv;
//解析出key以及個(gè)數(shù)
keyindex = getKeysFromCommand(mcmd,margv,margc,&numkeys);
for (j = 0; j < numkeys; j++) {
//拿到key
robj *thiskey = margv[keyindex[j]];
//計(jì)算slot
int thisslot = keyHashSlot((char*)thiskey->ptr,
sdslen(thiskey->ptr));
if (firstkey == NULL) {
firstkey = thiskey;
slot = thisslot;
//拿著計(jì)算的slot定位到對(duì)應(yīng)的節(jié)點(diǎn)
n = server.cluster->slots[slot];
//如果定位到的節(jié)點(diǎn)就是當(dāng)前節(jié)點(diǎn)正在做遷出或者遷入,則migrating_slot/importing_slot設(shè)置為1
if (n == myself &&
server.cluster->migrating_slots_to[slot] != NULL)
{
migrating_slot = 1;
} else if (server.cluster->importing_slots_from[slot] != NULL) {
importing_slot = 1;
}
} else {
//......
}
//如果正在做遷出或者嵌入,且當(dāng)前找不到當(dāng)前db找不到key的位置,則missing_keys++意為某個(gè)key可能正在被遷移中所以沒(méi)有命中
if ((migrating_slot || importing_slot) &&
lookupKeyRead(&server.db[0],thiskey) == NULL)
{
missing_keys++;
}
}
getKeysFreeResult(keyindex);
}
//所有key都沒(méi)有對(duì)應(yīng)節(jié)點(diǎn),直接返回當(dāng)前節(jié)點(diǎn)
if (n == NULL) return myself;
//......
//正在遷出且這個(gè)key在當(dāng)前節(jié)點(diǎn)沒(méi)有被命中,則將error_code設(shè)置為ask,并返回遷出的節(jié)點(diǎn)信息,告知客戶端到返回節(jié)點(diǎn)嘗試指令
if (migrating_slot && missing_keys) {
if (error_code) *error_code = REDIS_CLUSTER_REDIR_ASK;
return server.cluster->migrating_slots_to[slot];
}
//......
//返回其他節(jié)點(diǎn),error_code設(shè)置為move
if (n != myself && error_code) *error_code = REDIS_CLUSTER_REDIR_MOVED;
return n;
}
3. 結(jié)果告知客戶端
上述流程發(fā)現(xiàn)處理的節(jié)點(diǎn)不是自己之后,調(diào)用clusterRedirectClient進(jìn)行重定向,如果是REDIS_CLUSTER_REDIR_MOVED則告知客戶端這些slot后續(xù)直接找重定向節(jié)點(diǎn)處理就好了,后續(xù)無(wú)需找自己。若是REDIS_CLUSTER_REDIR_ASK則說(shuō)明當(dāng)前節(jié)點(diǎn)正處于數(shù)據(jù)遷移到目標(biāo)節(jié)點(diǎn),你可以到遷移的節(jié)點(diǎn)進(jìn)行請(qǐng)求,后續(xù)再次發(fā)起請(qǐng)求是還是找當(dāng)前節(jié)點(diǎn)看看能否出去,如果不能在進(jìn)行重定向:
void clusterRedirectClient(redisClient *c, clusterNode *n, int hashslot, int error_code) {
//......
if(......){
//......
} else if (error_code == REDIS_CLUSTER_REDIR_MOVED ||
error_code == REDIS_CLUSTER_REDIR_ASK)
{
//返回move命令告知要移動(dòng)到的節(jié)點(diǎn)后續(xù)直接到move的,如果是ask則返回正在遷往的節(jié)點(diǎn)地址,是臨時(shí)措施,下次客戶端還會(huì)找當(dāng)前節(jié)點(diǎn)
addReplySds(c,sdscatprintf(sdsempty(),
"-%s %d %s:%d\r\n",
(error_code == REDIS_CLUSTER_REDIR_ASK) ? "ASK" : "MOVED",
hashslot,n->ip,n->port));
} else {
redisPanic("getNodeByQuery() unknown error.");
}
}
二、小結(jié)
這篇文章比較精簡(jiǎn),我們通過(guò)源碼的方式簡(jiǎn)單的剖析了去中心化的redis如何在不同節(jié)點(diǎn)處理不同槽的請(qǐng)求,大體過(guò)程比較簡(jiǎn)單:
- 接收并處理客戶端傳入的key指令操作。
- 通過(guò)getNodeByQuery獲取key對(duì)應(yīng)的slot所屬節(jié)點(diǎn)。
- 如果是當(dāng)前節(jié)點(diǎn)的slot直接處理。
- 如果不是則查看是否正在遷出,如果是則返回ask讓客戶端到別的節(jié)點(diǎn)試試看,反之進(jìn)入步驟5。
- 如果定位的slot對(duì)應(yīng)的節(jié)點(diǎn)是別的節(jié)點(diǎn)則直接用move指令重定向客戶端,讓客戶端到另一個(gè)節(jié)點(diǎn)詢問(wèn)結(jié)果。