بلاگ/خطای Too Many Open Files در لینوکس: چرا رخ می‌دهد و چطور با ulimit، limits.conf و systemd آن را رفع کنیم؟
به‌روز شده

خطای Too Many Open Files در لینوکس: چرا رخ می‌دهد و چطور با ulimit، limits.conf و systemd آن را رفع کنیم؟

1405/08/080 بازدید
خطای Too Many Open Files در لینوکس: چرا رخ می‌دهد و چطور با ulimit، limits.conf و systemd آن را رفع کنیم؟

خطای Too Many Open Files در لینوکس: چرا رخ می‌دهد و چطور با ulimit، limits.conf و systemd آن را رفع کنیم؟

یک روز سرویس روی سرورتان بی‌خبر در لاگ می‌نویسد «too many open files» و دیتابیس یا وب‌سرور اتصال جدید قبول نمی‌کند. این خطا یعنی یک پردازه از سقف تعداد فایل‌های باز همزمان رد شده، سقفی که خود لینوکس برای هر پردازه و برای کل سیستم تعیین می‌کند. در این مقاله می‌بینیم این سقف از کجا می‌آید، چطور وضعیت فعلی را چک کنیم و چطور آن را هم موقت و هم برای همیشه بالا ببریم.

خطای Too Many Open Files دقیقا چیست؟

در لینوکس هر چیزی که یک پردازه باز می‌کند یک «فایل دیسکریپتور» (file descriptor) می‌گیرد: فایل روی دیسک، سوکت شبکه برای هر اتصال TCP، پایپ بین دو پردازه، حتی یک دستگاه ترمینال. هر کدام از این‌ها یک عدد کوچک در جدول فایل‌های باز آن پردازه اشغال می‌کند.

کرنل برای هر پردازه یک سقف روی تعداد این فایل‌های باز همزمان گذاشته. وقتی پردازه‌ای بخواهد از این سقف رد شود، سیستم‌عامل درخواست باز کردن فایل یا سوکت جدید را رد می‌کند و خطای EMFILE یا ENFILE برمی‌گرداند، که در لاگ نرم‌افزارهایی مثل nginx، MySQL یا Redis معمولا به شکل همین پیام «too many open files» دیده می‌شود.

این خطا از همان خانوادهٔ ارورهای ناگهانی لینوکس است که خطای OOM Killer در لینوکس و خطای No Space Left on Device در لینوکس هم در آن قرار می‌گیرند؛ هر سه از رسیدن به یک سقف سیستمی می‌آیند، فقط سقف‌شان فرق دارد — یکی فایل باز، یکی رم، یکی فضای دیسک.

نمودار سقف فایل‌های باز در لینوکس: رشد بی‌رویهٔ فایل دیسکریپتورهای باز که با ulimit، limits.conf و LimitNOFILE در systemd کنترل می‌شود

چرا این خطا رخ می‌دهد؟

چند دلیل رایج هست که معمولا ترکیبی از آن‌ها پشت این خطا قرار دارد:

  • پیش‌فرض توزیع پایین است. اکثر توزیع‌های لینوکس به‌صورت پیش‌فرض سقف نرم را روی ۱۰۲۴ می‌گذارند، عددی که برای یک وب‌سرور یا دیتابیس با ترافیک واقعی خیلی سریع پر می‌شود.
  • نشت فایل دیسکریپتور در خود برنامه. اگر کد شما فایل یا سوکت را باز می‌کند و فراموش می‌کند ببندد، تعداد فایل‌های باز هر روز کمی بیشتر می‌شود تا یک جا به سقف برخورد کند؛ این حالت را با افزایش ulimit نمی‌توان درست کرد، فقط دیرتر ظاهر می‌شود.
  • بار همزمان بالا. یک وب‌سرور که هزاران اتصال همزمان می‌پذیرد یا دیتابیسی با صدها کانکشن باز، به‌طور طبیعی به عددی بیشتر از ۱۰۲۴ نیاز دارد.
  • سقف سطح سیستم پر شده. علاوه بر سقف هر پردازه، یک سقف کلی هم روی کل سیستم وجود دارد (fs.file-max) که اگر همه پردازه‌ها روی هم آن را رد کنند، باز کردن فایل جدید برای هر پردازه‌ای شکست می‌خورد.

