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