almessadi.
العودة إلى الفهرس

Node.js OOMKilled: كيفية اكتشاف مشكلة الذاكرة الحقيقية_

ما الذي يتسبب فعليًا في تعرض خدمات Node.js لـ OOMKilled، كيف يمكن فحص الكومة، ومتى يساعد رفع حد الذاكرة مقابل إخفاء التسريب.

تاريخ النشر12 مارس 2024
وقت القراءة14 min read

عندما تتعرض خدمة Node.js لـ OOMKilled في Kubernetes، فإن السبب الجذري ليس دائمًا "الحاوية تحتاج المزيد من الذاكرة" وليس دائمًا "V8 تسربت".

لديك ثلاثة أجزاء متحركة على الأقل:

  • كومة V8
  • الذاكرة خارج كومة V8، مثل المخازن الأصلية
  • حد الذاكرة في الحاوية المفروض من قبل وقت التشغيل

إذا لم تفصل بين هذه الأمور، يمكنك أن تقضي أيامًا في إصلاح المشكلة الخاطئة.

ابدأ مع النموذج العقلي الصحيح

process.memoryUsage() يُظهر عدة أقسام من الذاكرة:

const usage = process.memoryUsage();

console.log({
  rss: usage.rss,
  heapTotal: usage.heapTotal,
  heapUsed: usage.heapUsed,
  external: usage.external,
  arrayBuffers: usage.arrayBuffers,
});

التمييز المهم هو:

  • heapUsed هي كائنات JavaScript التي تديرها V8
  • external و arrayBuffers غالبًا ما تمثل الذاكرة التي لا تزال تُحتسب ضد حد الحاوية
  • rss هو مجموعة العملية المقيمة وعادة ما يكون الرقم الذي يهتم به فرق العمليات أثناء الحوادث

يمكن أن يكون لديك كومة تبدو صحية وما زال يتم إيقافك لأن الذاكرة الأصلية أو المخازن تستمر في النمو.

--max-old-space-size هو أداة، وليس تشخيصًا

هذه العلامة ترفع من حجم كومة الجيل القديم لـ V8:

node --max-old-space-size=4096 server.js

قد تكون هذه هي الحل الصحيح عندما:

  • تحتاج الخدمة بشكل مشروع إلى المزيد من الكومة الحية
  • يجري جمع القمامة بشكل صحي
  • حد الحاوية لديه مساحة كافية

لكنها الحل الخطأ عندما:

  • يكون تسريب الذاكرة في كائنات المستخدم التي ينبغي أن تكون قابلة للجمع
  • ينمو الذاكرة في المخازن أو الإضافات الأصلية
  • تكون الحاوية قريبة جدًا من حد الحاوية

رفع حد الكومة على عملية تسريب الذاكرة فقط يؤخر الانهيار.

الأسباب الشائعة لنمو الذاكرة

في خدمات Node الإنتاجية، عادةً ما أتحقق من هذه أولاً:

  1. الخرائط أو المخازن طويلة الأمد بدون إخلاء
  2. مستمعي الأحداث المرفقين بشكل متكرر والذين لا يتم إزالتهم أبدًا
  3. الطوابير التي تقبل العمل أسرع مما يمكن للعمال تفريغه
  4. أحمال JSON كبيرة مخزنة بالكامل في الذاكرة
  5. تدفقات تم استبدالها بـ await response.json()
  6. الإضافات الأصلية أو مكتبات معالجة الصور/الفيديو التي تستهلك الذاكرة خارج الكومة

لا شيء من هذه الأمور غريب. إنها أخطاء هندسية طبيعية تحت الضغط.

لقطات الكومة تستحق الاحتكاك

إذا كان الشك يتعلق بنمو الكومة، خذ لقطة وافحص الحجم المحتجز:

import { writeHeapSnapshot } from "node:v8";

const filename = writeHeapSnapshot();
console.log(`تم كتابة لقطة الكومة إلى ${filename}`);

ثم افتح اللقطة في أدوات مطور Chrome وابحث عن:

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

السؤال ليس "أي كائن كبير؟" بل "لماذا لا يزال هذا الكائن قابلاً للوصول؟"

التسريبات غالبًا ما تختبئ في رمز الراحة

هذا النمط أكثر خطورة مما يبدو:

const pending = new Map<string, RequestContext>();

export function trackRequest(id: string, ctx: RequestContext) {
  pending.set(id, ctx);
}

بدون مسار حذف واضح، تصبح تلك الخريطة قاعدة بيانات غير مقصودة في الذاكرة.

الحل عادةً ليس ذكيًا. إنه انضباط دورة الحياة:

  • إزالة الإدخالات عند اكتمال العمل
  • حصر المخازن
  • تدفق الأحمال الكبيرة
  • تفضيل الضغط العكسي على تخزين كل شيء

الحاويات تغير وضع الفشل

داخل Kubernetes، تتنافس العملية مع حد الحاوية، وليس فقط مع إعدادات V8 الافتراضية.

وهذا يعني:

  • راقب rss، وليس فقط الكومة
  • اترك مساحة للتهيئة الأصلية
  • تجنب ضبط --max-old-space-size بالقرب من حد الحاوية

عملية لديها كومة بحجم 4 جيجابايت في حاوية بحجم 4 جيجابايت ليست "فعالة". إنها هشة.

حل واقعي للحوادث

عندما تبدأ خدمة Node في التعرض لـ OOMKilled:

  1. قم برسم rss و heapUsed وحجم الطلب معًا
  2. تحقق مما إذا كان النمو يعيد الضبط بعد انخفاض الحركة
  3. افحص مسارات الكود التي تستخدم المخازن كثيرًا والتعامل مع الأحمال الكبيرة
  4. التقط لقطة كومة إذا كان نمو الكومة يبدو مشبوهًا
  5. فقط بعد ذلك قرر ما إذا كان ضبط الكومة مبررًا

هذا الترتيب مهم. غالبًا ما يؤدي الضبط قبل الفهم إلى حادث أبطأ، وليس نظامًا أفضل.

قراءات إضافية