リバランスとは、既存のデータとサービス責任を現在の配置から目標の配置へ調整することです。トリガーには、スケールアウト、スケールイン、ホットスポット、および障害復旧が含まれます。これには「何を移動するか」「いつターゲットサービスに切り替えるか」「障害発生時に何に基づいて復旧するか」という3つの問題が関与します。
「一貫性ハッシュは所属先をわずかにしか変更しない」というのは、最初の質問の一部にしか答えていません。移行は、ルーティングテーブルを変更してファイルをコピーするだけではありません。コピーの間、ビジネスデータは継続的に変更され、古いクライアントは引き続き元の位置にリクエストを送信する可能性があります。
移行の粒度:固定パーティション、動的分割、トークン範囲
設計 移動単位 利点 主な境界
固定論理パーティション 1つまたは複数のパーティション キーからパーティションへのマッピングが安定 粒度が粗すぎる可能性があり、パーティションの負荷が必ずしも均等ではない
動的パーティション 分割・結合可能なキー範囲 データ量やホットスポットの変化に対応可能 境界、バージョン、並列操作の管理が必要
ハッシュリング トークンが覆う範囲 メンバーの変化がマッピングの一部のみ影響 範囲のバイト量とリクエスト量が不均等になる可能性がある
例えば、1000個の等しいサイズのパーティションが元々10台のマシンに均等に配置されていたとします。新しいマシンが1台追加された場合、各元のマシンから9個のパーティションを移動させることができます。その結果、元のノードはそれぞれ91個、新しいノードは90個になり、合計は依然として1000個です。すべてのパーティションを連続して再番号付けする必要はありません。移動対象のパーティションの所有者を更新するだけで十分です。
これは等しいサイズを想定した教育用の例です。実際のスケジューリングでは、バイト量、読み書き速度、ディスク容量、障害ドメインも考慮する必要があり、パーティション数だけで平均化することはできません。ホットスポットを解消するためには、純粋な容量均衡よりも多くの移行が必要になることもあります。
分割と移行は異なります:分割は論理境界を変更し、移行は配置位置を変更します。1つのパーティションが2つに分割されても、元のマシン上にそのまま残すことができます。境界が安定してから、そのうちの1つを移動します。メタデータはレプリケーションされたコントロールプレーンによって管理できるため、必ずしも単一の障害耐性のない中心ノードに依存する必要はありません。
なぜ「先にコピーしてからルーティングを変更する」だけでは不十分なのか
コピー開始時に x=1 であり、コピー中にユーザーが x を 2 に変更したとします。もしターゲットにコピーされるのがスナップショット内の 1 のみであれば、ルーティングを切り替えた後に古い値が読み取られてしまいます。新旧のノードに同時に書き込む場合、旧ノードは成功し新ノードがタイムアウトした場合、両方の位置で依然として不整合が発生する可能性があります。
正しいプロトコルは、以下の質問に答える必要があります:どのログがこの更新を記録しているか?ターゲットはどの位置から補完するか?タイムアウト再試行は重複して適用されるか?スナップショット内の古いレコードが先に到着した新しい値を上書きしないか?削除はどのように同期されるか?これらの問題は、「二重書き込みでデータ消失を保証する」という一言で片付けることはできません。
推論可能な単一書き込み者移行プロトコル
以下では、ソース S、ターゲット T、およびバージョン付きルーティングを用いて一般的な設計を記述します。システムが移行状態を永続的に記録し、書き込み権限を制約できると仮定します。これはすべてのデータベースが採用している固定実装ではありません。
単一書き込み者移行の例:データ追従と権限引継ぎは2つのステップ
移行レコードの作成
ソース、ターゲット、範囲、タスクID、およびルーティングバージョンを記録
1
ソースは引き続きサービスを提供し、ターゲットはスナップショットを受信
固定スナップショットに対応するログ位置 L0
2
ターゲットが L0 以降の増分をリプレイ
更新と削除を含み、リプレイ位置を永続的に記録
3
切り替えバリア Lcut の作成
古いエポックの新しい書き込みをブロックし、ターゲットをバリアまで追従させる
4
新しい所有者と新しいエポックの公開
古いエントリポイントは拒否、リダイレクト、またはプロトコルに従って転送
5
検証と古いコピーのクリーンアップ
復旧と保持条件を確認してから回収
6
これはプロトコルの例です。コピーメンバーのメンバーシップ変更は、独自のレプリケーションと合意形成ルールに従う必要があります。
スナップショットはログの境界に対応しなければならない
一貫性のあるスナップショットを取得し、ログ位置 L0 を特定します。スナップショットはその境界までの状態をカバーし、増分はそれ以降の変更をカバーします。ターゲットはまずスナップショットをロードしてから増分をリプレイするか、信頼できるバージョンルールでマージし、古いスナップショットが新しい更新を上書きしないようにします。ログの保持期間は十分に長くする必要があります。そうしないと、ターゲットが追いつく前に必要な増分が回収され、スナップショットを再構築せざるを得なくなります。
リプレイには、回復可能な位置と冪等性セマンティクスが必要です。同じバージョンへの重複書き込みは識別可能である必要があります。削除は、ログイベントや tombstone などの形式で伝播しなければならず、ターゲットがレコードを見ていないからといって、削除がすでに同期されていると見なしてはいけません。
ターゲットコピーがギャップと遅延スナップショットを拒否する方法
スナップショットが位置 10 に対応し、状態が {x:1,y:9} であるとします。その後、ログ 11 で x が 2 に変更され、12 で y が削除されます。以下では、これが Watch のように他のキーのバージョンをスキップできるものとは異なり、専用で位置が連続したパーティションログであると仮定します。各適用はデータと位置の両方を同時に公開します。実際のストレージでは、トランザクションまたは回復プロトコルを使用してこのアトミック関係を維持する必要があります。
class Target :
def __init__ ( self ) :
self . position = None
self . data = { }
def install_snapshot ( self , position , data ) :
if self . position is not None :
raise ValueError ( " このタスクはすでにスナップショットをインストールしており、増分後の状態を上書きできません" )
self . data , self . position = dict ( data ) , position
def replay ( self , position , operation , key , value = None ) :
if self . position is None :
raise ValueError ( " まずスナップショットを完全にインストールしてください" )
if position <= self . position :
return False if position != self . position + 1 :
raise ValueError ( " ログにギャップがあります。スキップできません" )
updated = dict ( self . data )
if operation == " put" :
updated [ key ] = value
elif operation == " delete" :
updated . pop ( key , None )
else :
raise ValueError ( " 不明なログタイプ" )
self . data , self . position = updated , position
return True
target = Target ( )
target . install_snapshot ( 10 , { " x" : 1 , " y" : 9 } )
assert target . replay ( 11 , " put" , " x" , 2 )
assert not target . replay ( 11 , " put" , " x" , 2 )
try :
target . replay ( 13 , " put" , " z" , 3 )
except ValueError :
pass
assert target . position == 11 and " z" not in target . data
assert target . replay ( 12 , " delete" , " y" )
assert target . data == { " x" : 2 }
try :
target . install_snapshot ( 10 , { " x" : 1 , " y" : 9 } )
except ValueError :
pass
assert target . data == { " x" : 2 } and target . position == 12
このモデルは、「スナップショットの完全なインストール後にのみリプレイを開始する」という直列戦略を採用しています。したがって、12 の削除は、このタスク内の古いスナップショットを防ぐために永遠に tombstone を保持する必要はありません。古いスナップショットのインストール自体が拒否されているためです。もし無順序ブロックと増分の並列マージを許可する場合、x の巻き戻しや y の復活を防ぐために、より細かなバージョンルールが必要です。
同じ position は、不変のログレコードの1つだけに該当する必要があります。実際のシステムでは、移行タスクID、ソースログID、スナップショットチェックサム、および範囲を検証する必要があり、別のタスクの位置 11 を処理済みとして扱ってはなりません。例では位置はメモリ内のみで、プロセスまたはディスクのクラッシュはシミュレートされていません。永続的な実装では、「position は 12 に達したが、削除はまだ保存されていない」という状態が残らないことを確実にする必要があります。
データの追従後、書き込み権限の引継ぎが必要
「ターゲットの遅延がゼロである」のは、ある時点での観察に過ぎません。ソースは次の瞬間にも新しい書き込みを受け付ける可能性があるため、明確な切り替えバリア Lcut が必要であり、古い書き込み者が制約のない成功する書き込みを生成できないように保証する必要があります。
単純な実装では、切り替えウィンドウ中にこの範囲への新しい書き込みを一時的にブロックし、受け付けられた操作の完了を待ち、ターゲットが Lcut まで適用してから新しいルーティングバージョンを公開します。より複雑な実装では、転送、ログプロトコル、またはレプリケーショングループの再構成によって中断時間を短縮できますが、それでも古い書き込みが漏れないことを証明する必要があります。
エポックは権限の世代です。リクエストに古いバージョンが含まれている場合、サーバーはプロトコルに基づいて拒否または転送を行うべきです。クライアントがキャッシュを更新するだけでは、復旧後もマスターであると認識し続ける古いプロセスを隔離するには不十分です。権限チェックは、実際に書き込みを受け付けるリソースの境界で実施されなければなりません。
「先ほど追従した」と「バリア後に追従した」には1回の書き込みの差がある
t1 でソースとターゲットの両方が 12 であると観察されたとします。t2 でソースがログ 13 を受け付け、t3 でコントローラが t1 の観察に基づいて直接ルーティングを切り替えた場合、ターゲットはすでに成功した 13 を欠落します。ラグゼロは瞬間的な写真であり、その後の書き込みを禁止するバリアとして機能することはできません。
瞬間的なラグゼロでは、ソースが次の瞬間に新しい書き込みを受け付けるのを阻止できない
t1:S=12、T=12
この時点で追従完了
t2:S が 13 を受け付け
T はまだ 12
t3:T を有効化できない
そうすると、受け付けられた 13 が漏れる
正しい切り替え:古い書き込み権限を無効化 → Lcut を確定 → T が Lcut まで適用するのを待つ
古い権限で受け付けられた在途操作を完了させてから、最終バリアを固定する。
有効なバリアを確立する鍵は、古い権限による新しい書き込みの受け付けを先に停止し、すでに受け付けられた在途操作を処理することです。これにより、増加し続けることのない Lcut を得ることができます。最後に受け付けられた操作が 13 である場合、ターゲットは 13 まで追従しなければなりません。以前ソースの末尾 12 まで追従していたからといって、早期に有効化することはできません。この短時間の書き込み拒否ウィンドウは、この単純化されたプロトコルが明確な引継ぎ境界を得るために支払う可用性のコストです。
ルーティングの公開にはアトミックな決定があり、クライアントは同時に更新されない
メタデータ内に明確な所有権切り替えポイントが存在する場合でも、クライアント、プロキシ、およびロングコネクションが新しい状態を認識するタイミングは異なります。そのため、古いエントリポイントは遷移動作を行う必要があり、ネットワーク全体のルーティングが瞬時に一貫すると仮定してはいけません。リダイレクトと転送は回数を制限する必要があり、2つのノードがお互いを指し合ってループを形成しないようにします。
コピーの移行は、投票メンバーを直接置き換えることを意味しません。Raft などのレプリケーショングループは、独自のメンバーシップ変更プロトコルを使用して quorum の安全性を保証する必要があります。移行ツールは、それ自体が新しいコピーに完全な権限があることを宣言するために、それを迂回することはできません。
先ほど追従したからといって、今すぐ切り替えられるわけではない
ターゲットを先に追従させ、ソースに1回書き込むことで、「ラグゼロの後に再び遅れる」という反例を構築できます。フリーズボタンには、受け付けの停止と在途書き込みの完了が含まれ、固定された Lcut を得ます。これはクライアントがルーティングキャッシュを更新するものではありません。新しい所有者を公開する前に、古い権限がすでに隔離され、ターゲットがバリアをカバーしていることを同時に満たす必要があり、切り替え後も古いソースを有効な書き込みエントリポイントとして扱ってはなりません。
図を準備しています 再読み込み
先ほど追従したからといって、今すぐ切り替えられるわけではない ソースがまだ書き込み可能な場合、ラグゼロは瞬間的な観測に過ぎません。増加しない Lcut を先に確立し、ターゲットがそれまで適用してから、このモデルで所有権を引継ぐことができます。
{"id":"migration-cutover-barrier","title":"先ほど追従したからといって、今すぐ切り替えられるわけではない","summary":"ソースがまだ書き込み可能な場合、ラグゼロは瞬間的な観測に過ぎません。増加しない Lcut を先に確立し、ターゲットがそれまで適用してから、このモデルで所有権を引継ぐことができます。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"metrics\"><div><span data-i18n=\"source\"></span><strong id=\"source\"></strong></div><div><span data-i18n=\"target\"></span><strong id=\"target\"></strong></div><div><span data-i18n=\"owner\"></span><strong id=\"owner\"></strong></div></div><div id=\"log\" class=\"track\"></div><p class=\"formula\" id=\"cut\"></p><div class=\"actions\"><button id=\"write\" data-i18n=\"write\"></button><button id=\"catchup\" data-i18n=\"catchup\"></button><button id=\"freeze\" data-i18n=\"freeze\"></button><button id=\"cutover\" data-i18n=\"cutover\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"open\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"wait\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"done\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"rejected\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n\n.metrics{grid-template-columns:repeat(3,minmax(0,1fr))}#log{min-height:52px;display:grid;grid-template-columns:repeat(6,minmax(0,1fr))}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let source,target,cut,owner;function reset(){source=target=10;cut=null;owner='S';draw();}function draw(note){set('source',source);set('target',target);set('owner',owner);set('cut','Lcut = '+(cut===null?'—':cut)+'; lag = '+(source-target));cells('log',Array.from({length:6},(_,i)=>({text:11+i,cls:11+i<=target?'on':11+i<=source?'gold':'empty'})));$('#catchup').disabled=target>=source;$('#freeze').disabled=cut!==null;$('#cutover').disabled=cut===null||target<cut||owner==='T';$('#write').disabled=source>=16;set('explanation',t(note||(owner==='T'?'done':cut===null?'open':'wait')));}$('#write').onclick=()=>{if(cut!==null){draw('rejected');return;}source++;draw();};$('#catchup').onclick=()=>{target++;draw();};$('#freeze').onclick=()=>{cut=source;draw();};$('#cutover').onclick=()=>{owner='T';draw();};$('#reset').onclick=reset;reset();","audio":false,"strings":{"catchup":"ターゲットが1回追従","cutover":"新しい所有者を公開","done":"所有権が引継がれました;古いソースが遅延リクエストを受け取っても、書き込みを受け付けることはできません。","freeze":"古い書き込み権限をフリーズ","heading":"先ほど追従したからといって、今すぐ切り替えられるわけではない","intro":"ソース S は 10 から開始し、ターゲット T はスナップショット 10 をすでにロードしています。ソースの書き込みと追従を交互に実行し、古い書き込み権限をフリーズして切り替え境界を確定します。","open":"ソースは新しい書き込みを受け付け可能です;ラグゼロは切り替えの許可ではありません。","owner":"現在の所有者","rejected":"古いソースの書き込みは拒否され、ログは増加していません。","reset":"リセット","scope":"単一書き込み者、連続不変ログ、ターゲットスナップショットはすでにインストール済み。フリーズ動作には、受け付けられた書き込みの排空が含まれます。切り替えはアトミックに権限を更新し、古いソースのリクエストは拒否されます。具体的な製品の移行はシミュレートしません。","scopeLabel":"モデルの範囲と規約","source":"ソースログ位置","target":"ターゲット適用位置","wait":"バリアが確立されました;ターゲットは Lcut まで適用してから有効化できます。","write":"古いソースに書き込む"}}
障害復旧:永続状態が次のステップを決定する
失敗位置 復旧時に確認すべきこと
スナップショットの転送途中、ターゲット再起動 タスクとスナップショットIDが一致するか、継続転送可能か、再構築が必要か
増分リプレイの中断 最後に永続的に適用された位置、重複イベントの処理方法
切り替えリクエストのタイムアウト 永続的な所有権決定は何であるか、タイムアウトで失敗を推測してはならない
切り替え後、古いソースが再オンライン 古いエポックがすでに無効かどうか、誤って書き込みを受け付けることができるか
ターゲット有効化後、パフォーマンス不足を発見 新しい制御された移行を実行するか、古いコピーはすでに時代遅れである可能性がある
切り替え前には通常、移行を停止してソースサービスを維持できます。切り替え後、ターゲットが新しい書き込みを受け付けている場合、古いコピーを直接有効化して「ロールバック」として使用することはできません。ターゲットの新しい変更を同期し、権限を再引継ぎする必要があります。
容量とレートリミット:追従は新增分よりも速くなければならない
スナップショットサイズを D、有効なレプリケーション帯域幅を b とします。圧縮やその他のボトルネックを考慮しない場合、フルデータフェーズには少なくとも D/b が必要です。その後、バックログが B あり、増分の発生率が r、ターゲットの有効適用率が a である場合、安定したレートでの単純化されたモデルでは、追従には約 B/(a-r) 必要です。ただし、a>r という条件があります。a≤r の場合、待っているだけでは追いつけません。
したがって、レートリミットはネットワークだけを見てはいけません。ディスクレイテンシ、ライト増幅、CPU、ログ保持スペース、ターゲット適用率、およびフロントエンドリクエストのテールレイテンシも監視する必要があります。スケジューリングは並列移行範囲を制限し、障害復旧用の容量を予約し、サービス指標が悪化すると一時停止する必要があります。すべてのシナリオで1台ずつスケールアウトする必要はありません。並列度はリソース予算とプロトコルの制限によって決定されるべきです。
Redis Cluster:一時的な転送と所属変更の区別
Redis Cluster は 16384 個のハッシュスロットを使用し、スロットとノードのマッピングは変化することがあります。移行に関連するクライアントプロトコルにおいて、ASK はこのリクエストを一時的に指定されたノードで実行する必要があることを示し、ASKING と組み合わせて使用されます。これは、そのスロットの長期ルーティングを直接新しいノードに変更することを要求するものではありません。MOVED は、スロットの所属に関する認識を更新すべきであることを示します。これは、移行中の一時的なサービス位置と安定したルーティングを混同してはならないことを示しています。Redis Cluster 仕様
以下の純粋なルーティングモデルを使用して、2種類の応答を区別します。スロット 8 は当初 S にキャッシュされています。ASK を受け取ったときは、このリトライ計画のみを生成し、MOVED を受け取った場合にのみ安定したスロットキャッシュを更新します。
routes = { 8 : " S" }
def redirected ( kind , slot , target ) :
if kind == " ASK" :
return target , True if kind == " MOVED" :
routes [ slot ] = target
return target , False
raise ValueError ( " サポートされていないリダイレクトタイプです" )
assert redirected ( " ASK" , 8 , " T" ) == ( " T" , True )
assert routes [ 8 ] == " S" assert redirected ( " MOVED" , 8 , " T" ) == ( " T" , False )
assert routes [ 8 ] == " T"
ASKING は接続上での1回限りの許可であり、1つの接続で ASKING を送信し、プールから別の接続を取得して元のリクエストを送信することはできません。実際のクライアントは、2つのコマンドが同じ接続上で正しく連携されることを保証し、リダイレクト回数と全体のデッドラインを処理する必要があります。ここではスロットキャッシュの変化のみをシミュレートしており、接続プール、コマンド実行、または全スロットテーブルの更新は実装されていません。後者は MOVED 後に一般的に使用される更新方法です。ASK とクライアントルーティングセマンティクス
この例はルーティング引継ぎを理解するためのものであり、Redis が上記の完全なスナップショットとログ移行フローを採用していることを意味するものではありません。
続きを読む:一貫性ハッシュ 、シャーディング戦略 、障害検出 。
参考
Keywords: リバランス, リシャーディング, スナップショット, ログ追従, フェンシング, エポック, ルーティング, ASK, MOVED