في عام 2015 ، كتبت عن الأدوات التي يوفرها روبي
للكشف عن تسرب الذاكرة المدارة . في الغالب ، تحدث المقال عن تسريبات يمكن التحكم فيها بسهولة. هذه المرة سأتحدث عن الأدوات والحيل التي يمكنك استخدامها للقضاء على التسريبات التي ليس من السهل تحليلها في روبي. على وجه الخصوص ، سأتحدث عن mwrap و heaptrack و iseq_collector و chap.
تسرب الذاكرة غير المدارة
هذا البرنامج الصغير يثير تسربًا مع دعوة مباشرة إلى malloc. يبدأ باستهلاك 16 ميغابايت من RSS ، وينتهي بـ 118 ميغابايت. يضع الكود في الذاكرة 100 ألف كتلة من 1024 بايت ويحذف 50 ألف منهم.
require 'fiddle' require 'objspace' def usage rss = `ps -p
على الرغم من أن RSS يبلغ 118 ميجابايت ، إلا أن كائن Ruby الخاص بنا لا يعرف سوى ثلاثة ميغابايت. في التحليل ، نرى جزءًا صغيرًا جدًا من تسرب الذاكرة الكبير جدًا.
مثال حقيقي على مثل هذا التسرب
وصفه أوليغ داشفسكي ، أوصي بقراءة هذا المقال الرائع.
تطبيق Mwrap
Mwrap هو ملف تعريف للذاكرة لـ Ruby يقوم بمراقبة جميع عمليات تخصيص البيانات في الذاكرة عن طريق اعتراض malloc والوظائف الأخرى لهذه العائلة. يعترض المكالمات ذلك المكان والذاكرة الخالية باستخدام
LD_PRELOAD . يستخدم
liburcu للعد ويمكنه تتبع عدادات التخصيص والحذف لكل نقطة اتصال في كود C و Ruby. Mwrap صغير الحجم ، أي حوالي ضعف حجم RSS لبرنامج مخصَّص ، وحوالي بطئ.
إنه يختلف عن العديد من المكتبات الأخرى في حجمها الصغير جدًا ودعمها لروبي. يتتبع المواقع في ملفات Ruby ولا يقتصر على التراجع عن مستوى c + masif التراجع والملفات التعريف مماثلة. هذا يبسط إلى حد كبير عزل مصادر المشاكل.
لاستخدام ملف التعريف ، تحتاج إلى تشغيل التطبيق من خلال Mwrap shell ، وسوف تنفذ بيئة LD_PRELOAD وتشغيل ثنائي روبي.
دعنا نضيف Mwrap إلى نصنا:
require 'mwrap' def report_leaks results = [] Mwrap.each do |location, total, allocations, frees, age_total, max_lifespan| results << [location, ((total / allocations.to_f) * (allocations - frees)), allocations, frees] end results.sort! do |(_, growth_a), (_, growth_b)| growth_b <=> growth_a end results[0..20].each do |location, growth, allocations, frees| next if growth == 0 puts "#{location} growth: #{growth.to_i} allocs/frees (#{allocations}/#{frees})" end end GC.start Mwrap.clear leak_memory GC.start
الآن قم بتشغيل البرنامج النصي باستخدام برنامج Mwrap:
% gem install mwrap % mwrap ruby leak.rb leak.rb:12 growth: 51200000 allocs/frees (100000/50000) leak.rb:51 growth: 4008 allocs/frees (1/0)
اكتشف Mwrap تسربًا صحيحًا في البرنامج النصي (50000 * 1024). وليس فقط تحديد ، ولكن أيضا عزل خط معين (
i = Fiddle.malloc(1024)
) ، مما أدى إلى تسرب. يرتبط منشئ ملفات التعريف بشكل صحيح إلى المكالمات إلى
Fiddle.free
.
من المهم الإشارة إلى أننا نتعامل مع التقييم. تراقب Mwrap الذاكرة المشتركة المخصصة من قِبل نظير الطلب ، ثم تراقب تحرير الذاكرة. ولكن إذا كان لديك نقطة اتصال واحدة تخصص كتل ذاكرة بأحجام مختلفة ، فستكون النتيجة غير دقيقة. لدينا إمكانية الوصول إلى التقييم:
((total / allocations) * (allocations - frees))
بالإضافة إلى ذلك ، لتبسيط تتبع التسرب ، يقوم Mwrap بتتبع
age_total
، وهو مجموع عمر كل عنصر تم تحريره ، ويتتبع أيضًا
max_lifespan
، عمر العنصر الأقدم في نقطة الاتصال. إذا كان
age_total / frees
كبيرًا ، فإن استهلاك الذاكرة يتزايد على الرغم من العديد من مجموعات البيانات المهملة.
Mwrap لديه العديد من المساعدين للحد من الضوضاء. سوف
Mwrap.clear
مسح جميع التخزين الداخلية.
Mwrap.quiet {}
Mwrap لتتبع كتلة التعليمات البرمجية.
ميزة أخرى مميزة في Mwrap هي تتبع العدد الإجمالي للبايتات المخصصة والمحررة. إزالة
clear
من البرنامج النصي وتشغيله:
usage puts "Tracked size: #{(Mwrap.total_bytes_allocated - Mwrap.total_bytes_freed) / 1024}"
النتيجة مثيرة جدًا للاهتمام ، لأنه على الرغم من حجم RSS الذي يبلغ 130 ميغابايت ، فإن Mwrap لا ترى سوى 91 ميغابايت. هذا يشير إلى أننا قمنا بتضخيم عمليتنا. يظهر التنفيذ بدون Mwrap أن العملية في الوضع الطبيعي تستغرق 118 ميجابايت ، وفي هذه الحالة البسيطة كان الفرق 12 ميجابايت. أدى تخصيص / تحرير نمط إلى تجزئة. يمكن أن تكون هذه المعرفة مفيدة للغاية ، في بعض الحالات ، يعالج glibc malloc غير المُصنَّع جزءًا كبيرًا بحيث تكون المساحة الكبيرة جدًا من الذاكرة المستخدمة في RSS مجانية.
يمكن Mwrap عزل تسرب redcarpet القديم؟
في
مقالته ، يناقش أوليغ طريقة شاملة للغاية لعزل تسرب ضئيل للغاية في السجادة الحمراء. هناك العديد من التفاصيل. من المهم جدا أن تأخذ القياسات. إذا كنت لا تبني جدولًا زمنيًا لعملية RSS ، فمن غير المرجح أن تكون قادرًا على التخلص من أي تسربات.
دعنا ندخل في آلة الزمن ونوضح مدى سهولة استخدام Mwrap لمثل هذه التسريبات.
def red_carpet_leak 100_000.times do markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, extensions = {}) markdown.render("hi") end end GC.start Mwrap.clear red_carpet_leak GC.start
Redcarpet 3.3.2:
redcarpet.rb:51 growth: 22724224 allocs/frees (500048/400028) redcarpet.rb:62 growth: 4008 allocs/frees (1/0) redcarpet.rb:52 growth: 634 allocs/frees (600007/600000)
Redcarpet 3.5.0:
redcarpet.rb:51 growth: 4433 allocs/frees (600045/600022) redcarpet.rb:52 growth: 453 allocs/frees (600005/600000)
إذا كنت تستطيع تشغيل العملية بنصف السرعة عن طريق إعادة تشغيلها ببساطة في منتج Mwrap مع تسجيل النتيجة إلى ملف ، فيمكنك تحديد نطاق واسع من تسرب الذاكرة.
تسرب غامض
في الآونة الأخيرة ، تم تحديث Rails إلى الإصدار 6. بشكل عام ، كانت التجربة إيجابية للغاية ، وظل الأداء على حاله تقريبًا. يحتوي Rails 6 على بعض الميزات الجيدة جدًا التي
سنستخدمها (على سبيل المثال
Zeitwerk ). غيّرت القضبان طريقة عرض القوالب ، مما تطلب بعض التغييرات من أجل التوافق. بعد أيام قليلة من التحديث ، لاحظنا زيادة في أداء مهام تطبيق Sidekiq لـ RSS.
ذكرت Mwrap زيادة حادة في استهلاك الذاكرة بسبب تخصيصها (
الرابط ):
source.encode!
في البداية كنا في حيرة للغاية. كنا نحاول أن نفهم لماذا غير راضين عن Mwrap؟ ربما كسر؟ كما نما استهلاك الذاكرة ، ظلت أكوام في روبي دون تغيير.

استهلك مليوني فتحات في الكومة 78 ميغابايت فقط (40 بايت لكل فتحة). يمكن أن تشغل الخطوط والمصفوفات مساحة أكبر ، لكنها لم تفسر استهلاك الذاكرة غير الطبيعي الذي لاحظناه. تم تأكيد ذلك عندما قمت
rbtrace -p SIDEKIQ_PID -e ObjectSpace.memsize_of_all
.
أين ذهبت الذاكرة؟
Heaptrack
Heaptrack هو ملف تعريف ذاكرة كومة لنظام التشغيل Linux.
أوضحت ميليان وولف تمامًا كيف يعمل المحلل وتحدثت عنه في عدة خطب (
1 ،
2 ،
3 ). في الواقع ، إنه ملف تعريف كومة أصلي فعال للغاية يقوم ، بمساعدة
libunwind ، بجمع backtraces من التطبيقات
المحمية . إنه يعمل بشكل أسرع بشكل ملحوظ من
Valgrind / Massif ولديه القدرة على جعله أكثر ملاءمة
للتوصيف المؤقت في المنتج. يمكن إرفاقه بعملية قيد التشغيل بالفعل!
كما هو الحال مع معظم محبي كومة الذاكرة المؤقتة ، عند استدعاء كل وظيفة في عائلة malloc ، يجب على Heaptrack الاعتماد. هذا الإجراء بالتأكيد يبطئ العملية قليلاً.
في رأيي ، الهندسة المعمارية هنا هي الأفضل للجميع. يتم تنفيذ اعتراض باستخدام
LD_PRELOAD
أو
GDB لتحميل منشئ ملفات التعريف. باستخدام
ملف FIFO خاص ، يقوم بنقل البيانات من عملية التشكيل الجانبي في أسرع وقت ممكن. إن
أداة التفاف
heaptrack عبارة عن برنامج نصي بسيط يسهل العثور على مشكلة. تقرأ العملية الثانية معلومات من FIFO وضغطات تتبع بيانات التتبع. نظرًا لأن Heaptrack يعمل مع "قطع" ، يمكنك تحليل الملف الشخصي بعد ثوانٍ قليلة من بداية التوصيف ، في منتصف الجلسة مباشرة. ما عليك سوى نسخ ملف التعريف إلى موقع آخر وإطلاق واجهة المستخدم الرسومية Heaptrack.
أخبرتني تذكرة GitLab هذه عن إمكانية إطلاق Heaptrack. إذا تمكنوا من تشغيله ، فيمكنني ذلك.
يتم تشغيل
--cap-add=SYS_PTRACE
،
--cap-add=SYS_PTRACE
إلى إعادة تشغيله باستخدام
--cap-add=SYS_PTRACE
، مما يسمح لـ GDB باستخدام
ptrace ، وهو أمر ضروري لـ Heaptrack لحقن نفسه. أحتاج أيضًا إلى
اختراق صغير لملف shell لتطبيق
root
على ملف تعريف العملية غير
root
(أطلقنا تطبيق Discourse لدينا في الحاوية تحت حساب محدود).
بعد الانتهاء من كل شيء ، يبقى فقط تنفيذ
heaptrack -p PID
وانتظار ظهور النتائج. تبين أن Heaptrack أداة ممتازة ، فقد كان من السهل جدًا تتبع كل ما يحدث مع تسرب الذاكرة.

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

يرتبط الربح الرئيسي بـ
iseq_set_sequence
في
compile.c
. اتضح أن التسرب يرجع إلى تسلسل التعليمات. هذا تطهير تسرب اكتشفها Mwrap. كان السبب هو
mod.module_eval(source, identifier, 0)
، مما أدى إلى إنشاء تسلسل للتعليمات التي لم يتم حذفها من الذاكرة.
إذا ، في تحليل بأثر رجعي ، فكرت بعناية في تفريغ كومة من روبي ، ثم كنت قد لاحظت كل هذه IMEMOs ، لأنها مدرجة في هذا التفريغ. إنها ببساطة غير مرئية أثناء عمليات التشخيص.
من هذه النقطة ، كان تصحيح الأخطاء بسيطًا جدًا. لقد تتبعت جميع المكالمات إلى وحدة eval وألقيت ما قيمته. لقد وجدنا أننا نضيف طرقًا إلى فصل كبير مرارًا وتكرارًا. فيما يلي عرض مبسط للأخطاء التي واجهناها:
require 'securerandom' module BigModule; end def leak_methods 10_000.times do method = "def _#{SecureRandom.hex}; #{"sleep;" * 100}; end" BigModule.module_eval(method) end end usage # RSS: 16164 ObjectSpace size 2869 leak_methods usage
يحتوي روبي على فئة لتخزين تسلسلات
RubyVM::InstructionSequence
من التعليمات:
RubyVM::InstructionSequence
. ومع ذلك ، يعتبر Ruby كسولًا جدًا في إنشاء هذه الكائنات المجمعة ، لأن تخزينها دون داعٍ يكون غير فعال. أنشأ كويتشي
ساسادا تبعية
iseq_collector . إذا أضفنا هذا الرمز ، يمكننا أن نجد ذاكرتنا المخفية:
require 'iseq_collector' puts "#{ObjectSpace.memsize_of_all_iseq / 1024}"
يجسد كل سلسلة من التعليمات ، والتي يمكن أن تزيد بشكل طفيف من استهلاك الذاكرة للعملية وتجعل جامع البيانات المهملة أكثر من ذلك بقليل.
على سبيل المثال ، إذا قمنا بحساب عدد ISEQs قبل وبعد بدء المجمع ، فسنرى أنه بعد بدء تشغيل
ObjectSpace.memsize_of_all_iseq
،
RubyVM::InstructionSequence
من 0 إلى 11128 (في هذا المثال):
def count_iseqs ObjectSpace.each_object(RubyVM::InstructionSequence).count end
ستبقى هذه الأغلفة طوال عمر الطريقة ، وستحتاج إلى زيارتها مع التشغيل الكامل لجامع البيانات المهملة. تم حل مشكلتنا من خلال إعادة استخدام الفئة المسؤولة عن تقديم قوالب البريد الإلكتروني (
الإصلاح 1 ،
الإصلاح العاجل 2 ).
الفصل
أثناء تصحيح الأخطاء ، استخدمت أداة مثيرة جدًا للاهتمام. قبل بضع سنوات ، قام Tim Boddy بسحب أداة داخلية يستخدمها VMWare لتحليل تسرب الذاكرة وجعل كودها مفتوحًا. هنا هو الفيديو الوحيد الذي تمكنت من العثور عليه:
https://www.youtube.com/watch؟v=EZ2n3kGtVDk . على عكس معظم الأدوات المشابهة ، لا تؤثر هذه الأداة على العملية القابلة للتنفيذ. يمكن تطبيقه ببساطة على ملفات التفريغ الرئيسي ، بينما يتم استخدام glibc كمخصص (لا يوجد دعم jemalloc / tcmalloc ، إلخ).
مع الفصل ، من السهل جدًا اكتشاف التسرب الذي أصابته. قليل من التوزيعات تحتوي على الفصل الثنائي ، ولكن يمكنك
تجميعه بسهولة
من الكود المصدري . وهو مدعوم بنشاط كبير.
# 444098 is the `Process.pid` of the leaking process I had sudo gcore -p 444098 chap core.444098 chap> summarize leaked Unsigned allocations have 49974 instances taking 0x312f1b0(51,573,168) bytes. Unsigned allocations of size 0x408 have 49974 instances taking 0x312f1b0(51,573,168) bytes. 49974 allocations use 0x312f1b0 (51,573,168) bytes. chap> list leaked ... Used allocation at 562ca267cdb0 of size 408 Used allocation at 562ca267d1c0 of size 408 Used allocation at 562ca267d5d0 of size 408 ... chap> summarize anchored .... Signature 7fbe5caa0500 has 1 instances taking 0xc8(200) bytes. 23916 allocations use 0x2ad7500 (44,922,112) bytes.
بإمكان الفصل استخدام التواقيع للبحث عن مواقع ذاكرة مختلفة ، ويمكن أن يكمل GDB. عند تصحيح الأخطاء في Ruby ، قد يكون ذلك مفيدًا للغاية في تحديد الذاكرة التي تستخدمها العملية. يُظهر إجمالي الذاكرة المستخدمة ، في بعض الأحيان يمكن أن يتجزئ glibc malloc كثيرًا بحيث يمكن أن يكون مستوى الصوت المستخدم مختلفًا تمامًا عن RSS الفعلي. يمكنك قراءة المناقشة:
الميزة رقم 14759: [PATCH] اضبط M_ARENA_MAX لنظام glibc malloc - Ruby master - نظام تتبع المشكلات في Ruby . تشاب قادر على حساب جميع الذاكرة المستخدمة بشكل صحيح وتقديم تحليل متعمق لتخصيصها.
بالإضافة إلى ذلك ، يمكن دمج الفصل في سير العمل لاكتشاف التسريبات تلقائيًا ووضع علامات على هذه التجميعات.
متابعة العمل
جعلتني جولة التصحيح هذه أطرح بعض الأسئلة المتعلقة بمجموعات أدوات المساعدة الخاصة بنا:
ملخص
مجموعة أدوات اليوم لتصحيح أخطاء الذاكرة المعقدة للغاية أفضل بكثير مما كانت عليه قبل 4 سنوات! أدوات Mwrap و Heaptrack و chap هي أدوات قوية للغاية لحل مشاكل الذاكرة التي تنشأ أثناء التطوير والتشغيل.
إذا كنت تبحث عن تسرب بسيط للذاكرة في روبي ، فإنني أوصي بقراءة
مقالتي لعام 2015 ، وهو في معظمه وثيق الصلة.
أتمنى أن تجد الأمر أسهل في المرة التالية التي تبدأ فيها تصحيح أخطاء تسرب ذاكرة أصلية معقدة.