هذا المقال يتطلب مستوى: متوسط
لو Node أو Nginx بتاعك بدأ فجأة يرمي EMFILE: too many open files والاتصالات بترفض، والـ CPU والـ RAM لسه مرتاحين، المشكلة مش في الكود ولا في السيرفر. انت خلّصت واصفات الملفات المسموح بيها للعملية. المقال ده هيوريك تكتشفها بأمر واحد وتصلّحها صح من غير ما تخبّي مشكلة أكبر.
لما السيرفر يقول Too many open files وهو مرتاح
المشكلة باختصار
كل عملية في لينكس ليها سقف لعدد الملفات اللي تقدر تفتحها في نفس اللحظة. والمصيبة إن "الملف" هنا مش بس ملف على الديسك. أي اتصال شبكة، أي socket، أي pipe، وحتى الـ log اللي بتكتب فيه، كله بيحجز واصف ملف واحد. السقف الافتراضي على أغلب التوزيعات هو 1024. يعني تطبيق بيستقبل اتصالات كتير ممكن يضرب في السقف ده وهو لسه بيستهلك 40% من الميموري بس.
يعني إيه "واصف ملف" أصلًا؟
تخيّل معايا لوحة مفاتيح في ريسبشن فندق فيها 1024 خطّاف. كل ضيف بيدخل بياخد مفتاح من خطّاف فاضي. طول ما فيه خطاطيف فاضية، أي ضيف جديد ياخد مفتاح ويدخل عادي. أول ما آخر خطّاف يتشغل، الضيف اللي بعده بيقف على الباب مهما كان الفندق نفسه مليان بالغرف الفاضية. المشكلة مش في عدد الغرف، المشكلة في عدد الخطاطيف.
ده بالظبط اللي بيحصل مع واصفات الملفات. الضيف = اتصال أو ملف مفتوح، والخطّاف = واصف الملف، ولوحة الخطاطيف = حد الـ nofile بتاع العملية.
بالتعريف الدقيق: واصف الملف (file descriptor) هو رقم صحيح صغير بيرجّعه الكيرنل للعملية لما تفتح ملف أو socket. الرقم ده مؤشّر لمدخل في جدول الملفات المفتوحة الخاص بالعملية. الكيرنل بيحدّ حجم الجدول ده عبر RLIMIT_NOFILE، وله قيمتان: soft limit (السقف الفعّال اللي بتصطدم بيه) وhard limit (أقصى قيمة تقدر ترفع لها الـ soft من غير صلاحيات root).
سيناريو واقعي
عندك سيرفر WebSocket للإشعارات اللحظية، كل مستخدم متصل بيفتح اتصال دائم واحد. الاتصال ده = واصف ملف. لو الـ soft limit عندك 1024، أول ما توصل لحوالي 1000 مستخدم متزامن (فيه واصفات محجوزة أصلًا لـ stdin وstdout والـ logs وملفات أخرى)، السيرفر بيبدأ يرمي EMFILE ويرفض أي مستخدم جديد. المستخدمين المتصلين خلاص شغّالين، بس اللي بيحاول يدخل بعد كده بياخد فشل اتصال. النتيجة: شكاوى عشوائية "مش بيفتح معايا" بينما الـ dashboard بيقول إن السيرفر مرتاح.
اكتشفها في 30 ثانية
شوف الحد الحالي للعملية نفسها مش للـ shell بتاعك، لأنهم ممكن يكونوا مختلفين:
PID=$(pgrep -o nginx)
cat /proc/$PID/limits | grep 'open files'
ls /proc/$PID/fd | wc -l
cat /proc/sys/fs/file-nr
لو cat /proc/$PID/limits رجّعلك 1024 في عمود الـ Soft Limit، وعدد الواصفات المفتوحة قرّب منه، ده تأكيد إن ده هو الخنّاق.
صلّحها صح
الحل الصحيح بيتحط على مستوى الخدمة نفسها، مش بالتلاعب في الـ shell. لو بتشغّل التطبيق كخدمة systemd (وده الأغلب في الإنتاج)، ضبط ulimit في الترمينال مش هيأثّر على الخدمة خالص. افتح ملف الخدمة بـ systemctl edit yourapp وضيف تحت [Service] السطر LimitNOFILE=65535، بعدين:
sudo systemctl daemon-reload
sudo systemctl restart yourapp
cat /proc/$(pgrep -o yourapp)/limits | grep 'open files'
لو التطبيق بيشتغل من جلسة دخول عادية (مش systemd)، الحد بيتحط في /etc/security/limits.conf عبر pam_limits بإضافة سطرين: * soft nofile 65535 و * hard nofile 65535. وفيه سقف على مستوى النظام كله اسمه fs.file-max نادرًا بيكون هو المشكلة (قيمته الافتراضية عالية جدًا وبتُحسب من حجم الذاكرة)، لكن تأكد إنه كفاية عبر cat /proc/sys/fs/file-max. ولو Nginx تحديدًا، ضيف worker_rlimit_nofile 65535; عشان يتزامن مع حد الخدمة.
الـ trade-off اللي محدش بيقوله
رفع الحد مش مجاني، بس تكلفته أقل مما الناس بتتخيّل. كل واصف ملف في حد ذاته بياخد جزء بسيط جدًا من ذاكرة الكيرنل. المشكلة الحقيقية مش في الواصف، لكن في اللي مربوط بيه: كل اتصال TCP مفتوح ليه buffers للإرسال والاستقبال بتاخد من عشرات لمئات الكيلوبايت. يعني LimitNOFILE=65535 لوحده تكلفته مهملة، لكن 65 ألف اتصال فعلي شغّال في نفس اللحظة ممكن ياكلوا جيجابايتات. بتكسب: سعة اتصالات أعلى ووقفة أخطاء الـ EMFILE. بتخسر: احتمال إنك تخبّي مشكلة أخطر لحد ما الميموري تخلص بدل ما الواصفات تخلص. الافتراض هنا إن سيرفرك عنده ذاكرة تكفي الحمل المتوقع.
متى الرقم الكبير مش هو الحل
لو رفعت الحد لـ 65535 ورجعت المشكلة تاني بعد يوم أو يومين، انت مش قدام مشكلة سقف، انت قدام تسريب واصفات (fd leak). ده بيحصل لما الكود بيفتح اتصالات أو ملفات وبينساش يقفلها. راقب العدّاد مع الوقت عبر watch -n 5 "ls /proc/PID/fd | wc -l". لو الرقم بيطلع بشكل مطّرد ومبيرجعش ينزل أبدًا، ده تسريب، ورفع الحد بيأجّل الانهيار بس. الحل الفعلي إنك تصلّح الكود يقفل اللي بيفتحه (اقفل الـ connections، استخدم connection pooling، وتأكد إن كل open ليه close مقابل حتى وقت الأخطاء).
الخطوة التالية
دلوقتي فورًا، شغّل الأمر ده على أهم خدمة عندك في الإنتاج: cat /proc/$(pgrep -o اسم_خدمتك)/limits | grep 'open files'. لو لقيت 1024 في عمود الـ Soft Limit، انت على بُعد إعداد واحد من مشكلة إنتاج محتملة. ضيف LimitNOFILE للخدمة، أعد تشغيلها، وراقب عدّاد الواصفات لمدة يوم عشان تتأكد إنه ثابت مش بيتسلّق. لو بيتسلّق، دوّر على تسريب في الكود قبل ما ترفع أي أرقام تانية.
المصادر
- Linux man-pages:
getrlimit(2)/setrlimit(2)— تعريفRLIMIT_NOFILEوالفرق بين soft وhard limit. - Linux Kernel Documentation —
admin-guide/sysctl/fs.rst: شرحfs.file-maxوfs.file-nr. - man-pages:
proc(5)— توثيق/proc/[pid]/limitsو/proc/[pid]/fd. - systemd docs:
systemd.exec(5)— توجيهLimitNOFILE. - man-pages:
limits.conf(5)وpam_limits(8)— ضبط حدود جلسات الدخول. - Nginx documentation — Core module: توجيه
worker_rlimit_nofile.