دليل عملي للتعامل مع تسرب الذاكرة في Node.js

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





ما هو تسرب الذاكرة؟


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

إدارة الذاكرة


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

  • . . . , . C C++. , , malloc free, .
  • . , , , . , , , . , , , , . . — JavaScript, , JVM (Java, Scala, Kotlin), Golang, Python, Ruby .
  • تطبيق مفهوم ملكية الذاكرة. مع هذا النهج ، يجب أن يكون لكل متغير مالكه الخاص. بمجرد أن يكون المالك خارج النطاق ، يتم تدمير القيمة في المتغير ، وتحرير الذاكرة. يتم استخدام هذه الفكرة في الصدأ.

هناك طرق أخرى لإدارة الذاكرة تستخدم في لغات البرمجة المختلفة. على سبيل المثال ، يستخدم C ++ 11 لغة RAII ، بينما يستخدم Swift آلية ARC . لكن الحديث عنها يتجاوز نطاق هذه المقالة. لمقارنة الأساليب المذكورة أعلاه لإدارة الذاكرة ، لفهم إيجابياتها وسلبياتها ، نحتاج إلى مقالة منفصلة.

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

جمع القمامة جافا سكريبت


كما سبق ذكره ، فإن JavaScript هي لغة تستخدم مفهوم جمع القمامة. أثناء تشغيل برامج JS ، يتم إطلاق آلية تسمى جامع القمامة بشكل دوري. يكتشف أي أجزاء من الذاكرة المخصصة يمكن الوصول إليها من رمز التطبيق. أي المتغيرات التي يتم الرجوع إليها. إذا اكتشف جامع القمامة أن جزءًا من الذاكرة لم يعد يتم الوصول إليه من رمز التطبيق ، فإنه يحرر هذه الذاكرة. يمكن تنفيذ النهج أعلاه باستخدام خوارزميتين رئيسيتين. الأول هو ما يسمى بخوارزمية Mark and Sweep. يتم استخدامه في JavaScript. والثاني هو العد المرجعي. يتم استخدامه في Python و PHP.


علامة المراحل (وضع العلامات) والاكتساح (تنظيف)

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

تسرب الذاكرة في تطبيقات Node.js


حتى الآن ، قمنا بتحليل ما يكفي من المفاهيم النظرية المتعلقة بتسريبات الذاكرة وجمع القمامة. لذلك - نحن على استعداد للنظر في كيف يبدو كل ذلك في التطبيقات الحقيقية. في هذا القسم ، سنكتب خادم Node.js به تسرب للذاكرة. سنحاول تحديد هذا التسرب باستخدام أدوات مختلفة ، ثم سنزيله.

▍ الإلمام بكود يحتوي على تسرب للذاكرة


لأغراض العرض التوضيحي ، كتبت خادم Express يحتوي على مسار تسرب للذاكرة. سنقوم بتصحيح هذا الخادم.

const express = require('express')

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

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

leakاختراق الذاكرة


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

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

▍ كومة تفريغ


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

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

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

قم بتثبيت الحزمة:

npm i heapdump

سنقوم بإجراء بعض التغييرات على رمز الخادم مما يتيح لنا استخدام هذه الحزمة:

const express = require('express');
const heapdump = require("heapdump");

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.get('/heapdump', (req, res) => {
  heapdump.writeSnapshot(`heapDump-${Date.now()}.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a bloated server written to", filename);

    res.status(200).send({msg: "successfully took a heap dump"})
  });
});

app.listen(port, () => {
  heapdump.writeSnapshot(`heapDumpAtServerStart.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a fresh server written to", filename);
  });
});

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

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

detection كشف تسرب الذاكرة


والآن ، يتم نشر الخادم. لقد كان يعمل لعدة أيام. يتلقى الكثير من الطلبات (في حالتنا ، الطلبات من نفس النوع فقط) وانتبهنا إلى زيادة حجم الذاكرة التي يستهلكها الخادم. يمكن الكشف عن تسرب للذاكرة باستخدام أدوات المراقبة مثل Express Status Monitor و Clinic و Prometheus . بعد ذلك ، نسمي API لتفريغ كومة الذاكرة المؤقتة. سيحتوي هذا التفريغ على جميع الكائنات التي تعذر على جامع البيانات المهملة حذفها.

إليك ما يبدو عليه الاستعلام لإنشاء تفريغ:

curl --location --request GET 'http://localhost:3000/heapdump'

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

بعد أن يكون لدينا كل من المقالب تحت تصرفنا (تفريغ خادم تم إطلاقه حديثًا وتفريغ خادم يعمل لبعض الوقت) ، يمكننا البدء في مقارنتهما.

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

قم بتشغيل Chrome واضغط على المفتاح.F12. سيؤدي ذلك إلى اكتشاف أدوات المطورين. هنا تحتاج إلى الانتقال إلى علامة التبويب Memoryوتحميل كل من لقطات الذاكرة.


تحميل الذاكرة مقالب على علامة التبويب الذاكرة من أدوات المطورين كروم

بعد تحميل كل من لقطات، تحتاج إلى تغييرperspectiveإلىComparisonوانقر على لقطة من ذاكرة الخادم الذي يعمل لبعض الوقت.


ابدأ في مقارنة اللقطات

هنا يمكننا تحليل العمودConstructorوالبحث عن الكائنات التي لا يستطيع جامع القمامة إزالتها. سيتم تمثيل معظم هذه الكائنات بروابط داخلية تستخدمها العقد. هنا من المفيد استخدام خدعة واحدة ، والتي تتكون من فرز القائمة حسب الحقلAlloc. Size. سيؤدي هذا إلى العثور بسرعة على الكائنات التي تستخدم أكبر قدر من الذاكرة. إذا قمت بتوسيع الكتلة(array)، ثم -(object elements)، يمكنك رؤية صفيفleaksيحتوي على عدد كبير من الكائنات التي لا يمكن حذفها باستخدام جامع القمامة.


تحليل مصفوفة مشبوهة

تسمح لنا هذه التقنية بالذهاب إلى الصفيفleaksوفهم أن العملية غير الصحيحة معه هي التي تسبب تسرب للذاكرة.

▍إصلاح تسرب للذاكرة


الآن بعد أن علمنا أن "الجاني" هو مصفوفة leaks، يمكننا تحليل الشفرة ومعرفة أن المشكلة هي أنه تم التصريح عن المصفوفة خارج معالج الطلبات. ونتيجة لذلك ، اتضح أن الرابط إليه لا يتم حذفه أبدًا. لإصلاح هذه المشكلة أمر بسيط للغاية - فقط قم بنقل إعلان الصفيف إلى المعالج:

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  const leaks = [];

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

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

ملخص


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

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

القراء الأعزاء! هل واجهت تسرب للذاكرة في مشاريع الويب الخاصة بك؟


All Articles