قبل از رفع، وضعیت فعلی را چک کنید

قبل از دستکاری تنظیمات، بد نیست ببینید الان کجا ایستاده‌اید:

# سقف نرم و سخت فعلی شل جاری
ulimit -n
ulimit -Hn

# سقف واقعی یک پردازه مشخص (مثلا nginx با PID 1234)
cat /proc/1234/limits | grep "Max open files"

# چند فایل الان باز است؟
lsof -p 1234 | wc -l

# سقف کلی سیستم و تعداد فایل باز فعلی کل سیستم
cat /proc/sys/fs/file-max
lsof | wc -l

اگر تعداد فایل باز یک پردازه با گذشت زمان مدام بالا می‌رود بدون اینکه بار واقعی‌اش زیاد شده باشد، احتمالا با نشت فایل دیسکریپتور طرف هستید، نه کمبود سقف.

رفع موقت با ulimit

سریع‌ترین راه برای تست این است که سقف شل فعلی را بالا ببرید:

ulimit -n 65535

این دستور فقط روی شل فعلی و پردازه‌هایی که از همین شل اجرا می‌شوند اثر دارد. بعد از بستن ترمینال یا ریستارت سرویس، به مقدار پیش‌فرض برمی‌گردد. برای تست سریع خوب است، برای رفع دائمی نه.

یک نکته: سقف سخت (hard limit) را یک کاربر عادی نمی‌تواند از مقداری که روت تعیین کرده بالاتر ببرد؛ فقط سقف نرم را تا سقف سخت می‌تواند تغییر دهد.

رفع دائمی برای کاربران و پردازه‌ها با limits.conf

برای اینکه این عدد بعد از هر لاگین یا ریبوت باقی بماند، باید آن را در /etc/security/limits.conf تعریف کنید (یا فایلی جدید در /etc/security/limits.d/، که تمیزتر است و با آپدیت پکیج‌ها هم تداخل نمی‌کند):

www-data soft nofile 65535
www-data hard nofile 65535
mysql    soft nofile 65535
mysql    hard nofile 65535

این تنظیم روی سشن‌های لاگینی اثر می‌گذارد که از ماژول PAM به نام pam_limits رد شوند؛ معمولا باید در /etc/pam.d/common-session (یا معادلش در توزیع شما) خط زیر وجود داشته باشد:

session required pam_limits.so

اگر سقف کلی سیستم هم کم است، در /etc/sysctl.conf یا یک فایل زیر /etc/sysctl.d/ اضافه کنید:

fs.file-max = 2097152

و با sysctl -p اعمالش کنید.

رفع برای سرویس‌های systemd با LimitNOFILE

نکته‌ای که خیلی‌ها اینجا گیر می‌کنند: در سرورهای فعلی اکثر سرویس‌ها (nginx، MySQL، PostgreSQL، Redis) را systemd مستقیم با PID 1 بالا می‌آورد، نه از یک لاگین شل. چون این سرویس‌ها اصلا از مسیری که PAM روی آن اثر می‌گذارد اجرا نمی‌شوند، هر چه در limits.conf بنویسید روی آن‌ها بی‌اثر می‌ماند. همین قاعده برای هر سرویسی که با systemd timer به‌جای cron زمان‌بندی می‌شود هم صادق است — تنظیم را باید روی unit خود سرویس بگذارید.

برای این سرویس‌ها باید سقف را مستقیم در unit file تعریف کنید:

systemctl edit nginx

و در فایل override که باز می‌شود:

[Service]
LimitNOFILE=65535

بعد:

systemctl daemon-reload
systemctl restart nginx

اگر می‌خواهید این سقف برای همه سرویس‌های مدیریت‌شده با systemd پیش‌فرض باشد، در /etc/systemd/system.conf می‌توانید بنویسید:

DefaultLimitNOFILE=65535

و بعد systemctl daemon-reexec بزنید تا روی systemd خودش هم اثر بگذارد.

