كيف تحققت من تسرب الذاكرة في Go باستخدام pprof على قاعدة بيانات كبيرة

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

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

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

يمكننا أن نتخيل رسم خرائط الذاكرة كشجرة ، وسيأخذنا عبور تلك الشجرة عبر التخصيصات المختلفة للأشياء والعلاقات. هذا يعني أن كل ما هو في الجذر هو سبب "الاحتفاظ" بالذاكرة وليس تجميعها (تجميع القمامة). نظرًا لعدم وجود طريقة بسيطة في Go لتحليل التفريغ الأساسي الكامل ، فإن الوصول إلى جذور كائن لا يحصل على GC-ed أمر صعب.

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

تسريبات الذاكرة

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

أثناء قيامنا ببناء نظامنا وفقًا لمبادئ التصميم الناشئة ، لا يُعتقد أن مثل هذه الاعتبارات ذات أهمية وهذا أمر جيد. والأهم من ذلك هو بناء النظام بطريقة تتجنب التحسينات المبكرة وتمكنك من تنفيذها لاحقًا مع نضوج الكود ، بدلاً من المبالغة في هندستها منذ البداية. ومع ذلك ، فإن بعض الأمثلة الشائعة لرؤية مشكلات ضغط الذاكرة تتحقق هي:

  • عمليات تخصيص كثيرة جدًا ، تمثيل بيانات غير صحيح
  • الاستخدام الكثيف للانعكاس أو الخيوط
  • باستخدام الكرة الأرضية
  • goroutines يتيم لا تنتهي أبدًا

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

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

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

اذهب أداة pprof

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

في pprofحزمة يخلق كومة عينات ملف تفريغ، والتي يمكنك تحليل في وقت لاحق / تصور لتعطيك خريطة على حد سواء:

  • عمليات تخصيص الذاكرة الحالية
  • إجمالي عمليات تخصيص الذاكرة (التراكمية)

الأداة لديها القدرة على مقارنة اللقطات. يمكن أن يمكّنك هذا من مقارنة عرض فرق الوقت لما حدث الآن وقبل 30 ثانية ، على سبيل المثال. بالنسبة لسيناريوهات الضغط ، يمكن أن يكون هذا مفيدًا للمساعدة في تحديد المناطق الإشكالية في التعليمات البرمجية.

ملامح pprof

طريقة عمل pprof هي استخدام ملفات التعريف.

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

يحتوي الملف runtime / pprof / pprof.go على المعلومات التفصيلية وتنفيذ ملفات التعريف.

يحتوي Go على العديد من الملفات الشخصية المضمنة لنا لاستخدامها في الحالات الشائعة:

  • goroutine - كومة آثار جميع goroutines الحالية
  • كومة - عينة من الذاكرة المخصصة للكائنات الحية
  • allocs - عينة من جميع عمليات تخصيص الذاكرة السابقة
  • threadcreate - آثار مكدس أدت إلى إنشاء سلاسل عمليات جديدة لنظام التشغيل
  • آثار الكتلة - المكدس التي أدت إلى حظر العناصر الأولية للتزامن
  • كائن المزامنة - آثار كومة من حاملي كائنات المزامنة

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

الكومة

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

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

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

هذا ملخص للمواقع المختلفة التي يحدث فيها تخصيص الذاكرة. هناك الكثير لها ولكن هذا سيكون خارج نطاق هذا المنشور.

الحصول على بيانات الكومة باستخدام pprof

هناك طريقتان رئيسيتان للحصول على البيانات لهذه الأداة. سيكون الأول عادةً جزءًا من اختبار أو فرع ويتضمن الاستيراد runtime/pprofثم الاتصال pprof.WriteHeapProfile(some_file)لكتابة معلومات الكومة.

لاحظ أن هذا WriteHeapProfileهو السكر النحوي للتشغيل:

// lookup takes a profile namepprof.Lookup("heap").WriteTo(some_file, 0)

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

