リリース・改善中
Algorithm
コンシステントハッシュ法は、ノードの追加や削除で移動するキーを必要最小限に抑えてキーをサーバーへ割り当てる手法で、分散キャッシュやストアに使われます。
コンシステントハッシュ法(consistent hashing)は、キーを多数のサーバーに分散配置する手法で、1997年に Karger らが分散 Web キャッシュのために提案しました。ハッシュ値の空間を円状のリングとみなし、サーバーとキーを同じリング上に置いて、キーから時計回りに最初に出会うサーバーがそのキーを担当します。サーバーごとにリング上の点を複数(仮想ノード)置くことで負荷を均等にします。
hash(key) % N では、サーバー数が変わるとほぼすべてのキーの配置が変わり、キャッシュのヒット率が崩れ、大量のデータ移動が発生します。コンシステントハッシュ法ではサーバーを1台追加しても平均で全キーの 1/(N+1) だけが、しかも新しいサーバーにだけ移動するため、運用中にノードを増減しやすくなります。Memcached クライアントの ketama、Amazon の Dynamo 論文、Cassandra、Nginx や HAProxy の consistent ハッシュ設定がこの考え方に基づいています。
まず mod-N 方式でどれだけのキーが移動するかを計算し、ソート済み配列と二分探索でハッシュリングを実装してみましょう。次に仮想ノード数によって負荷のばらつきがどう減るかを測定し、レプリカの選び方、ランデブーハッシュ、Jump Consistent Hash と比較すると、システム設計面接でも違いをはっきり説明できるようになります。
ノードの追加や削除で移動するキーは期待値で約 1/(N+1) にとどまり、既存ノード同士でキーをやり取りすることはありません。
ハッシュ空間を円とみなし、キーから時計回りに最初のノードを担当者とします。検索はソート済み位置に対する二分探索です。
ノードごとにリング上に複数の点を置いて負荷の偏りを減らし、点の数でサーバー容量の違い(重み)を表現します。
時計回りに異なるノードを R 台選んでレプリカを置きます。ランデブーハッシュや Jump Hash も同じ目的を別の方法で実現します。
MD5 ダイジェストの先頭 8 バイトをリング上の位置として使い、ノードごとに 100 個の仮想ノードを bisect.insort でソート済みリストに挿入します。get は bisect_left でキーの位置以上の最初の点を探し、末尾を越えたら先頭に戻ります。ノード D を追加しても移動するキーは約 4 分の 1 だけで、再び削除するとすべてのキーが元のノードに戻ることを確認できます。
consistent_hashing.py
import bisect
import hashlib
def h(key: str) -> int:
return int.from_bytes(hashlib.md5(key.encode()).digest()[:8], "big")
class HashRing:
def __init__(self, nodes=(), vnodes=100):
self.vnodes = vnodes
self.points = [] # sorted hash positions on the ring
self.owner = {} # position -> physical node
for node in nodes:
self.add(node)
def add(self, node):
for i in range(self.vnodes):
p = h(f"{node}#{i}")
bisect.insort(self.points, p)
self.owner[p] = node
def remove(self, node):
for i in range(self.vnodes):
p = h(f"{node}#{i}")
self.points.pop(bisect.bisect_left(self.points, p))
del self.owner[p]
def get(self, key):
if not self.points:
raise LookupError("ring is empty")
i = bisect.bisect_left(self.points, h(key)) % len(self.points)
return self.owner[self.points[i]]
ring = HashRing(["A", "B", "C"])
keys = [f"user:{n}" for n in range(10_000)]
before = {k: ring.get(k) for k in keys}
ring.add("D")
moved = sum(before[k] != ring.get(k) for k in keys)
print(f"moved {moved / len(keys):.1%} of keys") # about 25%
ring.remove("D")
print(all(ring.get(k) == before[k] for k in keys)) # True
python consistent_hashing.pyインストールから コンシステントハッシュ法 の中心となる考え方まで、6 章で順を追って学びます。
コンシステントハッシュ法 について質問し、経験を共有し、意見を交わす場所です。
まだディスカッションはありません。最初の話題を始めましょう。
コメント 0件
ログイン · ログインするとコメントできます。
最初のコメントを書いてみましょう。