Redis Cluster availability分析


この文書は主に個人の理解に基づいて、Redis Cluster設計の中でAvailabilityに対するいくつかの考慮を分析します.
RedisがClusterモードで起動すると、1つのmasterノードに対してクラスタがCLUSTER_である場合のみOK状態の場合、正常にアクセスを受けることができるのは、以前のブログ「Redis Cluster write safety分析」で議論されていた.
Redis Clusterが設計した3つの目標は、最後の1つが可用性です.
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.
以下では主に3つの状況について議論する.
partition障害
Redis Clusterはpartitionが発生した後、minority部分は使用できません.
majorityセクションにmasterの過半数とunreachable masterごとのslaveがあると仮定します.さて、NODE_経由TIMEOUT時間に数秒(slaveにfailoverを行う)を追加すると、clusterは使用可能な状態に戻ります.
クラスタの過半数がマスターに達すると、clusterはCLUSTER_とマークされません.FAIL.
Replicas migration機能
Redis Clusterの設計により、少数のノードで障害が発生した場合でもクラスタが使用可能になります.
例えば、N個のmasterを含むクラスタであり、各masterには一意のslaveがある.単一のnodeで障害が発生し、clusterは依然として利用可能であり、2番目の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機能を提供し、コード分析は以下の通りである.
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;

非CLUSTEROKクラスタは本来要求を正常に受け入れることができず,ドリフトが多く,このような状況を無視している.
(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がない時間があり、誤ドリフトを防ぐためにはドリフトに一定の遅延が必要であり、時間はCLUSTERSLAVE\_MIGRATION\_DELAY、現バージョンは5 sです.
sharding欠落障害
デフォルトでは、slotがバインドされていないことが検出されると、Redis Clusterはリクエストの受信を停止します.この構成では、clusterの一部のノードがオフになった場合、つまり1つの範囲の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はリクエストを拒否します.