redis cluster 是 redis 的分佈式實現。 如同官方文檔 cluster-spec 強調的那樣,其設計優先考慮高性能和線性擴展能力,並盡最大努力保證 write safety。html
這裏所說的 write 丟失是指,回覆 client ack 後,後續請求中出現數據未作變動或丟失的狀況,主要有主從切換、實例重啓、腦裂等三種狀況可能致使該問題,下面依次分析。node
failover 會帶來路由的變動,主動/被動狀況須要分開討論。git
爲表達方便,有如下假設,cluster 狀態正常, node C 爲 master,負責 slot 1-100,對應 slave 爲 C'。redis
master C 掛掉後,slave C' 在最多 2 倍 cluster_node_timeout 的時間內把 C 標記成 FAIL,進而觸發 failover 邏輯。網絡
在 slave C' 成功切換爲 master 前,1-100 slot 仍然由 C 負責,訪問會報錯。 C' 切爲 master 後,gossip 廣播路由變動,在這個過程當中,client 訪問 C',仍能夠獲得正常的迴應,而訪問其餘持有老路由的 node,請求會被 MOVED 到掛掉的 C,訪問報錯。併發
惟一可能出現 write 丟失的 case 由主從異步複製機制致使。
若是寫到 master 上的數據尚未來得及同步到 slave 就掛掉了,那麼這部分數據就會丟失(重啓後不存在 merge 操做)。master 回覆 client ack 與同步 slave 幾乎是同時進行的,這種狀況不多發生,但這是一個風險,時間窗口很小。app
主動 failover 經過 sysadmin 在 slave node 上執行 CLUSTER FAILOVER [FORCE|TAKEOVER]
命令觸發。異步
完整 manual failover 過程在以前的博客詳細討論過,歸納爲如下 6 個步驟,分佈式
三個選項分別有不一樣的行爲,分析以下,
(1)默認選項。
執行完整的mf 流程,master 有停服行爲,所以不存在 write 丟失的問題。 函數
(2)FORCE 選項。
從第 4 步開始執行。在 slave C' 統計選票階段,master C 仍然能夠正常接收用戶請求,且主從異步複製,這些均可能致使 write 丟失。mf 將在將來的某個時間點開始執行,timeout 時間爲 CLUSTER_MF_TIMEOUT(現版本爲 5s),每次 clusterCron
都會檢查。
(3)TAKEOVER 選項。
從第 5 步開始執行。slave 直接增長本身的 configEpoch(無需其餘 node 贊成),接管 slots。從 slave C' 切換爲 master ,到原 master 節點 C 更新路由,發到 C 的請求,均可能存在 write 丟失的可能,通常在一個 ping 的時間內完成,時間窗口很小。C 和 C' 之外節點更新路由滯後只會帶來多一次的 MOVED 錯誤,不會致使 write 丟失。
clusterState 結構體中有一個 state 成員變量,表示 cluster 的全局狀態,控制着當前 cluster 是否能夠提供服務,有如下兩種取值,
#define CLUSTER_OK 0 /* Everything looks ok */ #define CLUSTER_FAIL 1 /* The cluster can't work */
server 重啓後,state 被初始化爲 CLUSTER_FAIL,代碼邏輯能夠在 clusterInit
函數中找到。
對於 CLUSTER_FAIL 狀態的 cluster 是拒絕接受訪問的,代碼參考以下,
int processCommand(client *c) { ... if (server.cluster_enabled && !(c->flags & CLIENT_MASTER) && !(c->flags & CLIENT_LUA && server.lua_caller->flags & CLIENT_MASTER) && !(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0 && c->cmd->proc != execCommand)) { int hashslot; int error_code; clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc, &hashslot,&error_code); ... } ... }
重點在 getNodeByQuery
函數,在 cluster 模式開啓後,用來查找到真正要執行 command 的 node。
注意:redis cluster 採用去中心化的路由管理策略,每個 node 均可以直接訪問,若是要執行 command 的 node 不是當前鏈接的,它會返回一個 -MOVED 的重定向錯誤,指向真正要執行 command 的 node。
下面看 getNodeByQuery
函數的部分邏輯,
clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, int argc, int *hashslot, int *error_code) { ... if (server.cluster->state != CLUSTER_OK) { if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE; return NULL; } ... }
能夠看到,必須是 CLUSTER_OK 狀態的 cluster 才能正常訪問。
咱們說,這種限制對於保證 Write safety 是很是有必要的!
能夠想象,若是 master A 掛掉後,對應的 slave A' 經過選舉成功當選爲新 master。此時,A 重啓,且剛好有一些 client 看到的路由沒有更新,它們仍然會往 A 上寫數據,若是接受這些 write,就會丟數據!A' 纔是這個 sharding 你們公認的 master。因此,A' 重啓後須要先禁用服務,直到路由變動完成。
那麼,何時 cluster 纔會出現 CLUSTER_FAIL -> CLUSTER_OK 的狀態變動呢?答案要在 clusterCron
定時任務裏找。
void clusterCron(void) { ... if (update_state || server.cluster->state == CLUSTER_FAIL) clusterUpdateState(); }
關鍵邏輯在 clusterUpdateState
函數裏。
#define CLUSTER_WRITABLE_DELAY 2000 void clusterUpdateState(void) { static mstime_t first_call_time = 0; ... if (first_call_time == 0) first_call_time = mstime(); if (nodeIsMaster(myself) && server.cluster->state == CLUSTER_FAIL && mstime() - first_call_time < CLUSTER_WRITABLE_DELAY) return; new_state = CLUSTER_OK; ... if (new_state != server.cluster->state) { ... server.cluster->state = new_state; } }
在以上邏輯裏能夠看到,cluster 狀態變動要延遲 CLUSTER_WRITABLE_DELAY 毫秒,目前版本爲 2s。
訪問延遲就是爲了等待路由變動,那麼,何時觸發路由變動呢?
咱們知道,一個新 server 剛啓動,它與其餘 node 進行 gossip 通訊的 link 都是 null,在 clusterCron
裏檢查出來後會依次鏈接,併發送 ping。做爲一個路由過時的老節點,收到其餘節點發來的 update 消息,更改自身路由。
CLUSTER_WRITABLE_DELAY 毫秒後,A 節點恢復訪問,咱們認爲 CLUSTER_WRITABLE_DELAY 的時間窗口足夠更新路由。
因爲網絡的不可靠,網絡分區是一個必需要考慮的問題,也即 CAP 理論中的 P。
partition 發生後,cluster 被割裂成 majority 和 minority 兩部分,這裏以分區中 master 節點的數量來區分。
(1)對於 minority 部分,slave 會發起選舉,可是不能收到大多數 master 的選票,也就沒法完成正常的 failover 流程。同時在 clusterCron
裏大部分節點會被標記爲 CLUSTER_NODE_PFAIL 狀態,進而觸發 clusterUpdateState
的邏輯,大概以下,
void clusterCron(void) { ... di = dictGetSafeIterator(server.cluster->nodes); while((de = dictNext(di)) != NULL) { ... delay = now - node->ping_sent; if (delay > server.cluster_node_timeout) { if (!(node->flags & (CLUSTER_NODE_PFAIL|CLUSTER_NODE_FAIL))) { serverLog(LL_DEBUG,"*** NODE %.40s possibly failing", node->name); node->flags |= CLUSTER_NODE_PFAIL; update_state = 1; } } } ... if (update_state || server.cluster->state == CLUSTER_FAIL) clusterUpdateState(); }
而在 clusterUpdateState
函數裏,會改變 cluster 的狀態。
void clusterUpdateState(void) { static mstime_t among_minority_time; ... { dictIterator *di; dictEntry *de; server.cluster->size = 0; di = dictGetSafeIterator(server.cluster->nodes); while((de = dictNext(di)) != NULL) { clusterNode *node = dictGetVal(de); if (nodeIsMaster(node) && node->numslots) { server.cluster->size++; if ((node->flags & (CLUSTER_NODE_FAIL|CLUSTER_NODE_PFAIL)) == 0) reachable_masters++; } } dictReleaseIterator(di); } { int needed_quorum = (server.cluster->size / 2) + 1; if (reachable_masters < needed_quorum) { new_state = CLUSTER_FAIL; among_minority_time = mstime(); } } ... }
由上面代碼能夠看出,在 minority 中,cluster 狀態在一段時間後會被更改成 CLUSTER_FAIL。但,對於一個劃分到 minority 的 master B,在狀態更改前是一直能夠訪問的,這就有一個時間窗口,會致使 write 丟失!!
在 clusterCron
函數中能夠計算出這個時間窗口的大小。
從 partition 時間開始算起,cluster_node_timeout 時間後纔會有 node 標記爲 PFAIL,加上 gossip 消息傳播會偏向於攜帶 PFAIL 的節點,node B 沒必要等到 cluster_node_timeout/2 把 cluster nodes ping 遍,就能夠將 cluster 標記爲 CLUSTER_FAIL。能夠推算出,時間窗口大約爲 cluster_node_timeout。
另外,會記錄下禁用服務的時間,即 among_minority_time。
(2)對於 majority 部分,slave 會發起選舉,以 B 的 slave B' 爲例,failover 切爲新的 master,並提供服務。
若是 partition 時間小於 cluster_node_timeout,以致於沒有 PFAIL 標識出現,就不會有 write 丟失。
當 partition 恢復後,minority 中的 老 master B 從新加進 cluster,B 要想提供服務,就必須先將 cluster 狀態從 CLUSTER_FAIL 修改成 CLUSTER_OK,那麼,應該何時改呢?
咱們知道 B 中是舊路由,此時它應該變動爲 slave,因此,仍是須要等待一段時間作路由變動,不然有可能出現 write 丟失的問題(前面分析過),一樣在 clusterUpdateState
函數的邏輯裏。
#define CLUSTER_MAX_REJOIN_DELAY 5000 #define CLUSTER_MIN_REJOIN_DELAY 500 void clusterUpdateState(void) { ... if (new_state != server.cluster->state) { mstime_t rejoin_delay = server.cluster_node_timeout; if (rejoin_delay > CLUSTER_MAX_REJOIN_DELAY) rejoin_delay = CLUSTER_MAX_REJOIN_DELAY; if (rejoin_delay < CLUSTER_MIN_REJOIN_DELAY) rejoin_delay = CLUSTER_MIN_REJOIN_DELAY; if (new_state == CLUSTER_OK && nodeIsMaster(myself) && mstime() - among_minority_time < rejoin_delay) { return; } } }
能夠看出,時間窗口爲 cluster_node_timeout,最多 5s,最少 500ms。
failover 可能由於選舉和主從異步複製數據誤差帶來 write 丟失。
master 重啓經過 CLUSTER_WRITABLE_DELAY 延遲,等 cluster 狀態變動爲 CLUSTER_OK,能夠從新訪問,不存在 write 丟失。
partition 中的 minority 部分,在 cluster 狀態變動爲 CLUSTER_FAIL 以前,可能存在 write 丟失。
partition 恢復後,經過 rejoin_delay 延遲,等 cluster 狀態變動爲 CLUSTER_OK,能夠從新訪問,不存在 write 丟失。