ما ينفخ الذاكرة في روبي؟

لدينا في Phusion وكيل HTTP متعدد الخيوط بسيط في Ruby (يوزع حزم DEB و RPM). رأيت على ذلك استهلاك ذاكرة 1.3 جيجابايت. ولكن هذا جنون لعملية عديمي الجنسية ...


سؤال: ما هذا؟ الجواب: يستخدم روبي الذاكرة مع مرور الوقت!

اتضح أنني لست وحدي في هذه المشكلة. تطبيقات روبي يمكن أن تستخدم الكثير من الذاكرة. لكن لماذا؟ وفقًا لـ Heroku و Nate Burkopek ، فإن الانتفاخ يرجع بشكل أساسي إلى تجزئة الذاكرة وتوزيع الكومة المفرط.

خلص بيركوبك إلى أن هناك حلين:

  1. إما استخدام مخصص ذاكرة مختلف تمامًا عن glibc - عادة jemalloc ، أو:
  2. MALLOC_ARENA_MAX=2 متغير البيئة السحرية MALLOC_ARENA_MAX=2 .

أنا قلق بشأن وصف المشكلة والحلول المقترحة. يوجد خطأ هنا ... لست متأكدًا من أن المشكلة موصوفة تمامًا بشكل صحيح أو أن هذه هي الحلول الوحيدة المتاحة. كما أنه يزعجني أن يشير الكثيرون إلى jemalloc على أنها بركة فضية سحرية.

السحر هو مجرد علم لا نفهمه بعد . لذلك ذهبت في رحلة بحثية لمعرفة الحقيقة كاملة. ستغطي هذه المقالة الموضوعات التالية:

  1. كيف يعمل تخصيص الذاكرة.
  2. ما هو هذا "التفتت" و "التوزيع المفرط" للذاكرة الذي يتحدث عنه الجميع؟
  3. ما الذي يسبب استهلاك ذاكرة كبيرة؟ هل يتماشى الموقف مع ما يقوله الناس ، أم أن هناك شيئًا آخر؟ (المفسد: نعم ، هناك شيء آخر).
  4. هل هناك أي حلول بديلة؟ (المفسد: لقد وجدت واحدة).

ملاحظة: هذه المقالة ذات صلة فقط بنظام Linux ، وفقط لتطبيقات Ruby متعددة الخيوط.

محتوى



تخصيص ذاكرة روبي: مقدمة


يخصص روبي الذاكرة على ثلاثة مستويات ، من الأعلى إلى الأسفل:

  1. مترجم روبي الذي يدير كائنات روبي.
  2. مكتبة مخصص الذاكرة لنظام التشغيل.
  3. جوهر.

دعنا نذهب من خلال كل مستوى.

ياقوت


من جانبها ، ينظم روبي كائنات في مناطق من الذاكرة تسمى صفحات كومة روبي . تنقسم صفحة الكومة هذه إلى فتحات من نفس الحجم ، حيث يشغل كائن واحد فتحة واحدة. سواء كانت سلسلة أو جدول تجزئة أو صفيف أو فئة أو أي شيء آخر ، فإنها تحتل مكانًا واحدًا.



قد تكون الفتحات الموجودة على صفحة الكومة مشغولة أو مجانية. عندما يختار روبي كائنًا جديدًا ، فإنه يحاول على الفور شغل فتحة مجانية. إذا لم تكن هناك فتحات مجانية ، فسيتم تمييز صفحة كومة جديدة.

الفتحة صغيرة ، حوالي 40 بايت. من الواضح أن بعض الكائنات لن تناسبها ، على سبيل المثال ، خطوط 1 ميغابايت. ثم يخزن روبي المعلومات في مكان آخر خارج صفحة الكومة ، ويضع مؤشرًا على منطقة الذاكرة الخارجية هذه في الفتحة.


يتم تخزين البيانات التي لا تناسب الفتحة خارج صفحة الكومة. يضع روبي مؤشرًا لهذه البيانات الخارجية في الفتحة

يتم تخصيص صفحات كومة الذاكرة المؤقتة Ruby وأي مناطق ذاكرة خارجية باستخدام أداة تخصيص ذاكرة النظام.

مخصص ذاكرة النظام


