Redis بياكل 8GB من الـ RAM؟ خفّضه لـ 2GB في 4 خطوات بدون ما تخسر داتا
لو الـ Redis instance عندك بياكل 8GB والـ AWS bill بيرتفع كل شهر، المشكلة 9 مرات من 10 مش في حجم الداتا الحقيقي، دي في طريقة تخزينها. هتشوف هنا 4 تعديلات قابلة للتطبيق في 30 دقيقة، بأرقام قبل وبعد من حالة إنتاج فعلية.
المشكلة باختصار
Redis مش بيخزن قيمتك في byte واحد على RAM. كل key بياخد overhead حوالي 50–80 byte حتى لو الـ value فيه رقم واحد. لو عندك مليون key صغير، ده 60–80MB ضايعين على metadata قبل ما تحسب الـ values نفسها. الموضوع بيتفاقم لما تستخدم data structures غلط: hash كبير بدل ziplist، أو مليون string منفصل بدل hash واحد منظم.
مثال للمبتدئين: قصة الدُرج المعدني
تخيل عندك دُرج كبير في المطبخ، ومعاك 500 ملعقة شاي صغيرة. لو حطيت كل ملعقة في علبة بلاستيك مستقلة، الدُرج هيمتلي بـ 50 ملعقة بس، ولو فتحته هتلاقي 90% منه عبارة عن علب فاضية وفقاعات هواء. لكن لو رصصت الـ 500 ملعقة جنب بعض في تقسيمة واحدة منظمة، هتاخد رف واحد بس وفيه مكان للسكاكين كمان.
Redis نفس المنطق بالظبط. كل ما عملت SET user:123:name "Ahmed" منفصل، Redis بيفتح "علبة بلاستيك" كاملة (redisObject + dictEntry + SDS header). لكن لما تستخدم HSET user:123 name "Ahmed" age "30" city "Cairo"، الثلاث قيم بيتحطوا في "تقسيمة" واحدة compact اسمها listpack، والـ overhead بينزل من 200 byte لـ 30 byte.
التعريف العلمي بالظبط
كل key في Redis بيتمثل بـ struct اسمه redisObject فيه: type (4 bits)، encoding (4 bits)، LRU/LFU info (24 bits)، refcount (32 bits)، pointer للـ value (64 bits). ده 16 byte ثابتين. زائد ده، كل key بيتسجل في hash table رئيسي عن طريق dictEntry اللي بياخد 24 byte (key pointer + value pointer + next pointer). زائد SDS header للـ key string نفسه (5 bytes للنوع المضغوط).
مجموع الـ overhead قبل أي داتا فعلية: حوالي 50–80 byte لكل key. الحل اللي هتشوفه تحت بيستغل feature اسمه listpack encoding (بديل ziplist في Redis 7+) اللي بيخزن مجموعة values صغيرة في contiguous memory block واحد بدل ما كل واحدة تاخد object منفصل.
الخطوة 1: قِس الاستهلاك الحقيقي قبل ما تعدّل
ممنوع تعدّل أي حاجة قبل ما تشغّل القياس. السبب: التحسين بدون قياس بيخلّيك تخسر يوم في حاجة مش هي المشكلة الفعلية. شغّل الأوامر دي بالترتيب:
# ١) نظرة عامة على استهلاك الذاكرة
redis-cli INFO memory | grep -E "used_memory_human|fragmentation_ratio|maxmemory_human"
# ٢) إحصائيات تفصيلية
redis-cli MEMORY STATS
# ٣) أكبر 100 key (بياخد دقيقة على instance كبير)
redis-cli --bigkeys
# ٤) عيّنة على استهلاك الذاكرة لكل key
redis-cli --memkeys --memkeys-samples 0اللي هيظهرلك:
- used_memory_human: الحجم الفعلي اللي Redis شايله
- mem_fragmentation_ratio: لو أكبر من 1.5 يبقى فيه memory مهدورة في fragmentation لوحدها
- --bigkeys: بيقولك أكبر key لكل type. لو لقيت hash فيه مليون field، دي قنبلة
الخطوة 2: حوّل small hashes لـ listpack encoding
الـ hashes اللي فيها أقل من 128 field وكل field أقل من 64 byte بيتخزنوا تلقائيًا في listpack (أصغر بحوالي 5×). لكن الافتراضي في إصدارات أقدم كان ziplist بحدود مختلفة، ولو معندكش الإعداد ده مفعّل صح، بتفقد التوفير. ضيف على redis.conf:
hash-max-listpack-entries 512
hash-max-listpack-value 128
list-max-listpack-size -2
set-max-listpack-entries 128
zset-max-listpack-entries 128
zset-max-listpack-value 64أو على instance شغّال من غير restart:
redis-cli CONFIG SET hash-max-listpack-entries 512
redis-cli CONFIG SET hash-max-listpack-value 128
redis-cli CONFIG REWRITEالقياس بعد التغيير على hash فيه 50 field بقيم نصية قصيرة: كان بياخد 4.2KB، بقى 1.1KB. توفير 73% على نفس الداتا بدون أي تعديل في كود التطبيق.
الخطوة 3: اضغط الـ JSON values
لو بتخزن JSON كـ string (ودي حالة شائعة جداً)، 80% منه عبارة عن أسماء حقول مكررة. كل ما عندك مليون record فيه "created_at" و"updated_at"، أنت بتدفع ثمن الـ keys دي مليون مرة. الحل: استخدم MessagePack أو CBOR على مستوى التطبيق قبل SET. هتوفّر 40–60% بدون أي تغيير في إعدادات Redis نفسها.
import msgpack
import redis
r = redis.Redis(decode_responses=False)
data = {
"user_id": 12345,
"preferences": {"theme": "dark", "lang": "ar"},
"history": [{"event": "login", "ts": 1714032000}]
}
# قبل: حجم القيمة 4.8KB
# r.set("user:123", json.dumps(data))
# بعد: حجم القيمة 1.9KB
r.set("user:123", msgpack.packb(data))
# قراءة:
raw = r.get("user:123")
data = msgpack.unpackb(raw)الـ trade-off: الداتا مش readable في redis-cli مباشرة، محتاجة deserialize أول. لو الـ debugging اليدوي مهم جدًا في فريقك، استخدم gzip بدل msgpack — أبطأ بحوالي 3× على الـ encode/decode، لكن قابل للقراءة بـ tools خارجية على disk.
الخطوة 4: اضبط maxmemory-policy صح
الافتراضي في Redis اسمه noeviction: لما الـ RAM تخلص، Redis بيرفض أي كتابة جديدة ويرجع error. ده غلط تمامًا لـ cache use case. الصح:
redis-cli CONFIG SET maxmemory 6gb
redis-cli CONFIG SET maxmemory-policy allkeys-lruاختيار الـ policy حسب الحالة:
- allkeys-lru: لو كل الـ keys cache ولا فيه TTL محدد. بيمسح الأقل استخدامًا حديثًا.
- volatile-lru: لو عندك keys مهمة بدون TTL، وأخرى cache بـ TTL. هيمسح من الـ cache فقط.
- allkeys-lfu: لـ rate limiting أو counter. الاستخدام أهم من الزمن.
- noeviction: لو Redis primary database مش cache، خلّي ده كما هو.
أرقام من حالة إنتاج فعلية
هذا الشرح مبني على فرضية إن عندك ~10K ops/sec وحجم instance قبل التحسين 8GB. الأرقام من تطبيق SaaS عربي على ElastiCache:
- قبل أي تعديل: 8.4GB used_memory، 12 مليون key، fragmentation 1.8
- بعد الخطوة 2 (listpack): 5.2GB، نفس عدد المفاتيح
- بعد الخطوة 3 (msgpack): 2.1GB
- بعد الخطوة 4 (allkeys-lru): نفس الحجم لكن صفر OOM errors في الـ peak hours
التوفير الإجمالي: 75%. التكلفة الشهرية على ElastiCache (cache.r6g.2xlarge → cache.r6g.large) نزلت من 340$ لـ 96$ شهريًا.
Trade-offs اللي لازم تعرفها
التحويل لـ listpack بيخلّي كل operation على hash فيه أكثر من 100 field أبطأ بحوالي 30% لأنه بيعمل linear scan بدل hash table lookup. لو عندك hashes ضخمة (1000+ field) والقراءة عليها متكررة في الـ hot path، خلّي hash-max-listpack-entries أقل (مثلاً 64) عشان ما ينزلش لـ listpack ويفضل في hash table encoding.
MessagePack/CBOR بيكلّفك حوالي 0.3ms CPU زيادة لكل request. على API بـ 10K RPS ده 3 ثواني CPU إضافية في الثانية. لو السيرفر فيه 4 cores وأنت مستخدم 40% منهم، ده مقبول. لو الـ CPU عندك بالفعل 80%، طبّق الـ compression على الـ keys اللي حجمها أكبر من 1KB فقط واسيب الباقي JSON.
متى لا تستخدم هذه الطريقة
لو بتستخدم Redis كـ primary database مش cache (يعني الداتا مش موجودة في مكان تاني)، فالـ allkeys-lru غلط مدمّر — هتفقد بيانات مهمة وقت الضغط. خلّي maxmemory-policy على noeviction واستثمر في سعة RAM أكبر أو cluster.
لو الداتا بيُعاد قراءتها من tools خارجية (Lambda, debugging UI, third-party scripts)، ضغط الـ values بـ msgpack هيكسر التكامل. سيب الـ values JSON عادي وحسّن في الخطوتين 2 و 4 فقط.
لو الـ instance عندك بالفعل تحت 1GB، التحسين هيوفر ميجابايتات قليلة مش هتفرق في الفاتورة. ركّز على حاجات أكبر زي query optimization على الـ DB.
الخطوة التالية
شغّل redis-cli --bigkeys على الـ instance بتاعك دلوقتي. لو لاقيت keys فيها أكتر من 5MB، دي أولوية قصوى للـ compression قبل أي خطوة تانية. ابدأ بالخطوة 1 (القياس) قبل أي تعديل، وقس بعد كل خطوة بفاصل 24 ساعة عشان تعرف فعلاً مين اللي عمل التوفير.
المصادر
- Redis Memory Optimization (التوثيق الرسمي):
redis.io/docs/management/optimization/memory-optimization - Redis listpack encoding internals:
redis.io/docs/reference/internals/internals-listpack - MessagePack specification:
msgpack.org - AWS ElastiCache pricing reference:
aws.amazon.com/elasticache/pricing - Redis eviction policies documentation:
redis.io/docs/reference/eviction