本文檔主要基於我的理解,分析 Redis Cluster 設計中對於 Availability 的一些考量。html
當 Redis 以 Cluster 模式啓動時,對於一個 master 節點,只有當集羣爲 CLUSTER\_OK 狀態時,才能正常接受訪問,這在以前的博客 《Redis Cluster write safety 分析》討論過。node
Redis Cluster 設計的三個目標,最後一個纔是可用性。git
Redis Cluster is able to survive partitions where the majority of the master nodes are reachable and there is at least one reachable slave for every master node that is no longer reachable. Moreover using replicas migration, masters no longer replicated by any slave will receive one from a master which is covered by multiple slaves.
下面主要討論三種狀況。redis
Redis Cluster 在發生 partition 後,minority 部分是不可用的。app
假設 majority 部分有過半數 master 及每一個 unreachable master 的一個 slave。那麼,通過 NODE\_TIMEOUT 時間加額外幾秒鐘(給 slave 進行 failover),cluster 恢復可用狀態。函數
當集羣中有過半數 master 可達,cluster 就不會標記成 CLUSTER\_FAIL。ui
Redis Cluster 的設計,保證了少數節點發生故障時,集羣依然可用。this
舉個例子,包含 N 個 master 的集羣,每一個 master 有惟一 slave。
單個 node 出現故障,cluster 仍然可用,第二個 node 再出現故障,集羣仍然可用的機率是 1-(1/(N*2-1)。
計算方式以下,第一個 node fail 後,集羣剩下 N*2-1 個健康節點,此時 orphan master 剛好 fail 的機率是 1/(N*2-1)。
套用公式,假設 N = 5,那麼,2 個節點從 majority partition 出去,集羣不可用的機率是 11.11%。設計
redis 爲了提升集羣可用性,提供了 replicas migration 功能,代碼分析以下,code
void clusterCron(void) { int orphaned_masters; int max_slaves; int this_slaves; ... di = dictGetSafeIterator(server.cluster->nodes); while((de = dictNext(di)) != NULL) { clusterNode *node = dictGetVal(de); ... if (nodeIsSlave(myself) && nodeIsMaster(node) && !nodeFailed(node)) { // 統計 node 有幾個健康的 slave int okslaves = clusterCountNonFailingSlaves(node); // 沒有 slave 但依然負責 slot 的 master if (okslaves == 0 && node->numslots > 0 && node->flags & CLUSTER_NODE_MIGRATE_TO) { orphaned_masters++; } if (okslaves > max_slaves) max_slaves = okslaves; // node 是個人 master if (nodeIsSlave(myself) && myself->slaveof == node) this_slaves = okslaves; } } ... if (nodeIsSlave(myself)) { ... if (orphaned_masters && max_slaves >= 2 && this_slaves == max_slaves) clusterHandleSlaveMigration(max_slaves); } ... }
首先定義了什麼樣的節點算 orphaned master,即,負責部分 slot 但沒有健康 slave 的 master。orphaned master 有可用性風險,一旦掛掉,則整個 sharding 不可用。
以上代碼能夠看出,當 slave 檢測到本身的 master 擁有很多於 2 個健康 slave ,且 cluster 中剛好有 orphan master 時,觸發 clusterHandleSlaveMigration
函數邏輯,嘗試進行 slave 漂移,漂移步驟有 4 步,下面進行分步說明。
(1)CLUSTER\_FAIL 集羣漂移。
if (server.cluster->state != CLUSTER_OK) return;
非 CLUSTER\_OK 集羣原本就沒法正常接受請求,漂移畫蛇添足,忽略掉這種狀況。
(2)檢查 cluster-migration-barrier 參數。
redis conf 提供了cluster-migration-barrier 參數,用來決定 slave 數量達到多少個纔會把冗餘 slave 漂移出去。
for (j = 0; j < mymaster->numslaves; j++) if (!nodeFailed(mymaster->slaves[j]) && !nodeTimedOut(mymaster->slaves[j])) okslaves++; if (okslaves <= server.cluster_migration_barrier) return;
只有 mymaster 健康 slave 的個數超過 cluster-migration-barrier 配置的數量時,纔會漂移。
(3)選出要漂移的 slave以及漂給誰。
candidate = myself; di = dictGetSafeIterator(server.cluster->nodes); while((de = dictNext(di)) != NULL) { clusterNode *node = dictGetVal(de); int okslaves = 0, is_orphaned = 1; if (nodeIsSlave(node) || nodeFailed(node)) is_orphaned = 0; if (!(node->flags & CLUSTER_NODE_MIGRATE_TO)) is_orphaned = 0; if (nodeIsMaster(node)) okslaves = clusterCountNonFailingSlaves(node); if (okslaves > 0) is_orphaned = 0; if (is_orphaned) { if (!target && node->numslots > 0) target = node; if (!node->orphaned_time) node->orphaned_time = mstime(); } else { node->orphaned_time = 0; } if (okslaves == max_slaves) { for (j = 0; j < node->numslaves; j++) { if (memcmp(node->slaves[j]->name, candidate->name, CLUSTER_NAMELEN) < 0) { candidate = node->slaves[j]; } } } } dictReleaseIterator(di);
選擇 node name 最小的 slave ,漂移給遍歷到的第一個 orphaned master,若是有多個。
(4)執行漂移。
#define CLUSTER_SLAVE_MIGRATION_DELAY 5000 if (target && candidate == myself && (mstime()-target->orphaned_time) > CLUSTER_SLAVE_MIGRATION_DELAY) { serverLog(LL_WARNING,"Migrating to orphaned master %.40s", target->name); clusterSetMaster(target); }
在 failover 期間,master 有一段時間是沒有 slave 的,爲防止誤漂,漂移必須有必定的延遲,時間爲 CLUSTER\_SLAVE\_MIGRATION\_DELAY,現版本爲 5s。
默認狀況下,當檢測到有 slot 沒有綁定,Redis Cluster 就會中止接收請求。
在這種配置下,若是 cluster 部分節點掛掉,也就是說一個範圍內的 slot 再也不有節點負責,最終整個 cluster 會變得不能提供服務。
有時候,服務部分可用比整個不可用更有意義,所以,即便一部分 sharding 可用,也要讓 cluster 提供服務。redis 將這種選擇權交到了用戶手中,conf 裏提供 cluster-require-full-coverage 參數。
void clusterUpdateState(void) { ... if (server.cluster_require_full_coverage) { for (j = 0; j < CLUSTER_SLOTS; j++) { if (server.cluster->slots[j] == NULL || server.cluster->slots[j]->flags & (CLUSTER_NODE_FAIL)) { new_state = CLUSTER_FAIL; break; } } } ... }
以上代碼能夠看到,若是配置 cluster-require-full-coverage 爲 yes,那麼,有 slot 未綁定或者 sharding 缺失,會將 cluster 狀態設置爲 CLUSTER\_FAIL,server 就會拒絕請求。