يعد مخصص ذاكرة نظام التشغيل جزءًا من glibc (وقت تشغيل C). يتم استخدامه من قبل جميع التطبيقات تقريبا ، وليس فقط روبي. لديها واجهة برمجة تطبيقات بسيطة:

  • يتم تخصيص الذاكرة عن طريق استدعاء malloc(size) . تعطيها عدد البايتات التي تريد تخصيصها ، وتقوم بإرجاع عنوان التخصيص أو خطأ.
  • يتم تحرير الذاكرة المخصصة عن طريق الاتصال free(address) .

على عكس روبي ، حيث يتم تخصيص فتحات من نفس الحجم ، يتعامل مخصص الذاكرة مع طلبات تخصيص ذاكرة من أي حجم. كما ستتعلم لاحقًا ، هذه الحقيقة تؤدي إلى بعض المضاعفات.

بدوره ، يصل مخصص الذاكرة إلى واجهة برمجة تطبيقات kernel. يستغرق قطع الذاكرة أكبر بكثير من kernel مما يطلبه المشتركون ، نظرًا لأن مكالمة kernel باهظة الثمن وله واجهة برمجة تطبيقات kernel قيود: يمكن تخصيص ذاكرة فقط بمضاعفات 4 كيلوبايت.


يخصص "مخصص الذاكرة" مجموعات كبيرة - يطلق عليها "أكوام النظام" ويشارك محتوياتها لتلبية الطلبات من التطبيقات

تسمى مساحة الذاكرة التي يخصصها مخصص الذاكرة من kernel الكومة. لاحظ أنه لا علاقة له بصفحات كومة Ruby ، ​​لذلك من أجل الوضوح ، سوف نستخدم مصطلح كومة النظام .

ثم يخصص أداة تخصيص الذاكرة أجزاء من أكوام النظام للمتصلين حتى تتوفر مساحة خالية. في هذه الحالة ، يخصص مخصص الذاكرة كومة نظام جديدة من kernel. يشبه هذا كيفية تحديد Ruby الكائنات من صفحات كومة الذاكرة المؤقتة Ruby.


يخصص روبي الذاكرة من مخصص الذاكرة ، والذي بدوره يخصص الذاكرة من النواة

جوهر


يمكن للنواة تخصيص الذاكرة فقط بوحدات 4 كيلوبايت. واحد مثل كتلة 4K يسمى صفحة. لتجنب الخلط بين صفحات روبي كومة ، من أجل الوضوح سوف نستخدم مصطلح صفحة النظام (صفحة OS).

السبب يصعب تفسيره ، لكن هذه هي الطريقة التي تعمل بها جميع الألباب الحديثة.

يكون لتخصيص الذاكرة من خلال kernel تأثير كبير على الأداء ، وهذا هو السبب في محاولة موزعي الذاكرة تقليل عدد مكالمات kernel.

تعريف استخدام الذاكرة


وبالتالي ، يتم تخصيص الذاكرة على عدة مستويات ، ويقوم كل مستوى بتخصيص ذاكرة أكثر مما يحتاج إليه. يمكن أن تحتوي صفحات روبي كومة فتحات الحرة ، وكذلك أكوام النظام. لذلك ، فإن الإجابة على السؤال "مقدار الذاكرة المستخدمة؟" يعتمد كليا على أي مستوى تسأل!

أدوات مثل top أو ps تظهر استخدام الذاكرة من منظور النواة . هذا يعني أن المستويات العليا يجب أن تعمل بشكل موحد لتحرير الذاكرة من وجهة نظر kernel. كما ستتعلم لاحقًا ، هذا أصعب مما يبدو.

ما هو التفتت؟


تجزئة الذاكرة تعني أن عمليات تخصيص الذاكرة مبعثرة بشكل عشوائي. هذا يمكن أن يسبب مشاكل مثيرة للاهتمام.

تجزئة مستوى روبي


النظر في جمع القمامة روبي. يعني جمع البيانات المهملة لكائن وضع علامة على فتحة صفحة كومة الذاكرة المؤقتة Ruby على أنها مجانية ، مما يسمح بإعادة استخدامها. إذا كانت الصفحة بأكملها من كومة الذاكرة المؤقتة Ruby تتكون فقط من فتحات حرة ، يمكن تحرير صفحتها بأكملها مرة أخرى إلى مخصص الذاكرة (وربما إلى kernel).