برای اینکه ببینید تنظیم اعمال شده، دوباره cat /proc/<pid>/limits را روی پردازه سرویس چک کنید. اگر دنبال محدود کردن مصرف CPU و RAM همین سرویس هم هستید، همان الگوی unit file override را با systemd و cgroups می‌توانید برای آن‌ها هم به کار ببرید.

کدام روش را کجا استفاده کنیم؟

روشدامنه اثرماندگاریمناسب برای
ulimit -nفقط شل فعلی و فرزندهای آنتا بسته شدن شلتست سریع، دیباگ لحظه‌ای
limits.confکاربر یا گروه مشخص، در سشن‌های لاگینیدائمی، بعد از لاگین مجددپردازه‌هایی که از لاگین شل (cron، su، ssh) اجرا می‌شوند
LimitNOFILE در systemdیک سرویس مشخص یا همهٔ سرویس‌هادائمی، بعد از daemon-reloadسرویس‌هایی که systemd مستقیم اجرا می‌کند، یعنی اکثر دیتابیس‌ها و وب‌سرورها

چند نکته برای اینکه این خطا دوباره سر نزند

  • یک بار سقف را بالا بردن کافی نیست؛ اگر علت واقعی نشت فایل دیسکریپتور در کد است، فقط دیرتر دوباره به خطا می‌خورید. از lsof -p <pid> چند بار با فاصلهٔ چند دقیقه بگیرید و ببینید کدام فایل‌ها بدون بسته شدن مدام تکرار می‌شوند.
  • بعد از تغییر fs.file-max، آن را با sysctl -p یا ریبوت اعمال کنید، وگرنه فقط تا ریبوت بعدی تغییر نکرده می‌ماند.
  • اگر سروری که روی آن کار می‌کنید رم و منابعش هم محدود است، بالا بردن سقف فایل باز مشکل منابع واقعی را حل نمی‌کند، فقط اجازه می‌دهد پردازه‌ها بیشتر دوام بیاورند. برای تست همین تنظیمات بدون ریسک روی سرور اصلی، یک سرور مجازی لینوکس ساعتی آریانت گزینهٔ راحتی است: چند ساعت اجاره می‌کنید، limits.conf و unit فایل‌ها را امتحان می‌کنید، و اگر نتیجه خوب بود همان تنظیمات را روی سرور اصلی می‌گذارید.
  • برای سرویس‌هایی که با کانتینر اجرا می‌شوند، سقف فایل باز کانتینر از --ulimit nofile=65535:65535 در دستور docker run یا فیلد معادل در compose می‌آید، نه از limits.conf هاست.

سؤالات متداول

سقف پیش‌فرض تعداد فایل باز در لینوکس چقدر است؟ در اکثر توزیع‌ها سقف نرم پیش‌فرض ۱۰۲۴ و سقف سخت ۴۰۹۶ است، اما دقیق‌ترین عدد را همیشه با ulimit -n و ulimit -Hn روی سرور خودتان چک کنید چون بین توزیع‌ها فرق دارد.

آیا تغییر ulimit نیاز به ریستارت سرور دارد؟ برای تست موقت با خود دستور ulimit -n نه، فقط روی شل فعلی اثر می‌گذارد. برای تغییر دائمی در limits.conf باید دوباره لاگین کنید و برای تغییر در unit file سرویس باید systemctl daemon-reload و ریستارت سرویس را اجرا کنید.

چرا تغییر limits.conf روی سرویسی که با systemd بالا می‌آید اثر ندارد؟ چون آن سرویس مستقیم توسط systemd (PID 1) اجرا می‌شود، نه از یک لاگین شل که ماژول pam_limits روی آن اثر بگذارد. برای این سرویس‌ها باید LimitNOFILE را در unit file یا با systemctl edit تنظیم کنید.

چطور بفهمم کدام پردازه فایل دیسکریپتور را نشت می‌دهد؟ خروجی lsof -p <pid> را چند بار با فاصلهٔ زمانی بگیرید و فایل‌ها یا سوکت‌هایی را پیدا کنید که در هر بار بیشتر می‌شوند بدون اینکه هیچ‌وقت بسته شوند؛ این الگو نشانهٔ نشت در کد برنامه است، نه کمبود سقف سیستم.