والثاني ، وهو الأكثر إثارة للاهتمام ، هو تمكينه عبر HTTP (نقاط النهاية المستندة إلى الويب). يتيح لك ذلك استخراج البيانات المخصصة ، من حاوية قيد التشغيل في بيئة e2e / الاختبار أو حتى من "الإنتاج". هذا مكان آخر حيث يتفوق وقت تشغيل Go ومجموعة الأدوات. تم العثور على وثائق الحزمة بأكملها هنا ، ولكن TL ؛ DR هو أنك ستحتاج إلى إضافتها إلى الكود الخاص بك على هذا النحو:

import ( "net/http" _ "net/http/pprof")
...
func main() { ... http.ListenAndServe("localhost:8080", nil)}

"التأثير الجانبي" للاستيراد net/http/pprofهو تسجيل نقاط نهاية pprof ضمن جذر خادم الويب في /debug/pprof. الآن باستخدام curl ، يمكننا الحصول على ملفات معلومات الكومة للتحقيق:

curl -sK -v //localhost:8080/debug/pprof/heap > heap.out

http.ListenAndServe()لا يلزم إضافة ما سبق إلا إذا لم يكن برنامجك يحتوي على مستمع http من قبل. إذا كان لديك واحد ، فسيتم ربطه ولا داعي للاستماع إليه مرة أخرى. هناك أيضًا طرق لإعداده باستخدام ServeMux.HandleFunc()الذي سيكون أكثر منطقية بالنسبة لبرنامج أكثر تعقيدًا ممكّنًا لـ http.

باستخدام pprof

لذلك قمنا بجمع البيانات ، ماذا الآن؟ كما ذكر أعلاه ، هناك استراتيجيتان رئيسيتان لتحليل الذاكرة باستخدام pprof. واحد يدور حول النظر في التخصيصات الحالية (بايت أو عدد العناصر) ، تسمى inuse. الآخر يبحث في جميع البايتات المخصصة أو عدد العناصر خلال وقت تشغيل البرنامج ، يسمى alloc. هذا يعني أنه بغض النظر عما إذا كان gc-ed ، فهو عبارة عن تجميع لكل شيء تم أخذ عينات منه.

هذا مكان جيد للتأكيد على أن ملف تعريف الكومة عبارة عن عينة من تخصيصات الذاكرة . pprofوراء الكواليس تستخدم runtime.MemProfileالوظيفة ، التي تجمع افتراضيًا معلومات التخصيص لكل 512 كيلوبايت من البايتات المخصصة. من الممكن تغيير MemProfile لجمع المعلومات حول جميع الكائنات. لاحظ أنه على الأرجح ، سيؤدي ذلك إلى إبطاء تطبيقك.

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

بمجرد أن نقوم بتجميع ملف الملف الشخصي ، فقد حان الوقت لتحميله في عروض pprof التفاعلية لوحدة التحكم. افعل ذلك عن طريق تشغيل:

> go tool pprof heap.out

دعونا نلاحظ المعلومات المعروضة

Type: inuse_spaceTime: Jan 22, 2019 at 1:08pm (IST)Entering interactive mode (type "help" for commands, "o" for options)(pprof)

الشيء المهم الذي يجب ملاحظته هنا هو ملف Type: inuse_space. هذا يعني أننا نبحث في بيانات التخصيص للحظة معينة (عندما التقطنا الملف الشخصي). النوع هو قيمة التكوين sample_index، والقيم المحتملة هي:

  • inuse_space - مقدار الذاكرة المخصص ولم يتم إصداره بعد
  • inuse_object s— كمية العناصر المخصصة والتي لم يتم تحريرها بعد
  • Custom_space - إجمالي مساحة الذاكرة المخصصة (بغض النظر عن الإصدار)
  • تخصيص الكائنات - إجمالي عدد العناصر المخصصة (بغض النظر عن الإصدار)

اكتب الآن topفي التفاعلية ، سيكون الإخراج هو المستهلك الأعلى للذاكرة