ولكن ماذا يحدث إذا لم تكن جميع فتحات مجانية؟ ماذا لو كان لدينا العديد من صفحات روبي كومة وكان جامع القمامة يحرر الكائنات في أماكن مختلفة ، بحيث في النهاية هناك العديد من الفتحات المجانية ، ولكن في صفحات مختلفة؟ في هذه الحالة ، يحتوي Ruby على فتحات مجانية لوضع الكائنات ، ولكن سيستمر مخصص الذاكرة و kernel في تخصيص الذاكرة!

تجزئة الذاكرة المخصصة


مخصص الذاكرة لديه مشكلة مماثلة ولكن مختلفة تماما. لا يحتاج إلى مسح أكوام النظام بالكامل على الفور. نظريا ، يمكن تحرير أي صفحة نظام واحد. ولكن نظرًا لأن مخصص الذاكرة يتعامل مع تخصيصات الذاكرة ذات الحجم التعسفي ، فقد يكون هناك عدة تخصيصات على صفحة النظام. لا يمكن تحرير صفحة النظام حتى يتم تحرير جميع الاختيارات.



فكر فيما يحدث إذا كان لدينا تخصيص 3 كيلوبايت ، وكذلك تخصيص 2 كيلوبايت ، مقسومًا إلى صفحتين للنظام. إذا قمت بتحرير أول 3 كيلوبايت ، فستظل صفحات النظام مشغولة جزئيًا ولا يمكن تحريرها.



لذلك ، إذا فشلت الظروف ، سيكون هناك الكثير من المساحة الحرة على صفحات النظام ، لكن لن يتم تحريرها بالكامل.

والأسوأ من ذلك: ماذا لو كان هناك الكثير من الأماكن المجانية ، ولكن لا يوجد واحد منها كبير بما يكفي لتلبية طلب تخصيص جديد؟ سيتعين على مخصص الذاكرة تخصيص كومة نظام جديدة بالكامل.

هل يتسبب تجزئة صفحة كومة Ruby في حدوث انتفاخ في الذاكرة؟


من المحتمل أن التشرذم يسبب الإفراط في استخدام الذاكرة في روبي. إذا كان الأمر كذلك ، أيهما من الشظايا هو أكثر ضررا؟ هذا ...

  1. روبي تجزئة صفحة كومة؟ أو
  2. تجزئة الذاكرة المخصصة؟

الخيار الأول هو بسيط جدا للتحقق. يوفر Ruby APIs اثنين: ObjectSpace.memsize_of_all و GC.stat . بفضل هذه المعلومات ، يمكنك حساب جميع الذاكرة التي تلقاها روبي من المخصص.



إرجاع ObjectSpace.memsize_of_all الذاكرة التي تشغلها جميع كائنات Ruby النشطة. وهذا هو ، كل المساحة في فتحاتها وأي بيانات خارجية. في الرسم البياني أعلاه ، هذا هو حجم جميع الكائنات الأزرق والبرتقالي.

يتيح GC.stat معرفة حجم جميع الفتحات المجانية ، أي المنطقة الرمادية بأكملها في الرسم التوضيحي أعلاه. ها هي الخوارزمية:

 GC.stat[:heap_free_slots] * GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] 

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

كتبت برنامج اختبار بسيط ينشئ مجموعة من الخيوط ، كل منها يختار الخطوط في حلقة. هذه هي النتيجة بعد فترة من الوقت:



انها ... فقط ... مجنون!

توضح النتيجة أن Ruby له تأثير ضعيف على إجمالي حجم الذاكرة المستخدمة ، ولا يهم إذا كانت صفحات كومة الذاكرة المؤقتة Ruby مجزأة أم لا.

يجب أن نبحث عن الجاني في مكان آخر. على الأقل نحن نعرف الآن أن روبي لا يتحمل المسؤولية.

دراسة تجزئة الذاكرة


المشتبه به المحتمل الآخر هو مخصص الذاكرة. في النهاية ، لاحظت Nate Berkopek و Heroku أن التدليل مع مخصص الذاكرة (إما بديل كامل لـ jemalloc أو إعداد متغير البيئة السحرية MALLOC_ARENA_MAX=2 ) يقلل بشكل كبير من استخدام الذاكرة.

