لماذا يهم jemalloc؟ إدارة الذاكرة على نطاق واسع
يدير jemalloc تخصيص الذاكرة لبعض أكبر الأنظمة في العالم. تعرّف على كيفية تقليله للتجزئة ولماذا تستثمر Meta فيه أكثر.

في كل مرة يستدعي برنامجك malloc()، يجب أن يقرر أي جزء من الذاكرة الافتراضية سيعيده. هذا القرار بسيط في البرامج الصغيرة، لكنه يتحول إلى مشكلة هندسية ضخمة على نطاق واسع. المخصص السيئ يجزّئ الذاكرة، ويهدر RAM، ويسبب تنافساً على الأقفال بين الخيوط، وقفزات في زمن الاستجابة عندما يضطر نظام التشغيل لاسترداد الصفحات. المخصص الجيد لا يفعل أياً من ذلك. jemalloc مخصص جيد، وقد جددت Meta للتو التزامها به، لأنه على مقياسها يعني الفرق بين مخصص جيد وآخر متوسط مليارات الدولارات في تكاليف العتاد.
معظم المطورين لا يفكرون أبداً في تخصيص الذاكرة. يستدعون new أو malloc ويحصلون على مؤشر. لكن على مقياس Meta، أي مليارات الطلبات يومياً، وملايين الخوادم، وبيتابايتات من RAM، يؤثر سلوك المخصص تأثيراً مباشراً وقابلاً للقياس في كفاءة العتاد، وزمن الاستجابة في الذيل، والتكلفة التشغيلية.
ما الذي يفعله مخصص الذاكرة فعلياً
يقع مخصص الذاكرة بين برنامجك ونظام التشغيل. يوفر نظام التشغيل الذاكرة في كتل كبيرة (صفحات، عادةً 4 كيلوبايت أو 2 ميغابايت). أما برنامجك فيحتاج قطعاً صغيرة متغيرة الحجم، مثل سلسلة نصية بحجم 24 بايت هنا ومخزن مؤقت بحجم 4096 بايت هناك. مهمة المخصص هي تقطيع صفحات نظام التشغيل إلى القطع التي يحتاجها البرنامج، وإعادة تدوير القطع المحررة للتخصيصات القادمة.
النهج الساذج، أي طلب صفحة جديدة من نظام التشغيل مع كل تخصيص وإعادتها عند التحرير، بطيء بشكل كارثي. استدعاءات النظام لها تكلفتها، والصفحات أكبر بكثير من معظم التخصيصات. ستستهلك 4 كيلوبايت من الذاكرة لسلسلة نصية بحجم 24 بايت.
المخصصات الحقيقية تحتفظ بمجمّع من الذاكرة وتوزع منه تخصيصات فرعية. التحديات هنا: تقليل التجزئة (الفجوات بين التخصيصات الصغيرة جداً للاستفادة منها)، وتقليل التنافس على الأقفال (عدة خيوط تخصص في الوقت نفسه)، وتقليل التكلفة الإضافية (البيانات الوصفية لكل تخصيص يجب أن تكون صغيرة مقارنة بالتخصيص نفسه).
كيف يعمل jemalloc
طُوّر jemalloc، من تطوير Jason Evans ومن هنا جاء حرف «je»، في الأصل لنظام FreeBSD، ثم اعتمدته Meta (وكانت حينها Facebook) كمخصص افتراضي لخدماتها المكتوبة بلغتي C وC++. يعالج تصميمه التحديات الثلاثة الرئيسية بتقنيات محددة.
ذاكرات التخزين المؤقت للخيوط تقضي على التنافس. لكل خيط ذاكرة تخزين مؤقت خاصة به للتخصيصات الصغيرة. عندما يستدعي خيط malloc() لكائن صغير، يُلبّى التخصيص بالكامل من ذاكرته المحلية، بلا أقفال ولا عمليات ذرية ولا تنافس. لا يذهب الخيط إلى الساحة المشتركة لتعبئة ذاكرته إلا عندما تنفد.
jemalloc allocation flow:
Thread calls malloc(64)
↓
Check thread cache for 64-byte size class
→ Hit: return cached chunk (no lock, no contention)
→ Miss: refill from arena
↓
Arena (shared, but per-thread affinity)
→ Find a partially-full slab for 64-byte objects
→ Carve out a chunk
→ Return to thread cache
↓
Return chunk to caller
Thread caches handle 95%+ of allocations without any locking.
فئات الحجم تقلل التجزئة. بدلاً من تخصيص عدد البايتات المطلوب بالضبط، يقرّب jemalloc الحجم إلى أقرب فئة حجم. فئات الحجم مختارة بعناية: 8، 16، 32، 48، 64، 80، 96، 112، 128، 160، 192، 224، 256، وهكذا، بتباعد يزداد كلما كبرت الأحجام. يعني هذا أن تخصيص 50 بايت يحصل على قطعة 64 بايت (هدر بنسبة 23%)، وهذا يبدو سيئاً لكنه في الحقيقة جيد، لأن كل قطع الـ64 بايت قابلة للتبادل، فلا توجد تجزئة خارجية داخل الفئة الواحدة.
الشرائح تنظم التخصيصات بنفس الحجم. كل شريحة (slab) منطقة متصلة من الذاكرة مقسمة إلى قطع من فئة الحجم نفسها. شريحة الكائنات بحجم 64 بايت تحوي قطعاً بحجم 64 بايت فقط. هذا يلغي أسوأ أنواع التجزئة، وهو أن تبقى الذاكرة المحررة غير قابلة لإعادة الاستخدام لأنها محصورة بين تخصيصات نشطة بأحجام مختلفة.
مشكلة التجزئة
تجزئة الذاكرة هي القاتل الصامت للخدمات طويلة التشغيل. الخادم الذي بدأ للتو يستخدم الذاكرة بكفاءة، فالتخصيصات متراصة بإحكام. بعد أيام أو أسابيع من التخصيص والتحرير المختلط، يتحول الكومة إلى جبن سويسري: فجوات حرة صغيرة كثيرة بين التخصيصات النشطة. قد يبلغ إجمالي الذاكرة الحرة 2 غيغابايت، لكن أكبر منطقة حرة متصلة لا تتجاوز 64 كيلوبايت.
التجزئة الخارجية (فجوات لا يمكن استخدامها بين التخصيصات) والتجزئة الداخلية (الهدر داخل التخصيصات بسبب التقريب) مهمتان، لكن الخارجية أخطر. التجزئة الداخلية محدودة بتباعد فئات الحجم، ففي أسوأ الأحوال تهدر نحو 25% لكل تخصيص. أما الخارجية فلا حد لها وتتراكم مع الوقت.
نهج jemalloc القائم على الشرائح يلغي إلى حد كبير التجزئة الخارجية للتخصيصات الصغيرة، وهي الغالبية العظمى. بما أن جميع الكائنات في الشريحة لها الحجم نفسه، فإن تحرير أحدها يخلق فجوة بالحجم المناسب تماماً للتخصيص التالي من فئة الحجم ذاتها. لا توجد فجوات غير قابلة للاستخدام.
أما التخصيصات الكبيرة (عادةً أكبر من 14 كيلوبايت)، فيتبع jemalloc استراتيجية مختلفة: تحصل هذه التخصيصات على صفحاتها الخاصة، ويمكن إعادة الصفحات المحررة إلى نظام التشغيل أو إعادة استخدامها لفئات حجم أخرى. هنا قد تظهر التجزئة أيضاً، لكن التخصيصات الكبيرة نادرة نسبياً.
jemalloc مقابل glibc malloc مقابل tcmalloc
المخصصات الثلاثة الرئيسية في النظام البيئي للينكس تتخذ مقايضات مختلفة.
- glibc malloc (ptmalloc2) هو الافتراضي في معظم أنظمة لينكس. يستخدم الساحات (arenas) لقابلية التوسع مع الخيوط، لكن عدد فئات حجمه أقل من jemalloc، مما يؤدي إلى تجزئة أكبر في الخدمات طويلة التشغيل. ميزته الأساسية أنه الافتراضي، ولا يحتاج إلى أي إعداد.
- tcmalloc (Google) هو malloc يعتمد على التخزين المؤقت للخيوط، طُوّر في الأصل لخدمات Google المكتوبة بـC++. لديه تخزين مؤقت محلي ممتاز للخيوط وتكلفة إضافية منخفضة. يناسب أحمال العمل التي تكثر فيها التخصيصات قصيرة العمر، وهو أقل ملاءمة لأحمال العمل ذات الضغط العالي على الذاكرة حيث تهم التجزئة.
- jemalloc مُحسّن لتقليل التجزئة وسلوك يمكن التنبؤ به تحت الحمل المستمر. يستخدم فئات حجم وإدارة شرائح أكثر تطوراً من غيره. المقايضة: تكلفة إضافية أعلى قليلاً لكل تخصيص (بيانات وصفية أكثر)، مقابل كفاءة أفضل في استخدام الذاكرة مع الوقت.
الاختيار الصحيح يعتمد على حمل العمل. للعمليات قصيرة العمر، تتقارب الأداءات الثلاثة. أما للخوادم طويلة التشغيل ذات أنماط التخصيص المختلطة، مثل خوادم الويب وقواعد البيانات وذاكرات التخزين المؤقت، فعادةً ما تتفوق مقاومة jemalloc للتجزئة، إذ يستخدم ذاكرة أقل بنسبة 10 إلى 30% لنفس حمل العمل مقارنة بـglibc malloc، وهذا على المقياس الكبير يُترجم مباشرة إلى توفير في العتاد.
لماذا تهتم Meta بهذا
على مقياس Meta، خفض 10% من استهلاك الذاكرة عبر خدماتها بلغة C++ يوفر ملايين الدولارات في تكاليف العتاد. ليس سنوياً، بل شهرياً. عندما تدير ملايين الخوادم، تعمل كل منها بخدمات تخصص الذاكرة وتحررها مليارات المرات يومياً، فتصبح كفاءة المخصص بنداً في الميزانية.
يركز استثمار Meta المتجدد في jemalloc على عدة مجالات: دعم أفضل للصفحات الضخمة (huge pages) لتقليل أخطاء TLB على الأنظمة كبيرة الذاكرة، وتحسين إعادة الذاكرة إلى نظام التشغيل لتقليل الذاكرة المقيمة عند انخفاض الحمل، وأدوات تحليل أفضل لفهم أين تُستخدم الذاكرة وأين تُهدر.
جانب التحليل مثير للاهتمام بشكل خاص. يتضمن jemalloc أداة تحليل للكومة مدمجة، يمكنك من خلالها أخذ عينات من التخصيصات والحصول على تقرير يوضح أين خُصصت الذاكرة، وكم منها نشط مقابل المحرر، ومدى تجزؤ الكومة. هذا التحليل له تكلفة شبه معدومة في الإنتاج، ما يجعل تشغيله بشكل مستمر على خوادم الإنتاج أمراً عملياً.
استخدام jemalloc في مشاريعك
التحويل إلى jemalloc بسيط عادةً في برامج C/C++. على لينكس، يمكنك الربط معه مباشرة، أو استخدام LD_PRELOAD لحقنه وقت التشغيل، دون أي تغيير في الكود.
# Install jemalloc
sudo apt install libjemalloc-dev # Debian/Ubuntu
brew install jemalloc # macOS
# Use LD_PRELOAD — works with any program, no recompilation
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so ./my_server
# Or link at compile time
gcc -o my_server my_server.c -ljemalloc
# Enable profiling
export MALLOC_CONF="prof:true,prof_prefix:jeprof"
./my_server
# Then analyze: jeprof --svg ./my_server jeprof.*.heap > heap.svg
عدة مشاريع كبيرة تستخدم jemalloc كمخصص افتراضي: Redis، وRust (يستخدمه كمخصص افتراضي على بعض المنصات)، وFirefox (طُوّر jemalloc في الأصل لنظام FreeBSD الذي كان Firefox يستهدفه)، وكثير من محركات الألعاب.
بالنسبة للغات ذات الذاكرة المُدارة (Python وJava وGo)، يتولى وقت تشغيل اللغة التخصيص، ولا ينطبق jemalloc عليها مباشرة. لكن المفاهيم، مثل التخزين المؤقت المحلي للخيوط وفئات الحجم وتخصيص الشرائح، موجودة في كل مُجمِّع ذاكرة (garbage collector) ومخصص حديث. مثلاً، يعتمد مخصص وقت تشغيل Go تصميماً مستوحى من tcmalloc مع ذاكرات تخزين مؤقت لكل معالج (P) وفئات حجم.
البنية التحتية الخفية
تخصيص الذاكرة بنية تحتية غير مرئية عندما تعمل بشكل صحيح، وكارثية عندما لا تعمل. الخادم الذي يتسرب ذاكرته تدريجياً بسبب التجزئة سينتهي بقتله من نظام التشغيل (OOM-kill)، ولن يظهر السبب في أي سجل تطبيق، لأنه يقع أسفل طبقة التطبيق.
بالنسبة لمعظم التطبيقات، المخصص الافتراضي كافٍ. لكن إذا كنت تشغل خوادم طويلة العمر، وتعاني من نمو غير مفسر في الذاكرة، أو تعمل على مقياس تهم فيه كفاءة العتاد، فإن فهم المخصص الذي تستخدمه، وربما الانتقال إلى مخصص أفضل، من أعلى التغييرات أثراً في البنية التحتية التي يمكنك إجراؤها. jemalloc ليس سحراً. إنه هندسة: تصميم دقيق لبنى البيانات، ومقايضات مدروسة، واهتمام لا يلين بالتفاصيل التي تهم على المقياس الكبير.


