مرحبا الزملاء.
لم يسفر بحثنا الطويل عن الكتب الأكثر مبيعًا عن تحسين الشفرة عن تحقيق النتائج الأولى إلا ، لكننا على استعداد لإرضاءكم بأن الترجمة من كتاب Ben Watson الأسطوري "
كتابة رمز .NET عالي الأداء " قد اكتملت للتو. في المتاجر - مبدئيا في أبريل ، لمشاهدة الإعلان.
واليوم نقدم لك قراءة مقالة عملية بحتة حول أكثر أنواع الضغط
تسربًا للذاكرة ، والتي كتبها
نيلسون إيليدزه (سترايك).
لذلك ، لديك برنامج يستغرق وقتًا أطول لإكماله ، وكلما طال الوقت. ربما ، لن يكون من الصعب عليك أن تفهم أن هذه علامة مؤكدة على حدوث تسرب للذاكرة.
ومع ذلك ، ماذا نعني بالضبط بـ "تسرب الذاكرة"؟ في تجربتي ، يتم تقسيم تسرب الذاكرة الصريح إلى ثلاث فئات رئيسية ، تتميز كل منها بسلوك خاص ، ولتصحيح كل فئة من الفئات ، هناك حاجة إلى أدوات وتقنيات خاصة. في هذه المقالة ، أريد وصف الفئات الثلاثة واقتراح كيفية التعرف عليها بشكل صحيح
ما الفئة التي تتعامل معها وكيفية العثور على تسرب.
النوع (1): جزء الذاكرة غير قابل للوصول المخصص
هذا تسرب ذاكرة كلاسيكي في C / C ++. خصص شخص ما الذاكرة باستخدام
new
أو
malloc
، ولم يتصل
free
أو
delete
لتحرير الذاكرة بعد الانتهاء من العمل معها.
void leak_memory() { char *leaked = malloc(4096); use_a_buffer(leaked); }
كيفية تحديد ما إذا كان تسرب ينتمي إلى هذه الفئة- إذا كتبت بـ C أو C ++ ، خاصة في C ++ دون الاستخدام الواسع النطاق للمؤشرات الذكية للتحكم في عمر شرائح الذاكرة ، فهذا هو الخيار الذي ننظر فيه أولاً.
- إذا كان البرنامج يعمل في بيئة بها مجموعة من البيانات المهملة ، فمن المحتمل أن يحدث تسرب من هذا النوع عن طريق امتداد الرمز الأصلي ، ومع ذلك ، يجب أولاً التخلص من التسريبات من النوعين (2) و (3).
كيفية العثور على مثل هذا التسرب- استخدام ASAN . استخدام ASAN. استخدام ASAN.
- استخدام كاشف مختلف. جربت أدوات Valgrind أو tcmalloc للعمل مع مجموعة ، وهناك أيضًا أدوات أخرى في بيئات أخرى.
- تسمح بعض برامج تخصيص الذاكرة بإلقاء ملف تعريف كومة الذاكرة المؤقتة ، والذي سيُظهر جميع مناطق الذاكرة غير المخصصة. إذا كان لديك تسرب ، وبعد مرور بعض الوقت ، فإن جميع التصريفات النشطة تقريبًا سوف تتدفق منه ، لذلك قد لا يكون العثور عليها صعبًا.
- إذا فشل كل شيء آخر ، تفريغ تفريغ ذاكرة وفحصها بدقة قدر الإمكان . ولكن بالتأكيد لا ينبغي أن تبدأ مع هذا.
النوع (2): تخصيصات الذاكرة غير المخطط لها لفترة طويلةمثل هذه المواقف ليست "تسريبات" بالمعنى التقليدي للكلمة ، حيث لا يزال يتم الاحتفاظ بالرابط من مكان ما إلى هذه القطعة من الذاكرة ، لذلك في النهاية يمكن إصدارها (إذا تمكن البرنامج من الوصول إلى هناك دون استخدام كل الذاكرة).
يمكن أن تنشأ المواقف في هذه الفئة لأسباب عديدة محددة. الاكثر شيوعا هي:
- التراكم غير المقصود للدولة في هيكل عالمي ؛ على سبيل المثال ، يكتب خادم HTTP في القائمة العامة لكل كائن
Request
مستلم. - مخابئ بدون سياسة تقادم مدروسة. على سبيل المثال ، ذاكرة التخزين المؤقت ORM التي تخزن مؤقتًا كل كائن تم تحميله نشطًا أثناء الترحيل ، حيث يتم تحميل جميع السجلات الموجودة في الجدول دون استثناء.
- يتم التقاط حالة ضخمة جدا في الدائرة. هذه الحالة شائعة بشكل خاص في Java Script ، ولكنها يمكن أن تحدث أيضًا في البيئات الأخرى.
- بمعنى أوسع ، هو الاحتفاظ غير المقصود لكل عنصر من عناصر مجموعة أو دفق ، بينما كان من المفترض أن هذه العناصر ستتم معالجتها عبر الإنترنت.
كيفية تحديد ما إذا كان تسرب ينتمي إلى هذه الفئة- إذا كان البرنامج يعمل في بيئة بها مجموعة من البيانات المهملة ، فهذا هو الخيار الذي ندرسه أولاً.
- قارن بين حجم الكومة المعروض في إحصائيات أداة تجميع مجمعي البيانات المهملة مع حجم الذاكرة الخالية التي تم إنشاؤها بواسطة نظام التشغيل. إذا وقع تسرب في هذه الفئة ، فستكون الأرقام قابلة للمقارنة ، والأهم من ذلك أنها ستتبع بعضها البعض بمرور الوقت.
كيفية العثور على مثل هذا التسرباستخدم ملفات التعريف أو أدوات تفريغ الكومة المتوفرة في البيئة الخاصة بك. أعرف أن هناك
غبي في Python أو
memory_profiler في Ruby ، كما كتبت
ObjectSpace مباشرة في Ruby.
النوع (3): ذاكرة مجانية ولكن غير مستعملة أو غير صالحة للاستعماليصعب وصف هذه الفئة ، ولكن من المهم للغاية فهمها ومراعاة ذلك.
تحدث التسريبات من هذا النوع في المنطقة الرمادية ، بين الذاكرة ، والتي تعتبر "خالية" من وجهة نظر المُخصص داخل بيئة VM أو بيئة التشغيل ، والذاكرة ، والتي هي "حرة" من وجهة نظر نظام التشغيل. السبب الأكثر شيوعًا (ولكن ليس السبب الوحيد) لهذه الظاهرة هو
تجزئة الكومة . تأخذ بعض أدوات التخصيص ببساطة ولا تعيد الذاكرة إلى نظام التشغيل بعد تخصيصها.
يمكن النظر في حالة من هذا النوع مع مثال لبرنامج قصير كتب في بيثون:
import sys from guppy import hpy hp = hpy() def rss(): return 4096 * int(open('/proc/self/stat').read().split(' ')[23]) def gcsize(): return hp.heap().size rss0, gc0 = (rss(), gcsize()) buf = [bytearray(1024) for i in range(200*1024)] print("start rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0)) buf = buf[::2] print("end rss={} gcsize={}".format(rss()-rss0, gcsize()-gc0))
نحن نخصص 200000 كيلو بايت من المخازن المؤقتة ، ثم نحفظ كل لاحقة. في كل ثانية ، نعرض حالة الذاكرة من وجهة نظر نظام التشغيل ومن وجهة نظر جامع البيانات المهملة Python الخاص بنا.
على جهاز الكمبيوتر المحمول ، أحصل على شيء مثل هذا:
start rss=232222720 gcsize=11667592
end rss=232222720 gcsize=5769520
يمكننا أن نتأكد من أن Python قد حررت بالفعل نصف المخازن المؤقتة ، لأن مستوى gcsize انخفض إلى ما يقرب من نصف قيمة الذروة ، لكن لم يستطع إرجاع بايت من هذه الذاكرة إلى نظام التشغيل. تظل الذاكرة المحررة في متناول عملية بيثون نفسها ، ولكن ليس لأي عملية أخرى على هذا الجهاز.
هذه الأجزاء المجانية ولكن غير المستخدمة من الذاكرة يمكن أن تكون إشكالية وغير ضارة. إذا كان برنامج Python يتصرف بهذه الطريقة ثم يخصص حفنة من شظايا 1 كيلوبايت ، فسيتم إعادة استخدام هذه المساحة ببساطة ، وكل شيء على ما يرام.
ولكن ، إذا فعلنا ذلك أثناء الإعداد الأولي ، وقمنا بعد ذلك بتخصيص الذاكرة كحد أدنى ، أو إذا كانت كل الأجزاء المخصصة لاحقًا 1.5 كيلو بايت لكل منها ولم تنسجم مع هذه المخازن المؤقتة التي تركت مقدمًا ، فستظل كل الذاكرة المخصصة بهذه الطريقة في وضع الخمول سوف تضيع.
تعتبر المشكلات من هذا النوع وثيقة الصلة بشكل خاص في بيئة معينة ، أي في أنظمة الخادم متعددة المعالجات للعمل مع لغات مثل Ruby أو Python.
لنفترض أننا أنشأنا نظامًا:
- في كل خادم ، يتم استخدام N العامل المفرد الخيوط لخدمة الطلبات بكفاءة. لنأخذ N = 10 للتأكد من دقتها.
- كقاعدة عامة ، كل موظف لديه قدر ثابت تقريبا من الذاكرة. لدقة ، دعونا نلقي 500MB.
- مع بعض التردد المنخفض ، نتلقى الطلبات التي تتطلب ذاكرة أكبر بكثير من الطلب المتوسط. لدقة ، دعنا نفترض أنه بمجرد وصولنا للطلب ، يتطلب وقت التنفيذ بالإضافة إلى ذلك 1 غيغابايت إضافية من الذاكرة ، وعندما تتم معالجة الطلب ، يتم تحرير هذه الذاكرة.
مرة واحدة في الدقيقة ، يصل طلب "الحيتانيات" هذا ، الذي نعهد بمعالجته إلى أحد العمال العشرة ، على سبيل المثال ، عشوائيًا:
~random
. من الناحية المثالية ، أثناء معالجة هذا الطلب ، يجب على هذا الموظف تخصيص 1 جيجابايت من ذاكرة الوصول العشوائي ، وبعد نهاية العمل ، يُرجع هذه الذاكرة إلى نظام التشغيل بحيث يمكن إعادة استخدامها لاحقًا. لمعالجة الطلبات بشكل غير محدود وفقًا لهذا المبدأ ، سيحتاج الخادم إلى 10 * 500 ميجابايت فقط + 1 جيجابايت = 6 جيجابايت من ذاكرة الوصول العشوائي.
ومع ذلك ، لنفترض أنه نظرًا للتجزئة أو لسبب آخر ، لن يتمكن الجهاز الظاهري من إعادة هذه الذاكرة إلى نظام التشغيل. بمعنى أن مقدار ذاكرة الوصول العشوائي (RAM) التي يتطلبها نظام التشغيل يساوي أكبر كمية ذاكرة لديك في أي وقت. في هذه الحالة ، عندما يخدم موظف معين مثل هذا الطلب كثيف الاستخدام للموارد ، فإن المنطقة التي تشغلها مثل هذه العملية في الذاكرة سوف تنتفخ إلى الأبد بواسطة غيغابايت كاملة.
عند بدء تشغيل الخادم ، سترى أن حجم الذاكرة المستخدمة هو 10 * 500MB = 5GB. بمجرد وصول أول طلب كبير ، يشغل العامل الأول ذاكرة 1 جيجابايت ، ثم لا يعيدها. سيقفز إجمالي حجم الذاكرة المستخدمة إلى 6 جيجابايت. قد يتم في بعض الأحيان إعادة توجيه الطلبات الواردة التالية إلى العملية التي سبق أن عالجت "الحوت" ، وفي هذه الحالة لن يتغير حجم الذاكرة المستخدمة. ولكن في بعض الأحيان يتم تسليم مثل هذا الطلب الكبير إلى موظف آخر ، وبسبب ذلك سيتم تضخيم الذاكرة بسعة 1 جيجابايت أخرى ، وهكذا حتى يتمكن كل موظف من معالجة مثل هذا الطلب الكبير مرة واحدة على الأقل. في هذه الحالة ، سوف يستغرق ما يصل إلى 10 * (500 ميجابايت + 1 جيجابايت) = 15 جيجابايت من ذاكرة الوصول العشوائي مع هذه العمليات ، وهو أكثر بكثير من 6 جيجابايت المثالي! علاوة على ذلك ، إذا نظرت إلى كيفية استخدام أسطول الخادم مع مرور الوقت ، يمكنك أن ترى كيف ينمو حجم الذاكرة المستخدمة تدريجياً من 5 غيغابايت إلى 15 غيغابايت ، وهو ما سيذكرنا تمامًا بحدوث تسرب "حقيقي".
كيفية تحديد ما إذا كان تسرب ينتمي إلى هذه الفئة- قارن بين حجم الكومة المعروض في إحصائيات أداة تجميع مجمعي البيانات المهملة مع حجم الذاكرة الخالية التي تم إنشاؤها بواسطة نظام التشغيل. إذا كان التسرب ينتمي إلى هذه الفئة (الثالثة) ، فسوف تتباعد الأرقام بمرور الوقت.
- أحب تكوين خوادم التطبيق الخاصة بي بحيث يكون كلا هذين الرقمين متوقفين بشكل دوري في البنية التحتية للمسلسلات الزمنية الخاصة بي ، لذلك من المناسب عرض الرسوم البيانية عليها.
- على نظام Linux ، شاهد حالة نظام التشغيل في الحقل 24 من
/proc/self/stat
، واعرض أداة تخصيص الذاكرة من خلال لغة أو واجهة برمجة تطبيقات خاصة بالآلة الافتراضية.
كيفية العثور على مثل هذا التسربكما ذكرنا سابقًا ، هذه الفئة أكثر غدراً من تلك السابقة ، حيث أن المشكلة غالباً ما تنشأ حتى عندما تعمل جميع المكونات "كما هو مقصود". ومع ذلك ، هناك عدد من الحيل المفيدة للمساعدة في تخفيف أو تقليل تأثير "التسريبات الافتراضية":
- إعادة تشغيل العمليات الخاصة بك في كثير من الأحيان. إذا نمت المشكلة ببطء ، فقد لا يكون من الصعب إعادة تشغيل جميع عمليات التطبيق مرة واحدة كل 15 دقيقة أو مرة واحدة في الساعة.
- طريقة أكثر جذرية: يمكنك تعليم جميع العمليات لإعادة التشغيل بشكل مستقل ، بمجرد أن تتجاوز المساحة التي تشغلها في الذاكرة قيمة عتبة معينة أو تنمو بقيمة محددة مسبقًا. ومع ذلك ، حاول أن تتوقع أن أسطول الخوادم بالكامل لا يمكنه بدء إعادة تشغيل متزامن تلقائي.
- تغيير مخصص الذاكرة. في المدى الطويل ، عادةً ما يتعامل tcmalloc و jemalloc مع التجزئة بشكل أفضل بكثير من المُخصص الافتراضي ، وتجربة استخدامهما مريحة للغاية باستخدام متغير
LD_PRELOAD
. - معرفة ما إذا كان لديك استفسارات فردية تستهلك ذاكرة أكثر بكثير من بقية. في Stripe ، تقوم خوادم API الخاصة بنا بقياس RSS (استهلاك ثابت للذاكرة) قبل وبعد تقديم كل طلب API وتسجيل الدلتا. بعد ذلك ، يمكننا بسهولة الاستعلام عن أنظمة تجميع السجلات الخاصة بنا لتحديد ما إذا كانت هناك أطراف ومستخدمون (وأنماط) يمكن استخدامها لشطب رشقات استهلاك الذاكرة.
- ضبط أداة تجميع مجمعي البيانات المهملة / الذاكرة. لدى الكثير منها معلمات قابلة للتخصيص تتيح لك تحديد مدى فعالية هذه الآلية في إعادة الذاكرة إلى نظام التشغيل ، ومدى تحسينها للقضاء على التجزئة. هناك خيارات أخرى مفيدة. كل شيء هنا معقد للغاية: تأكد من أنك تفهم بالضبط ما تقيسه وتحسنه ، وحاول أيضًا العثور على خبير في الجهاز الظاهري المناسب والتشاور معه.