لنرى أولاً ما يفعله MALLOC_ARENA_MAX=2 ولماذا يساعد. ثم نفحص التجزئة على مستوى الموزع.

تخصيص الذاكرة المفرطة و glibc


سبب MALLOC_ARENA_MAX=2 هو MALLOC_ARENA_MAX=2 تعدد العمليات. عندما تحاول عدة مؤشرات ترابط في وقت واحد تخصيص ذاكرة من كومة الذاكرة المؤقتة للنظام نفسه ، فإنها تقاتل من أجل الوصول. مؤشر ترابط واحد فقط في كل مرة يمكنه استلام الذاكرة ، مما يقلل من أداء تخصيص الذاكرة متعدد الخيوط.


مؤشر ترابط واحد فقط في كل مرة يمكن أن تعمل مع كومة الذاكرة المؤقتة النظام. في المهام متعددة الخيوط ، ينشأ تعارض ، وبالتالي ينخفض ​​الأداء

في مخصص الذاكرة لمثل هذه الحالة هناك الأمثل. يحاول إنشاء عدة أكوام نظام وتعيينها إلى مؤشرات ترابط مختلفة. في معظم الأوقات ، يعمل مؤشر الترابط فقط مع كومة الذاكرة المؤقتة الخاصة به ، مما يؤدي إلى تجنب التعارض مع مؤشرات الترابط الأخرى.

في الواقع ، فإن الحد الأقصى لعدد أكوام النظام المخصصة بهذه الطريقة يساوي افتراضيًا عدد المعالجات الافتراضية مضروبًا في 8. وهذا هو ، في نظام ثنائي النواة ذي مؤشرات ترابط مفرطة ، ينتج كل منهما أكوام نظام 2 * 2 * 8 = 32 ! هذا ما أسميه التوزيع المفرط .

لماذا المضاعف الافتراضي كبير جدًا؟ لأن المطور الرئيسي لمخصص الذاكرة هو Red Hat. عملائها هم شركات كبيرة مع خوادم قوية وطنا من ذاكرة الوصول العشوائي. يتيح لك التحسين أعلاه زيادة متوسط ​​أداء مؤشرات متعددة بنسبة 10 ٪ بسبب زيادة كبيرة في استخدام الذاكرة. لعملاء ريد هات ، هذا حل وسط جيد. بالنسبة لمعظم البقية - بالكاد.

تزعم نيت في مدونتها ومقالة Heroku أن زيادة عدد أكوام النظام يزيد من التفتت ، ويستشهد بالوثائق الرسمية. يقلل المتغير MALLOC_ARENA_MAX الحد الأقصى لعدد أكوام النظام المخصصة MALLOC_ARENA_MAX . بهذا المنطق ، فإنه يقلل من تجزئة.

تصور أكوام النظام


هل بيان Nate و Heroku صحيح أن زيادة عدد أكوام النظام يزيد من التفتت؟ في الواقع ، هل هناك أي مشكلة مع التجزئة على مستوى مخصص الذاكرة؟ لم أكن أرغب في أخذ أي من هذه الافتراضات أمراً مفروغاً منه ، لذلك بدأت الدراسة.

لسوء الحظ ، لا توجد أدوات لتصور أكوام النظام ، لذلك كتبت متخيلًا بنفسي .

أولاً ، تحتاج إلى الحفاظ بطريقة أو بأخرى على مخطط توزيع أكوام النظام. لقد درست مصدر مخصص الذاكرة ونظرت في كيفية تمثيلها داخليًا للذاكرة. ثم كتب مكتبة تتكرر عبر هياكل البيانات هذه ويكتب المخطط إلى ملف. أخيرًا ، كتب أداة تأخذ مثل هذا الملف كمدخلات وتجميع التصور كصور HTML و PNG ( شفرة المصدر ).



فيما يلي مثال لتصور كومة واحدة لنظام معين (هناك الكثير). الكتل الصغيرة في هذا التصور تمثل صفحات النظام.

  • وتستخدم المناطق الحمراء خلايا الذاكرة.
  • غرايز هي مناطق حرة لا تصدر مرة أخرى إلى جوهرها.
  • يتم تحرير المناطق البيضاء للنواة.

