Consistent Hashing بالعربي: وزّع مفاتيحك على 100 سيرفر بدون reshuffle
لو بتستخدم hash(key) % N لتوزيع المفاتيح على servers، لما N يتغيّر حتى بواحد، بتنقل ~99% من البيانات. Consistent hashing بيحرّك 1/N بس. ده الفرق بين cache بيفضى كل ما سيرفر يقع، وبين cluster ثابت خلال الـ failover.
المشكلة باختصار
عندك 4 servers للـ cache. بتحسب hash(user_id) % 4 عشان تقرّر أي سيرفر يحتفظ بالـ session. واحد وقع. N بقت 3. hash(user_id) % 3 بيرجّع قيم مختلفة لـ 75% من المفاتيح. النتيجة: الـ cache miss rate بيطلع فجأة لـ 75%، وقاعدة البيانات بتشرب الضربة كلها في نفس اللحظة، وبتدخل في cascading failure.
في حالة حقيقية، Discord وقف عن modulo hashing ونقل Cassandra cluster بتاعهم لـ consistent hashing. وقت الـ failover نزل من دقائق لثواني، والـ p99 latency على reads ثبت بدل ما يطلع فوق 800ms. المصدر في آخر المقال.
الفكرة بمثال بسيط: رفوف المكتبة
تخيّل مكتبة فيها 4 رفوف. كل كتاب ليه رقم. الطريقة الشائعة: رقم_الكتاب % 4. الكتاب رقم 17 يروح للرف الأول (17 % 4 = 1). لو رف اتشال، N بقت 3. نفس الكتاب دلوقتي: 17 % 3 = 2. راح لرف تاني خالص. ولو حسبت على كل الكتب، هتلاقي تقريبًا ثلاثة أرباع المكتبة اتنقلت. يعني فريق نقل في يوم واحد.
Consistent hashing بيقول: خلي الرفوف على دايرة ساعة. الرف الأول عند الساعة 3، التاني عند 6، التالت 9، الرابع 12. كل كتاب كمان بياخد مكان على الدايرة حسب رقمه. قاعدة وحيدة: الكتاب بيروح لأقرب رف في اتجاه عقارب الساعة. لو رف الساعة 6 اتشال، الكتب اللي كانت بين الساعة 3 و 6 هي بس اللي تتنقل لرف الساعة 9. باقي المكتبة ما اتحركتش. النقل: ربع المكتبة تقريبًا، مش تلاتة أرباع.
التفسير العلمي الدقيق
الـ hash function (زي MurmurHash3 أو MD5 أو SHA-1) بترجّع رقم في فضاء كبير، عادة 32-bit أو 64-bit. بنتعامل مع الفضاء ده كحلقة: بعد القيمة العظمى بيرجع للصفر. كل سيرفر بياخد نقطة واحدة على الحلقة بـ hash(server_id). كل مفتاح بياخد نقطته بـ hash(key). المفتاح بيتخزن في أول سيرفر تلاقيه تمشي في اتجاه عقارب الساعة بعد نقطة المفتاح.
لما سيرفر يتشال، المفاتيح اللي بين نقطة السيرفر ده ونقطة السيرفر اللي قبله مباشرةً هي اللي تنتقل للسيرفر اللي بعده. عدديًا: K/N مفتاح بيتحرك من أصل K. لما تضيف سيرفر جديد، نفس الحساب: K/N بس.
مشكلة التوزيع غير المتوازن
لو السيرفرات وقعت على نقاط متقاربة، السيرفر اللي بعدهم هيكون مسؤول عن قوس كبير من الحلقة، فيأخد حمل أكبر بكتير من الباقي. الحل الصناعي: virtual nodes. كل سيرفر فيزيائي بياخد 100 إلى 200 موقع على الحلقة بدل موقع واحد، عن طريق hashing لـ server_id#0, server_id#1… إلخ. التوزيع بيبقى أنعم بكتير، ونسبة عدم التوازن بين السيرفرات بتنزل من ~35% عند vnode واحد لأقل من 5% عند 200 vnode. ده القياس الموثّق من ورقة Dynamo.
كود Python شغّال من الصفر
التطبيق ده بيوضّح الفكرة في أقل من 40 سطر. يعتمد على bisect للبحث بـ O(log V) على الحلقة المرتّبة:
import hashlib
from bisect import bisect
class ConsistentHash:
def __init__(self, servers, vnodes=150):
self.ring = {}
self.sorted_hashes = []
self.vnodes = vnodes
for s in servers:
self.add_server(s)
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_server(self, server):
for i in range(self.vnodes):
h = self._hash(f"{server}#{i}")
self.ring[h] = server
self.sorted_hashes.append(h)
self.sorted_hashes.sort()
def remove_server(self, server):
for i in range(self.vnodes):
h = self._hash(f"{server}#{i}")
self.ring.pop(h, None)
self.sorted_hashes.remove(h)
def get_server(self, key):
if not self.ring:
return None
h = self._hash(key)
idx = bisect(self.sorted_hashes, h) % len(self.sorted_hashes)
return self.ring[self.sorted_hashes[idx]]
ch = ConsistentHash(["s1", "s2", "s3", "s4"])
before = {f"user_{i}": ch.get_server(f"user_{i}") for i in range(10000)}
ch.remove_server("s2")
after = {f"user_{i}": ch.get_server(f"user_{i}") for i in range(10000)}
moved = sum(1 for k in before if before[k] != after[k])
print(f"المفاتيح المنقولة: {moved} من 10000 = {moved/100:.1f}%")
# ناتج فعلي: ~2500 من 10000 = ~25%
لو تستبدله بـ hash(key) % 4 ثم تغيّر لـ % 3، المفاتيح المنقولة بتطلع ~75% بدل ~25%. يعني تلات أضعاف ضغط إعادة التسخين على الـ DB في لحظة واحدة.
Trade-offs لازم تعرفها
- استهلاك ذاكرة أعلى: 150 vnode × 100 سيرفر = 15,000 نقطة. كل نقطة ~16 بايت، تقريبًا 240KB. مقبول، لكنه مش صفر.
- زمن lookup لوغاريتمي: O(log V) في حالة الحلقة المرتّبة. لو كاتبه يدوي بـ linear scan هتاخد O(V) — في حلقة 15K نقطة ده فرق حقيقي.
- توزيع مش مثالي: حتى مع 200 vnode، فيه فرق حمل ~5% بين السيرفرات. لو محتاج توزيع مثالي، استخدم rendezvous hashing (HRW) أو jump consistent hash.
- Hot keys مش بتتحل: لو مفتاح واحد عليه 40% من الـ traffic، consistent hashing هيحطه على سيرفر واحد يقع. الحل: consistent hashing with bounded loads أو key splitting يدوي.
الفرضية: الشرح ده مبني على حالات cache/sharding فيها المفاتيح موزّعة عشوائيًا بشكل معقول، وعدد السيرفرات في مدى 10–1000. خارج المدى ده، في اعتبارات تانية (مثل rack awareness و cross-AZ replication).
متى لا تستخدم Consistent Hashing
- السيرفرات ثابتة ومفيش scaling:
hash % Nأبسط وأسرع وأقل ذاكرة. استخدم consistent hashing بس لو elastic scaling أو failover متكرر جزء من متطلباتك. - محتاج replication بعدد نسخ ثابت عالي: تقدر تعمله بـ Consistent hashing (تمشي N مرة حوالين الحلقة)، لكن Cassandra-style token ring بيطلع أنضف في الغالب.
- الحمل hot keys مش عدد مفاتيح كبير: consistent hashing بيحل مشكلة إعادة التوزيع مش hot spots. استخدم caching على مستوى التطبيق أو partition splitting.
الخطوة التالية
افتح الـ client بتاع Redis أو Memcached في مشروعك. دوّر على سطر شبيه بـ nodes[hash(key) % len(nodes)]. لو لقيته، استبدله بمكتبة consistent hashing فعلية: uhashring أو redis-py-cluster في Python، hashring في Node.js. ثم شغّل سكربت التجربة فوق وقيس فعليًا نسبة المفاتيح المنقولة قبل وبعد إزالة node. لو الأرقام طلعت متطابقة مع النظرية (~1/N)، ده دليل إن الـ config صح. لو طلعت أعلى، غالبًا عندك فرقة vnode منخفضة أو hash function ضعيفة.
المصادر
- Karger et al., "Consistent Hashing and Random Trees", STOC 1997 — الورقة الأصلية للمفهوم: dl.acm.org/doi/10.1145/258533.258660
- DeCandia et al., "Dynamo: Amazon's Highly Available Key-value Store", SOSP 2007 — أول تطبيق صناعي واسع: allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf
- Discord Engineering, "How Discord Stores Trillions of Messages": discord.com/blog/how-discord-stores-trillions-of-messages
- Lamping & Veach, "A Fast, Minimal Memory, Consistent Hash Algorithm" (Jump Hash), Google 2014: arxiv.org/abs/1406.2294
- Mirrokni et al., "Consistent Hashing with Bounded Loads", Google Research 2016: research.google/pubs/consistent-hashing-with-bounded-loads
- Redis Cluster Specification — key hashing و hash slots: redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec