عند تشغيل تطبيقات Node.js في حاويات Docker ، لا تعمل إعدادات الذاكرة التقليدية دائمًا كما هو متوقع. إن المادة ، التي ننشر ترجمتها اليوم ، مكرسة للعثور على إجابة عن السؤال حول سبب ذلك. كما سيقدم توصيات عملية لإدارة الذاكرة المتاحة لتطبيقات Node.js التي تعمل في حاويات.

مراجعة التوصيات
افترض أن تطبيق Node.js يعمل في حاوية ذات حد ذاكرة محدد. إذا كنا نتحدث عن Docker ، فإن الخيار -
--memory
يمكن أن يستخدم لتعيين هذا الحد. شيء مماثل ممكن عند العمل مع أنظمة orchestration حاوية. في هذه الحالة ، يوصى عند بدء تشغيل تطبيق Node.js ، باستخدام خيار
--max-old-space-size
. يسمح لك هذا بإبلاغ النظام الأساسي عن مقدار الذاكرة المتوفرة له ، وكذلك مراعاة حقيقة أن هذا المبلغ يجب أن يكون أقل من الحد المحدد على مستوى الحاوية.
عند تشغيل تطبيق Node.js داخل الحاوية ، قم بتعيين سعة الذاكرة المتاحة له وفقًا لقيمة الذروة لاستخدام الذاكرة النشطة بواسطة التطبيق. يتم ذلك إذا كان يمكن تكوين حدود ذاكرة الحاوية.
الآن دعنا نتحدث عن مشكلة استخدام الذاكرة في الحاويات بمزيد من التفاصيل.
حد ذاكرة عامل الميناء
بشكل افتراضي ، لا تحتوي الحاويات على حدود للموارد ويمكنها استخدام قدر الذاكرة الذي يسمح به نظام التشغيل. يحتوي الأمر
docker run
على خيارات لسطر الأوامر تتيح لك تعيين حدود فيما يتعلق باستخدام الذاكرة أو موارد المعالج.
قد يبدو أمر بدء تشغيل الحاوية كما يلي:
docker run --memory <x><y> --interactive --tty <imagename> bash
يرجى ملاحظة ما يلي:
x
هي الحد الأقصى لمقدار الذاكرة المتاح للحاوية ، معبراً عنه بوحدات y
.- يمكن أن تأخذ
y
القيمة b
(بايت) ، k
(كيلو بايت) ، m
(ميغابايت) ، g
(غيغا بايت).
فيما يلي مثال لأمر إطلاق الحاوية:
docker run --memory 1000000b --interactive --tty <imagename> bash
هنا ، يتم تعيين حد الذاكرة إلى
1000000
بايت.
للتحقق من حد الذاكرة المعين على مستوى الحاوية ، يمكنك ، في الحاوية ، تشغيل الأمر التالي:
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
دعنا نتحدث عن سلوك النظام عند تحديد حد ذاكرة التطبيق Node.js باستخدام مفتاح
--max-old-space-size
. في هذه الحالة ، سيتوافق حد الذاكرة هذا مع الحد المعين على مستوى الحاوية.
ما يسمى "الفضاء القديم" في اسم المفتاح هو واحد من شظايا الكومة التي تسيطر عليها V8 (المكان الذي يتم وضع كائنات JavaScript "القديمة"). هذا المفتاح ، إذا لم تدخل في التفاصيل التي نلمسها أدناه ، يتحكم في الحد الأقصى لحجم الكومة. يمكن العثور على تفاصيل مفاتيح تبديل سطر الأوامر Node.js
هنا .
بشكل عام ، عندما يحاول تطبيق ما استخدام ذاكرة أكبر مما هو متاح في الحاوية ، يتم إنهاء تشغيله.
في المثال التالي (يسمى ملف التطبيق
test-fatal-error.js
) ، يتم وضع كائنات
MyRecord
في صفيف
list
، مع فاصل زمني قدره 10 ميلي ثانية. هذا يؤدي إلى نمو كومة الذاكرة المؤقتة غير المنضبط ، محاكاة تسرب ذاكرة.
'use strict'; const list = []; setInterval(()=> { const record = new MyRecord(); list.push(record); },10); function MyRecord() { var x='hii'; this.name = x.repeat(10000000); this.id = x.repeat(10000000); this.account = x.repeat(10000000); } setInterval(()=> { console.log(process.memoryUsage()) },100);
يرجى ملاحظة أن جميع أمثلة البرامج التي سنناقشها هنا موضوعة في صورة Docker ، والتي يمكن تنزيلها من Docker Hub:
docker pull ravali1906/dockermemory
يمكنك استخدام هذه الصورة للتجارب المستقلة.
بالإضافة إلى ذلك ، يمكنك حزم التطبيق في حاوية Docker ، وجمع الصورة وتشغيلها مع حد الذاكرة:
docker run --memory 512m --interactive --tty ravali1906/dockermemory bash
هنا
ravali1906/dockermemory
هو اسم الصورة.
يمكنك الآن بدء تشغيل التطبيق عن طريق تحديد حد ذاكرة له يتجاوز حد الحاوية:
$ node --max_old_space_size=1024 test-fatal-error.js { rss: 550498304, heapTotal: 1090719744, heapUsed: 1030627104, external: 8272 } Killed
هنا ، يمثل
--max_old_space_size
حد الذاكرة المشار إليه بالميغابايت. يوفر الأسلوب
process.memoryUsage()
معلومات حول استخدام الذاكرة. يتم التعبير عن القيم بالبايت.
التطبيق في وقت ما يتم إنهاء قسرا. يحدث هذا عندما يعبر مقدار الذاكرة المستخدم به حد معين. ما هذه الحدود؟ ما هي القيود المفروضة على مقدار الذاكرة التي يمكن أن نتحدث عنها؟
السلوك المتوقع للتطبيق الذي يتم تشغيله باستخدام المفتاح هو - الحجم الأقصى للمساحة القديمة
بشكل افتراضي ، يبلغ الحد الأقصى لحجم الكومة في Node.js (حتى الإصدار 11.x) 700 ميغابايت على الأنظمة الأساسية 32 بت و 1400 ميغابايت على الأنظمة 64 بت. يمكنك أن تقرأ عن تعيين هذه القيم
هنا .
من الناحية النظرية ، إذا استخدمت مفتاح
--max-old-space-size
حد للذاكرة يتجاوز حد ذاكرة الحاوية ، فيمكنك توقع إنهاء التطبيق بواسطة آلية أمان kernel kernel لنظام Linux OOM Killer.
في الواقع ، هذا قد لا يحدث.
السلوك الفعلي للتطبيق الذي يعمل باستخدام المفتاح هو الحجم الأقصى للمساحة القديمة
لا يقوم التطبيق ، بعد بدء التشغيل مباشرة ، بتخصيص كل الذاكرة التي تم تحديد
--max-old-space-size
باستخدام
--max-old-space-size
. يعتمد حجم كومة JavaScript على احتياجات التطبيق. يمكنك تحديد مقدار الذاكرة التي يستخدمها التطبيق بناءً على قيمة الحقل
heapUsed
من الكائن الذي تم إرجاعه بواسطة الأسلوب
process.memoryUsage()
. في الواقع ، نحن نتحدث عن الذاكرة المخصصة في كومة الذاكرة المؤقتة للكائنات.
نتيجةً لذلك ، نستنتج أنه سيتم إنهاء التطبيق بالقوة إذا كان حجم الكومة أكبر من الحد
--memory
بواسطة مفتاح
--memory
عند بدء تشغيل الحاوية.
ولكن في الواقع هذا قد لا يحدث أيضا.
عند إنشاء ملفات تعريف للتطبيقات كثيفة الاستخدام للموارد Node.js التي تعمل في حاويات بحد معين من الذاكرة ، يمكن ملاحظة الأنماط التالية:
- يتم تشغيل OOM Killer بعد وقت
heapTotal
من اللحظة التي تكون فيها heapUsed
و heapUsed
أعلى بكثير من حدود الذاكرة. - لا يستجيب برنامج OOM Killer لتجاوز الحدود.
شرح لسلوك تطبيقات Node.js في الحاويات
تشرف الحاوية على أحد المؤشرات المهمة للتطبيقات التي تعمل عليها. هذا هو
RSS (حجم مجموعة المقيمين). يمثل هذا المؤشر جزءًا معينًا من الذاكرة الافتراضية للتطبيق.
علاوة على ذلك ، إنها جزء من الذاكرة المخصصة للتطبيق.
لكن هذا ليس كل شيء. آر إس إس هي جزء من الذاكرة النشطة المخصصة للتطبيق.
قد لا تكون جميع الذاكرة المخصصة لتطبيق ما نشطة. والحقيقة هي أن "الذاكرة المخصصة" لا يتم تخصيصها فعليًا بالضرورة حتى تبدأ العملية بالفعل في استخدامها. بالإضافة إلى ذلك ، استجابة لطلبات تخصيص الذاكرة من العمليات الأخرى ، يمكن لنظام التشغيل تفريغ الأجزاء غير النشطة من ذاكرة التطبيق إلى ملف الصفحة ونقل المساحة المحررة إلى عمليات أخرى. وعندما يحتاج التطبيق مرة أخرى إلى هذه الأجزاء من الذاكرة ، سيتم نقلها من ملف المبادلة وإعادتها إلى الذاكرة الفعلية.
يشير مقياس RSS إلى مقدار الذاكرة النشطة والمتاحة للتطبيق في مساحة عنوانه. هو الذي يؤثر على قرار الإغلاق القسري للتطبيق.
دليل
▍ مثال رقم 1. تطبيق يخصص الذاكرة لمخزن مؤقت
المثال التالي ،
buffer_example.js
، يُظهر برنامجًا يخصص ذاكرة
buffer_example.js
مؤقت:
const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024) console.log(Math.round(buf.length / (1024 * 1024))) console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))
لكي تتجاوز مساحة الذاكرة المخصصة بواسطة البرنامج الحد المعين عند بدء تشغيل الحاوية ، قم أولاً بتشغيل الحاوية باستخدام الأمر التالي:
docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash
بعد ذلك ، قم بتشغيل البرنامج:
$ node buffer_example 2000 2000 16
كما ترون ، فإن النظام لم يكمل البرنامج ، على الرغم من أن الذاكرة المخصصة من قبل البرنامج تتجاوز حد الحاوية. حدث هذا بسبب حقيقة أن البرنامج لا يعمل مع جميع الذاكرة المخصصة. RSS صغير جدًا ، ولا يتجاوز حد ذاكرة الحاوية.
▍ مثال رقم 2. تطبيق ملء المخزن المؤقت مع البيانات
في المثال التالي ،
buffer_example_fill.js
، لا يتم تخصيص الذاكرة فقط ، ولكن أيضًا مليئة بالبيانات:
const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024,'x') console.log(Math.round(buf.length / (1024 * 1024))) console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))
تشغيل الحاوية:
docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash
بعد ذلك ، قم بتشغيل التطبيق:
$ node buffer_example_fill.js 2000 2000 984
على ما يبدو ، حتى الآن لا ينتهي التطبيق! لماذا؟ الحقيقة هي أنه عندما تصل كمية الذاكرة النشطة إلى الحد المحدد عند بدء تشغيل الحاوية ، وهناك مساحة في ملف الصفحة ، يتم نقل بعض الصفحات القديمة في ذاكرة العملية إلى ملف الصفحة. يتم توفير الذاكرة التي تم إصدارها لنفس العملية. بشكل افتراضي ، يخصص Docker مساحة لملف المبادلة مساوٍ لحد الذاكرة المحدد باستخدام علامة
--memory
. بالنظر إلى هذا ، يمكننا القول أن العملية بها 2 غيغابايت من الذاكرة - 1 غيغابايت في الذاكرة النشطة ، و 1 غيغابايت في ملف الصفحة. أي أنه نظرًا لحقيقة أن التطبيق يمكنه استخدام ذاكرته الخاصة ، والتي يتم نقل محتوياتها مؤقتًا إلى ملف الصفحة ، يكون حجم فهرس RSS في حدود الحاوية. نتيجة لذلك ، يواصل التطبيق العمل.
▍ مثال رقم 3. تطبيق يملأ مخزنًا مؤقتًا ببيانات تعمل في حاوية لا تستخدم ملف صفحة
إليك الرمز الذي
buffer_example_fill.js
هنا (هذا هو نفس ملف
buffer_example_fill.js
):
const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024,'x') console.log(Math.round(buf.length / (1024 * 1024))) console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))
هذه المرة ، قم بتشغيل الحاوية ، مع إعداد ميزات العمل مع ملف المبادلة بشكل صريح:
docker run --memory 1024m --memory-swap=1024m --memory-swappiness=0 --interactive --tty ravali1906/dockermemory bash
قم بتشغيل التطبيق:
$ node buffer_example_fill.js 2000 Killed
رؤية الرسالة
Killed
؟ عندما تكون قيمة مفتاح
--memory-swap
مساوية
--memory
مفتاح
--memory
، فإن هذا يخبر الحاوية بأنه لا ينبغي لها استخدام ملف
--memory
. بالإضافة إلى ذلك ، بشكل افتراضي ، يمكن لنواة نظام التشغيل الذي تعمل فيه الحاوية نفسها أن تتخلص من قدر معين من صفحات الذاكرة المجهولة التي تستخدمها الحاوية في ملف الصفحة.
--memory-swappiness
على
0
، نقوم بتعطيل هذه الميزة. نتيجة لذلك ، اتضح أن ملف الترحيل غير مستخدم داخل الحاوية. تنتهي العملية عندما يتجاوز قياس RSS حد ذاكرة الحاوية.
توصيات عامة
عندما يتم تشغيل تطبيقات Node.js باستخدام
--max-old-space-size
، الذي تتجاوز قيمته حد الذاكرة المحدد عند بدء تشغيل الحاوية ، فقد يبدو أن Node.js "لا ينتبه" إلى حد الحاوية. ولكن ، كما يتضح من الأمثلة السابقة ، فإن السبب الواضح لهذا السلوك هو حقيقة أن التطبيق ببساطة لا يستخدم وحدة تخزين الكومة بأكملها المحددة مع
--max-old-space-size
.
تذكر أن التطبيق لن يتصرف دائمًا بنفس الطريقة إذا كان يستخدم ذاكرة أكثر مما هو متاح في الحاوية. لماذا؟ الحقيقة هي أن الذاكرة النشطة للعملية (RSS) تتأثر بالعديد من العوامل الخارجية التي لا يمكن للتطبيق نفسه التأثير عليها. يعتمدون على الحمل على النظام وعلى خصائص البيئة. على سبيل المثال ، هذه هي ميزات التطبيق نفسه ، ومستوى التوازي في النظام ، وميزات جدولة نظام التشغيل ، وميزات أداة تجميع مجمعي البيانات المهملة ، وما إلى ذلك. بالإضافة إلى ذلك ، قد تتغير هذه العوامل ، من الإطلاق إلى الإطلاق.
توصيات حول تعيين حجم كومة Node.js لتلك الحالات عندما يمكنك التحكم في هذا الخيار ، ولكن ليس مع قيود الذاكرة على مستوى الحاوية
- قم بتشغيل تطبيق Node.js الأدنى في الحاوية وقياس حجم RSS الثابت (في حالتي ، لـ Node.js 10.x ، هذا حوالي 20 ميغابايت).
- يحتوي كومة Node.js ليس فقط على old_space ، ولكن أيضًا على الآخرين (مثل new_space و code_space وما إلى ذلك). لذلك ، إذا أخذت في الاعتبار التكوين القياسي للنظام الأساسي ، فيجب عليك الاعتماد على حقيقة أن البرنامج سيحتاج إلى ذاكرة أكبر بنحو 20 ميجابايت. إذا تغيرت الإعدادات القياسية ، فيجب أن تؤخذ هذه التغييرات في الاعتبار أيضًا.
- نحتاج الآن إلى طرح القيمة التي تم الحصول عليها (افترض أنها ستكون 40 ميغابايت) من مقدار الذاكرة المتوفرة في الحاوية. ما تبقى هو القيمة التي ، دون خوف من نفاد
--max-old-space-size
البرنامج من نفاد الذاكرة ، يمكن تحديدها كقيمة رئيسية - --max-old-space-size
.
لا توجد توصيات بشأن تعيين حدود ذاكرة الحاوية للحالات التي يمكن فيها التحكم في هذه المعلمة ، لكن معلمات التطبيق Node.js ليست كذلك
- قم بتشغيل التطبيق في أوضاع تتيح لك معرفة قيم الذروة للذاكرة التي تستهلكها.
- تحليل النتيجة آر إس إس. على وجه الخصوص ، هنا ، إلى جانب طريقة
process.memoryUsage()
، قد يكون الأمر Linux top
في متناول يدي. - شريطة أن يتم استخدام القيمة التي تم الحصول عليها كحد أقصى لذاكرة الحاوية في الحاوية التي تم التخطيط لتشغيل التطبيق فيها ، لا شيء ولكن لن يتم تنفيذها. من أجل أن تكون آمنة ، فمن المستحسن زيادتها بنسبة 10 ٪ على الأقل.
النتائج
في Node.js 12.x ، يتم حل بعض المشكلات التي تمت مناقشتها هنا عن طريق ضبط حجم الكومة ، والذي يتم تنفيذه وفقًا لمقدار ذاكرة الوصول العشوائي المتاحة. تعمل هذه الآلية أيضًا عند تشغيل تطبيقات Node.js في حاويات. لكن الإعدادات قد تختلف عن الإعدادات الافتراضية. يحدث هذا ، على سبيل المثال ، عند
--max_old_space_size
مفتاح
--max_old_space_size
عند بدء تشغيل التطبيق. لمثل هذه الحالات ، كل ما سبق لا يزال مناسبا. هذا يشير إلى أن أي شخص يقوم بتشغيل تطبيقات Node.js في حاويات يجب أن يكون حذراً ومسؤولاً عن إعدادات الذاكرة. بالإضافة إلى ذلك ، فإن المعرفة بالحدود القياسية لاستخدام الذاكرة ، والتي تعتبر متحفظة إلى حد ما ، يمكن أن تحسن أداء التطبيق عن طريق تغيير هذه الحدود عن عمد.
أعزائي القراء! هل نفدت مشاكل الذاكرة عند تشغيل تطبيقات Node.js في حاويات Docker؟