يمكن استخلاص الاستنتاجات التالية من التصور:

  1. هناك بعض التجزئة. البقع الحمراء مبعثرة من الذاكرة ، وبعض صفحات النظام نصف حمراء فقط.
  2. لدهشتي ، تحتوي معظم أكوام النظام على قدر كبير من صفحات النظام المجانية بالكامل (الرمادي)!

ثم بزغت لي:

على الرغم من أن التجزئة لا يزال يمثل مشكلة ، فليس هذا هو الهدف!

بدلاً من ذلك ، المشكلة كبيرة في اللون الرمادي: لا يرسل مخصص الذاكرة هذا الذاكرة إلى النواة !

بعد إعادة دراسة الكود المصدر لمخصص الذاكرة ، اتضح أنه بشكل افتراضي لا يرسل سوى صفحات النظام إلى النواة في نهاية كومة الذاكرة المؤقتة للنظام ، وحتى نادراً ما يفعل ذلك . ربما ، يتم تنفيذ مثل هذه الخوارزمية لأسباب تتعلق بالأداء.

الخدعة السحرية: الختان


لحسن الحظ ، وجدت خدعة واحدة. هناك واجهة برمجة واحدة ستجبر أداة تخصيص الذاكرة على إصدارها للنواة ليس فقط الأخير ، ولكن جميع صفحات النظام ذات الصلة. يطلق عليه malloc_trim .

كنت أعرف هذه الوظيفة ، لكنني لم أعتقد أنها كانت مفيدة ، لأن الدليل يقول ما يلي:

تحاول الدالة malloc_trim () تحرير ذاكرة حرة في أعلى الكومة.

الدليل خاطئ! يقول تحليل الكود المصدري أن البرنامج يحرر جميع صفحات النظام ذات الصلة ، وليس فقط الجزء العلوي.

ماذا يحدث إذا تم استدعاء هذه الوظيفة أثناء جمع القمامة؟ قمت بتعديل شفرة مصدر Ruby 2.6 لاستدعاء malloc_trim() في دالة gc_start من gc.c ، على سبيل المثال:

 gc_prof_timer_start(objspace); { gc_marks(objspace, do_full_mark); // BEGIN MODIFICATION if (do_full_mark) { malloc_trim(0); } // END MODIFICATION } gc_prof_timer_stop(objspace); 

وهنا نتائج الاختبار:



يا له من فرق كبير! خفض تصحيح بسيط من استهلاك الذاكرة إلى MALLOC_ARENA_MAX=2 تقريبًا MALLOC_ARENA_MAX=2 .

إليك كيف يبدو في التصور:



نرى العديد من المساحات البيضاء التي تتوافق مع صفحات النظام التي تم تحريرها مرة أخرى إلى النواة.

استنتاج


اتضح أن التفتت ، في الأساس ، ليس له علاقة به. لا يزال إلغاء التجزئة مفيدًا ، لكن المشكلة الرئيسية هي أن مخصص الذاكرة لا يحب تحرير الذاكرة مرة أخرى إلى kernel.

لحسن الحظ ، تبين أن الحل بسيط للغاية. كان الشيء الرئيسي هو العثور على السبب الجذري.

رمز مصدر متخيل


شفرة المصدر

ماذا عن الأداء؟


بقي الأداء أحد الشواغل الرئيسية. لا يمكن malloc_trim() الاتصال malloc_trim() مجانًا ، ولكن وفقًا للرمز ، تعمل الخوارزمية في الوقت الخطي. التفتت إلى نوح جيبس ، الذي أطلق مؤشر Rails Bench القياسي. لدهشتي ، تسبب التصحيح زيادة طفيفة في الأداء.





فجر عقلي. التأثير غير مفهوم ، لكن الخبر جيد.

بحاجة الى مزيد من الاختبارات.


كجزء من هذه الدراسة ، تم التحقق من عدد محدود فقط من الحالات. ليس من المعروف تأثير ذلك على أعباء العمل الأخرى. إذا كنت تريد المساعدة في الاختبار ، يرجى الاتصال بي .

Source: https://habr.com/ru/post/ar444482/


All Articles