يمكننا أن نرى سطرًا يخبرنا عنه Dropped Nodes، وهذا يعني أنه تمت تصفيته. العقدة هي إدخال كائن أو "عقدة" في الشجرة. يُعد إسقاط العقد فكرة جيدة لتقليل بعض الضوضاء ، ولكن في بعض الأحيان قد يخفي السبب الجذري لمشكلة الذاكرة. سنرى مثالاً على ذلك ونحن نواصل تحقيقنا.

إذا كنت تريد تضمين جميع بيانات ملف التعريف ، أضف -nodefraction=0الخيار عند تشغيل pprof أو اكتب nodefraction=0التفاعلي.

في القائمة الناتجة يمكننا أن نرى قيمتين ، flatو cum.

  • مسطح يعني أن الذاكرة المخصصة بواسطة هذه الوظيفة وتحتفظ بها تلك الوظيفة
  • cum يعني أن الذاكرة تم تخصيصها بواسطة هذه الوظيفة أو الوظيفة التي استدعتها أسفل المكدس

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

حيلة أخرى رائعة topفي النافذة التفاعلية هي أنها تعمل بالفعل top10. يدعم الأمر العلوي topNالتنسيق حيث Nيوجد عدد الإدخالات التي تريد رؤيتها. في الحالة التي تم لصقها أعلاه top70، ستؤدي الكتابة على سبيل المثال إلى إخراج جميع العقد.

التصورات

بينما topNيعطي قائمة نصية ، هناك العديد من خيارات التصور المفيدة جدًا التي تأتي مع pprof. من الممكن أن تكتب pngأو gifوغيرها الكثير (انظر go tool pprof -helpللحصول على قائمة كاملة).

في نظامنا ، يبدو الإخراج المرئي الافتراضي مثل:

قد يكون هذا مخيفًا في البداية ، ولكنه تصور تدفقات تخصيص الذاكرة (وفقًا لتتبعات المكدس) في برنامج. قراءة الرسم البياني ليست معقدة كما تبدو. يُظهر المربع الأبيض الذي يحتوي على رقم مساحة مخصصة (وتراكم مقدار الذاكرة التي يشغلها الآن على حافة الرسم البياني) ، ويظهر كل مستطيل أعرض وظيفة التخصيص.

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

التعمق في البحث وإيجاد السبب الجذري

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

في حالتنا ، يمكننا أن نرى أن الذاكرة يتم الاحتفاظ بها membuffers، وهي مكتبة تسلسل البيانات الخاصة بنا. هذا لا يعني أن لدينا تسرب للذاكرة في مقطع الكود هذا ، فهذا يعني أنه يتم الاحتفاظ بالذاكرة بواسطة هذه الوظيفة. من المهم فهم كيفية قراءة الرسم البياني ومخرجات pprof بشكل عام. في هذه الحالة ، نفهم أنه عندما نقوم بتسلسل البيانات ، مما يعني أننا نخصص الذاكرة للبنى والكائنات البدائية (int ، string) ، فلن يتم تحريرها أبدًا.

عند القفز إلى الاستنتاجات أو إساءة تفسير الرسم البياني ، كان من الممكن أن نفترض أن إحدى العقد الموجودة على مسار التسلسل مسؤولة عن الاحتفاظ بالذاكرة ، على سبيل المثال:

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

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

هذا هو الوقت المناسب لتقديم pprofأمر آخر يسمى list. يقبل تعبيرًا عاديًا سيكون مرشحًا لما يتم سرده. "القائمة" هي في الواقع شفرة المصدر المشروحة المتعلقة بالتخصيص. في سياق المسجل الذي نبحث فيه ، سننفذ list RequestNewكما نود أن نرى المكالمات التي يتم إجراؤها إلى المسجل. تأتي هذه الاستدعاءات من وظيفتين تبدأان بنفس البادئة.

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

