Redis Cluster write safety分析


redis clusterはredisの分散実装である.公式文書cluster-specが強調したように、その設計は高性能と線形拡張能力を優先し、write safetyを保証するために最善を尽くしている.
ここでwriteロスとは、クライアントackに返信した後、後続のリクエストでデータが変更されていない場合やロスしている場合を指し、主に切替、インスタンス再起動、脳裂の3つのケースがこの問題を引き起こす可能性があり、以下、順次分析する.

ケース1主従切替


failoverはルーティングの変更をもたらし,アクティブ/パッシブの場合は別々に議論する必要がある.

受動failover


表現の便宜上、cluster状態は正常で、node Cはmasterで、slot 1-100を担当し、slaveはC'に対応すると仮定している.
マスター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は、プライマリスレーブ非同期レプリケーションメカニズムによって発生します.マスターに書き込まれたデータがslaveに同期できずに切られた場合、このデータは失われます(再起動後はmerge操作は存在しません).master返信client ackは同期slaveとほぼ同時に行われ、このような状況はめったに発生しないが、これはリスクであり、時間ウィンドウが小さい.

アクティブfailover


アクティブfailoverはsysadminによってslave node上でCLUSTER FAILOVER [FORCE|TAKEOVER]コマンドを実行してトリガーされます.
完全manual failoverプロセスは、前のブログで詳細に議論され、以下の6つのステップに要約される.
  • slaveは要求を開始し、gossipメッセージはCLUSTERMSMGを携帯する.TYPE_MFSTARTマーク.
  • masterはclientをブロックし、停止時間は2倍CLUSTER_MF_TIMEOUT、現在のバージョンは10 sです.
  • slaveは、プライマリからoffsetデータのコピーを追跡する.
  • slaveが選挙を開始し、当選した.
  • slaveは、自身のroleを切り替え、slotsを引き継ぎ、新しいルーティング情報をブロードキャストする.
  • 他のノードはルーティングを変更し、clusterルーティングは平らになります.

  • 3つのオプションにはそれぞれ異なる動作があり、以下のように分析され、(1)デフォルトのオプションです.完全なmfプロセスを実行すると、masterは停止動作をするため、writeが失われる問題はありません.
    (2)FORCEオプション.手順4から実行します.slave C'統計票フェーズでは、master Cは依然としてユーザー要求を正常に受信することができ、writeの損失を招く可能性があります.mfは将来のある時点で実行を開始し、timeout時間はCLUSTER_MF_TIMEOUT(現バージョン5 s)は、clusterCron毎にチェックされます.
    (3)TAKEOVERオプション.手順5から実行します.slaveは自分のconfigEpoch(他のnodeの同意を必要としない)を直接増やし、slotsを引き継ぐ.slave C'からmasterに切り替え、元のmasterノードCにルーティングを更新し、Cに送信する要求に至るまで、writeが失われる可能性があり、一般的にはpingの時間内に完了し、タイムウィンドウが小さい.CとC'以外のノードがルーティングヒステリシスを更新すると、一度だけMOVEDエラーが発生し、writeが失われることはありません.

    ケース2マスター再起動


    cluster状態初期化


    clusterState構造体には、clusterのグローバル状態を表すstateメンバー変数があり、現在のclusterがサービスを提供できるかどうかを制御します.次の2つの値があります.
     #define CLUSTER_OK 0 /* Everything looks ok */
     #define CLUSTER_FAIL 1 /* The cluster can't work */

    サーバが再起動すると、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が現在の接続ではない場合、本当にcommandを実行するnodeを指す-MOVEのリダイレクトエラーが返されます.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を保証するために非常に必要だと言っています.マスターAが切れると、対応するslave A'が選挙で新マスターに当選することが想像できる.このとき、Aは再起動され、ちょうどclientが見たルートが更新されていないので、Aにデータを書きます.これらのwriteを受け入れると、データが失われます.A'こそこのshardingみんなが公認しているマスターです.したがって、A'が再起動されると、ルーティング変更が完了するまでサービスを無効にする必要があります.

    cluster状態変更


    では、いつ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ミリ秒、現在のバージョンは2 sです.
    アクセス遅延はルーティング変更を待つためですが、ルーティング変更はいつトリガーされますか?新しいserverが起動したばかりで、他のnodeとgossip通信を行うlinkはnullであり、clusterCronでチェックされた後、順次接続され、pingが送信されることを知っています.ルーティングが期限切れになった古いノードとして、他のノードからupdateメッセージを受信し、自身のルーティングを変更します.
    CLUSTER_WRITABLE_DELAYミリ秒後、Aノードはアクセスを再開し、CLUSTER_WRITABLE_DELAYのタイムウィンドウはルーティングを更新するのに十分です.

    case 3 partition


    partition発生


    ネットワークの信頼性が低いため,ネットワークパーティションはCAP理論におけるPという考慮すべき問題である.
    partitionが発生するとclusterはmajorityとminorityの2つの部分に分割され,ここではパーティション内のmasterノードの数で区別される.
    (1)minority部分ではslaveが選挙を開始するが、多くのmasterの票を受け取ることができず、正常なfailoverプロセスを完了することができない.また、clusterCronでは、ほとんどのノードがCLUSTERとしてマークされます.NODE_PFIAL状態、さらに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に区切られたマスターBについては、ステータスが変更されるまでアクセスできます.これにはタイムウィンドウがあり、writeが失われます!!
    このタイムウィンドウのサイズは、clusterCron関数で計算できます.partition時間から計算するとcluster_node_timeout時間後にnodeがPFIALとマークされ、gossipメッセージ伝播がPFIALを携帯するノードに偏っているため、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は、PFIEL識別子が現れなければwriteが失われないほどです.

    partitionリカバリ


    partitionが回復すると、minorityの古いマスター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、最大5 s、最低500 ms.

    小結


    failoverは、選挙とプライマリの非同期レプリケーションデータの偏差によってwriteが失われる可能性があります.マスター再起動CLUSTER経由WRITABLE_DELAY遅延、cluster状態がCLUSTER_に変更されるのを待つOK、再アクセス可能、writeロスは存在しません.partitionのminority部分、cluster状態でCLUSTER_に変更FAILの前に、writeの紛失がある可能性があります.partitionが回復した後、rejoin_を通過delay遅延、cluster状態がCLUSTERに変更されるのを待つOK、再アクセス可能、writeロスは存在しません.