コンシステントハッシュ法
ノードを足し引きするたびに全データが再配置される地獄から抜け出すための定石。リング構造と仮想ノードの原理を押さえれば、分散キャッシュやシャーディングの再配置コストを最小化できます。
- 1.単純な mod N 方式はノード数 N が変わると大半のキーが別ノードへ移動する。コンシステントハッシュ法は移動するキーを平均で全体の 1/N 程度に抑える。
- 2.ハッシュ空間を環(リング)とみなし、キーは時計回りに最初に出会うノードへ割り当てる。ノードの増減は隣接区間だけに影響する。
- 3.1台を多数の仮想ノードとして環に配置することで負荷の偏りを平準化し、各ノードの容量差も重みとして表現できる。
なぜ mod N では困るのか
複数のサーバーにキーを振り分ける最も素朴な方法は、ハッシュ値をノード数で割った余りを使う node = hash(key) % N です。N が固定なら高速で均等ですが、N が変わった瞬間に破綻します。
たとえば N=4 から N=5 へ1台増やすと、各キーの割り当ては hash(key) % 4 から hash(key) % 5 に変わります。余りの周期が変わるため、ほとんどのキーが別のノードへ移動します。分散キャッシュなら大半のエントリがキャッシュミスになり、シャーディングなら大量のデータ再配置(リシャーディング)が走ります。
N から N+1 へ変えたとき、移動せずに済むキーはおよそ全体の 1/(N+1) しかありません。残りの大半が動きます。10台を11台にしただけで9割近いキーが再配置対象になる計算で、運用中のシステムでは到底許容できません。コンシステントハッシュ法はこの移動量を 平均で 1/N 程度 に抑えることを目的とした手法です。
リング構造:時計回りの担当決め
コンシステントハッシュ法では、ハッシュ関数の出力空間(たとえば 0 から 2^32 − 1)を**端と端をつないだ環(リング)**とみなします。ここに2種類の点を配置します。
- ノード: 各サーバーの識別子(IP やホスト名)をハッシュした値をリング上の位置とする。
- キー: データのキーをハッシュした値をリング上の位置とする。
割り当てルールは単純です。あるキーの位置から時計回りに進み、最初に出会ったノードがそのキーの担当になります。各ノードは「自分の手前のノードから自分まで」の円弧区間を受け持ちます。
リング(0 → 最大値 → 0 に戻る)
[Node A]
/ \
key3 key1
| |
[Node C] ------- [Node B]
\ /
key2 ─→ 時計回りで最初のノードが担当
key1 → Node B(key1 の右隣のノード)
key2 → Node C
key3 → Node A
この構造の利点は局所性です。ノードを1台追加すると、新ノードはリング上の1点に入り、その直前区間を担当していた1台からだけキーを引き取ります。他のノードの担当区間は一切変わりません。ノードを1台削除した場合も同様に、その担当ぶんが時計回りの次のノードへ吸収されるだけで、無関係なキーは動きません。
リングが均等に分割されているとき、1台が受け持つ区間はリング全体の約 1/N です。ノードを1台増減すると影響を受けるのは隣接する 1〜2 区間だけなので、移動するキーは全体の 約 1/N。N が大きいほど1回の増減で動くキーの割合は小さくなります。これが mod N との決定的な差です。
検索の実装:ソート済みリングを二分探索
リングは実装上、ノード位置をソートした配列(または平衡木)として持ちます。キーの担当ノードを引くには、キーのハッシュ値以上で最小のノード位置を探せばよく、二分探索で O(log N) で求まります。配列末尾を超えたら先頭へ折り返す(環なので)点だけ注意します。
import bisect, hashlib
class HashRing:
def __init__(self):
self.ring = {} # 位置 -> ノード
self.sorted_keys = [] # ソート済みの位置リスト
def _hash(self, s):
return int(hashlib.md5(s.encode()).hexdigest(), 16)
def add_node(self, node):
pos = self._hash(node)
self.ring[pos] = node
bisect.insort(self.sorted_keys, pos)
def get_node(self, key):
if not self.ring:
return None
h = self._hash(key)
# h 以上で最小の位置を探す。末尾を超えたら先頭へ折り返す
i = bisect.bisect_left(self.sorted_keys, h) % len(self.sorted_keys)
return self.ring[self.sorted_keys[i]]
bisect_left の結果を要素数で割った余りを取ることで、環の折り返し(最大値を超えたキーは先頭ノードへ)を自然に表現しています。
仮想ノード:偏りをならす
素朴なリングには弱点があります。ノードをランダムにハッシュ配置すると、区間の大きさが大きくばらつくことです。たまたま隣接して配置された2ノードの間は狭く、離れた区間を1台が抱えると、その1台に負荷が集中します。N が小さいほどこの偏りは顕著です。
解決策が仮想ノード(virtual node / vnode)です。1台の物理ノードを、NodeA#0, NodeA#1, ... のようにサフィックスを変えた複数の点としてリング上にばらまくのです。
- 物理ノード1台あたり仮想ノードを V 個(数十〜数百)配置する。
- 各仮想ノードは独立にハッシュされるので、リング全体に細かく散らばる。
- 1台の物理ノードの担当区間は、V 個の小区間の合計になる。
小さな区間を多数集めれば大数の法則により合計がならされ、各物理ノードの負荷は平均へ収束します。V が大きいほど分散は小さくなります(標準偏差はおおむね V の平方根に反比例)。
仮想ノードの個数を物理ノードごとに変えれば、容量差をそのまま重みとして表現できます。CPU やメモリが2倍のノードには仮想ノードを2倍配置すれば、担当区間も約2倍になり、自然と多くのキーを引き受けます。ヘテロな(性能がまちまちな)クラスタを均す常套手段です。
| 観点 | 仮想ノードなし | 仮想ノードあり |
|---|---|---|
| 負荷の均等性 | 区間のばらつきが大きく偏りやすい | 多数の小区間を集約してならされる |
| ノード離脱時の引き継ぎ | 1台が全ぶんを抱える | 複数の生存ノードへ分散される |
| 容量差の表現 | 難しい | 仮想ノード数で重み付け可能 |
| メモリ・探索コスト | 小さい | リング要素が V 倍に増える |
仮想ノードのもう一つの効能は離脱時の負荷分散です。仮想ノードがないと、1台が落ちたときその担当ぶんすべてが時計回りの次の1台に集中し、玉突きで過負荷を招きます。仮想ノードがあれば、落ちた物理ノードの V 個の区間はそれぞれ別の生存ノードへ散るため、負荷の引き継ぎが平準化されます。
応用:分散キャッシュとシャーディング
コンシステントハッシュ法が最初に広く知られたのは分散キャッシュの文脈です。memcached クライアントの ketama などが代表例で、キャッシュサーバーの増減時にヒット率の急落を防ぐのが狙いです。mod N ならサーバー1台の追加で全キャッシュが無効化されかねませんが、リング方式なら無効化されるのは約 1/N で済みます。
データを永続化するシャーディングでも同じ原理が使われます。Amazon Dynamo に始まる一連の分散データストア(Cassandra, ScyllaDB, Riak など)はリングを基盤にデータを分散します。さらにこれらはレプリケーションと組み合わせ、あるキーの担当ノードから時計回りに次の N 台へ複製を置く(preference list)ことで冗長性を確保します。原理の比較はレプリケーションとシャーディングやパーティショニングも参照してください。
「mod N の弱点は何か」「コンシステントハッシュ法で N 台増減したとき移動するキーの割合は」「仮想ノードは何のために導入するか」は分散システム系の定番問題です。答えは順に「N 変化で大半のキーが移動」「平均 1/N 程度」「負荷の偏り平準化と重み付け、離脱時の負荷分散」。範囲検索が必要ならハッシュ分散よりパーティショニングのレンジ方式が向く、という対比も問われます。
限界と注意点
コンシステントハッシュ法は万能ではありません。第一に、ハッシュで散らすため範囲検索に弱い点です。連続したキー(時系列など)の範囲取得は全ノードに散ってしまい、レンジパーティショニングのような区間スキャンができません。第二に、ホットキーには無力です。特定の1キーへアクセスが集中する場合、担当する1ノードが過負荷になるのはリング方式でも変わりません(キー自体を分割する別の工夫が要ります)。
仮想ノードを入れても、データサイズやアクセス頻度がキーごとに偏れば負荷は偏ります。近年の大規模システムでは、リングの単純な時計回り割り当てを改良した bounded-load(各ノードに上限を設け超過ぶんを次へ送る)や、ジャンプ法(jump consistent hash)など、用途に応じた派生手法が使われます。リングは出発点であり、実運用では負荷監視とリバランスの仕組みが不可欠です。
まとめ
データベース Article
コンシステントハッシュ法を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
分散システム
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
ハッシュ空間を環(リング)とみなし、キーは時計回りに最初に出会うノードへ割り当てる。ノードの増減は隣接区間だけに影響する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「分散システム / シャーディング」に近いか確認する。
- 強みである「単純な mod N 方式はノード数 N が変わると大半のキーが別ノードへ移動する。コンシステントハッシュ法は移動するキーを平均で全体の 1/N 程度に抑える。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。