listيمكن العثور على شفرة المصدر عند البحث عنها في بيئتك GOPATH. في الحالات التي لا يتطابق فيها الجذر الذي يبحث عنه ، والذي يعتمد على آلة الإنشاء الخاصة بك ، يمكنك استخدام -trim_pathالخيار. سيساعد هذا في إصلاحه والسماح لك بمشاهدة شفرة المصدر المشروحة. تذكر أن تضبط git على الالتزام الصحيح الذي كان يعمل عندما تم التقاط ملف تعريف الكومة.

فلماذا يتم الاحتفاظ بالذاكرة؟

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

في هذه المرحلة ، في حالة Java أو .Net ، سنفتح بعض تحليل "جذور gc" أو ملف التعريف ونصل إلى الكائن الفعلي الذي يشير إلى تلك البيانات ، ويقوم بإنشاء التسريب. كما هو موضح ، هذا غير ممكن تمامًا مع Go ، سواء بسبب مشكلة في الأدوات ولكن أيضًا بسبب تمثيل ذاكرة Go منخفض المستوى.

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

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

عند الإعداد ، nodefraction=0سنتمكن من رؤية الخريطة الكاملة للكائنات المخصصة ، بما في ذلك الكائنات الأصغر. لنلقِ نظرة على الإخراج:

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

الأقصر ، باللون الأزرق ، والذي له حافة تربطه بالنظام بأكمله هو inMemoryBlockPersistance. يشرح هذا الاسم أيضًا "التسريب" الذي تخيلناه. هذه هي الواجهة الخلفية للبيانات ، والتي تخزن جميع البيانات في الذاكرة ولا تستمر في القرص. ما هو جميل أن نلاحظ أنه يمكننا أن نرى على الفور أنه يحمل جسمين كبيرين. لماذا اثنان؟ لأنه يمكننا رؤية حجم الكائن 1.28 ميجا بايت وتحتفظ الوظيفة بـ 2.57 ميجا بايت ، أي اثنين منهم.

المشكلة مفهومة جيدًا في هذه المرحلة. كان بإمكاننا استخدام delve (مصحح الأخطاء) لنرى أن هذه هي المصفوفة التي تحتوي على جميع الكتل لبرنامج التشغيل المستمر في الذاكرة لدينا.

إذن ما الذي يمكننا إصلاحه؟

حسنًا ، هذا مقرف ، كان خطأ بشريًا. بينما كانت العملية تثقيفية (والمشاركة تهتم) ، لم نتحسن ، أم أننا؟

كان هناك شيء واحد لا يزال "يشم" حول هذه الكومة من المعلومات. كانت البيانات التي تم إلغاء تسلسلها تستهلك الكثير من الذاكرة ، فلماذا 142 ميغا بايت لشيء يجب أن يأخذ أقل بكثير؟ . . يمكن لـ pprof الإجابة على ذلك - في الواقع ، إنه موجود للإجابة على مثل هذه الأسئلة بالضبط.

للنظر في الكود المصدري المشروح للوظيفة ، سنعمل list lazy. نستخدم lazy، كاسم الوظيفة الذي نبحث عنه lazyCalcOffsets()ولا نعرف أي وظائف أخرى في الكود لدينا تبدأ بـ lazy. list lazyCalcOffsetsستعمل الكتابة بشكل جيد بالطبع.

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

بعد ذلك ، يمكننا أن نرى أن الصنع () يأخذ بعض الذاكرة. هذا منطقي ، إنه المؤشر إلى بنية البيانات. ومع ذلك ، نرى أيضًا أن التخصيص في السطر 43 يشغل الذاكرة ، مما يعني أنه ينشئ تخصيصًا.

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

يجب أن يؤخذ ما يلي بحذر: سيكون من المقبول أن نقول إن استخدام a map[int]T، عندما لا تكون البيانات قليلة أو يمكن تحويلها إلى مؤشرات متسلسلة ، يجب عادةً محاولة تطبيق شريحة إذا كان استهلاك الذاكرة هو الاعتبار المناسب . ومع ذلك ، قد تؤدي شريحة كبيرة ، عند توسيعها ، إلى إبطاء العملية ، حيث سيكون هذا التباطؤ في الخريطة ضئيلًا. لا توجد صيغة سحرية للتحسينات.

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

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

دعونا نلقي نظرة على benchcmpاختبارين فقط

تعمل اختبارات القراءة على تهيئة بنية البيانات ، مما يؤدي إلى إنشاء عمليات التخصيص. يمكننا أن نرى أن وقت التشغيل قد تحسن بنسبة 30٪ تقريبًا ، وانخفضت عمليات التخصيص بنسبة 50٪ واستهلاك الذاكرة بنسبة> 90٪ (!)

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

إذا نظرنا pprofمرة أخرى ، وأخذ ملف تعريف كومة من نفس الاختبار ، فسنرى أن استهلاك الذاكرة الآن انخفض في الواقع بنسبة 90 ٪ تقريبًا.

ستكون النتيجة أنه بالنسبة لمجموعات البيانات الأصغر ، لا يجب عليك استخدام الخرائط حيث تكفي الشرائح ، لأن الخرائط بها عبء كبير.

تفريغ كامل النواة

كما ذكرنا ، هذا هو المكان الذي نرى فيه أكبر قيود على الأدوات في الوقت الحالي. عندما كنا نحقق في هذه المشكلة ، أصبحنا مهووسين بالقدرة على الوصول إلى الكائن الجذر ، دون نجاح كبير. يتطور Go بمرور الوقت بوتيرة كبيرة ، لكن هذا التطور يأتي بسعر في حالة التفريغ الكامل أو تمثيل الذاكرة. تنسيق تفريغ الكومة الكامل ، كما يتغير ، غير متوافق مع الإصدارات السابقة. أحدث إصدار موصوف هنا وكتابة ملف تفريغ كامل ، يمكنك استخدام debug.WriteHeapDump().

وإن كنا في الوقت الحالي لا نجد أنفسنا "عالقين" لأنه لا يوجد حل جيد لاستكشاف مقالب كاملة. pprofأجاب على جميع أسئلتنا حتى الآن.

لاحظ أن الإنترنت يتذكر الكثير من المعلومات التي لم تعد ذات صلة. إليك بعض الأشياء التي يجب تجاهلها إذا كنت ستحاول فتح ملف تفريغ كامل بنفسك ، اعتبارًا من go1.11:

  • لا توجد طريقة لفتح وتفريغ نواة كاملة على نظام MacOS ، فقط Linux.
  • الأدوات الموجودة في //github.com/randall77/hprof مخصصة لـ Go1.3 ، هناك مفترق لـ 1.7+ ولكنه لا يعمل بشكل صحيح أيضًا (غير مكتمل).
  • عرض النقاط في //github.com/golang/debug/tree/master/cmd/viewcore لا يتم تجميعه حقًا. من السهل إصلاحه (الحزم الداخلية تشير إلى golang.org وليس github.com) ، لكنها لا تعمل أيضًا ، ليس على نظام MacOS ، ربما على Linux.
  • أيضًا //github.com/randall77/corelib فشل في نظام التشغيل MacOS

pprof UI

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

go tool pprof -http=:8080 heap.out

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

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

استنتاج

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

بعض القراءات الجيدة الأخرى:

  • //rakyll.org/archive/ - أعتقد أن هذا أحد المساهمين في متابعة الأداء ، والكثير من المشاركات الجيدة في مدونتها
  • //github.com/google/gops - كتبها JBD (الذي يدير موقع rakyll.org) ، تضمن هذه الأداة مشاركة المدونة الخاصة بها.
  • //medium.com/@cep21/using-go-1-10-new-trace-features-to-debug-an-integration-test-1dc39e4e812d - go tool traceوالتي تدور حول تشكيل وحدة المعالجة المركزية ، هذه مشاركة رائعة حول ميزة التنميط .