Kubernetes و کانتینرمهندسانسطح متوسط

خطای fsnotify watcher در Kubernetes؛ سقف inotify نود

اگر پادهای Grafana، Prometheus یا هر اپلیکیشنی که از inotify استفاده می‌کند با خطای «failed to create fsnotify watcher: too many open files» کرش می‌کنند، مقصر سقف پیش‌فرض هسته لینوکس روی نود است.

تحریریه کلودیکپ ۷ دقیقه مطالعه
خطای fsnotify watcher در Kubernetes؛ سقف inotify نود

شرح مسئله: کرش پاد با پیام failed to create fsnotify watcher

یکی از سناریوهای پرتکرار در کلاسترهایی که اپ‌های file-watcher اجرا می‌کنند، این است که پادها به‌شکل ناگهانی به CrashLoopBackOff می‌افتند یا رول‌آوت یک Deployment وسط راه گیر می‌کند. لاگ کانتینر معمولاً چیزی شبیه این نشان می‌دهد:

level=error msg="failed to create fsnotify watcher: too many open files" logger=ngalert.sender.router
level=error msg="failed to start alertmanager: failed to create fsnotify watcher: too many open files"

نشانه‌های همراه که معمولاً کنارش دیده می‌شوند:

  • پاد بعد از چند ثانیه کارکرد ری‌استارت می‌شود و kubectl describe pod دلیل Error یا OOMKilled را نشان می‌دهد، در حالی که مصرف حافظه پایین است.
  • خطای مشابه روی سرویس‌های مختلف همان نود تکرار می‌شود؛ مثلاً هم‌زمان Grafana و Node Exporter و یک اپلیکیشن Node.js.
  • بعد از ری‌استارت کلاستر یا اضافه‌کردن چند workload جدید، مشکل تازه ظاهر شده است.
  • اگر تعداد پادها را روی همان نود کمتر کنید، مشکل خودبه‌خود محو می‌شود — که گمراه‌کننده است و مهندس را به سمت مشکل منابع (CPU/Memory) می‌فرستد.

یکی از سردرگمی‌های رایج این است که پیام «too many open files» شبیه خطای ulimit -n است، در حالی که اینجا موضوع کاملاً متفاوت است: محدودیت inotify هسته لینوکس، نه file descriptor هر پروسه.

علت ریشه‌ای: سقف inotify هسته، نه منابع پاد

لینوکس برای پشتیبانی از watch کردن فایل‌ها و دایرکتوری‌ها، سه محدودیت per-user دارد که از طریق kernel در اختیار همه پروسه‌ها قرار می‌گیرد:

پارامترمقدار پیش‌فرض معمولکاربرد
fs.inotify.max_user_instances128تعداد نمونه‌های inotify که یک کاربر می‌تواند بسازد
fs.inotify.max_user_watches8192تعداد کل watch های فعال یک کاربر
fs.inotify.max_queued_events16384صف رویدادهای در انتظار پردازش

کتابخانه‌هایی مثل fsnotify در Go یا chokidar در Node.js، برای هر watcher یک inotify instance جدا باز می‌کنند. Grafana وقتی Unified Alerting را روشن می‌کند، Prometheus برای hot-reload کردن فایل کانفیگ، Java WatchService و بسیاری از سرویس‌های دیگر هم همین رفتار را دارند. همه این‌ها روی نود به یک user (معمولاً root، چون کانتینرها به‌طور پیش‌فرض به همان user namespace نود نگاه می‌کنند) حساب می‌شوند.

یعنی وقتی روی یک نود مثلاً ۲۰ پاد file-watcher دارید و هر کدام چند instance می‌سازد، خیلی سریع به سقف ۱۲۸ می‌خورید و پروسه بعدی که سعی می‌کند inotify instance بسازد، با خطای EMFILE و پیام too many open files رد می‌شود. این سقف توسط Kubernetes مدیریت نمی‌شود و از ایمیج نود یا توزیع لینوکس ارث می‌رسد.

راه‌حل گام‌به‌گام

گام ۱: تأیید مقدار فعلی روی نود

از روی خود نود یا با یک Pod مخصوص تروبل‌شوتینگ، مقادیر را بخوانید. این مقادیر kernel-global هستند و از داخل کانتینر هم دیده می‌شوند:

sudo sysctl fs.inotify.max_user_instances fs.inotify.max_user_watches fs.inotify.max_queued_events
# خروجی نمونه
# fs.inotify.max_user_instances = 128
# fs.inotify.max_user_watches = 8192
# fs.inotify.max_queued_events = 16384

برای اطمینان از اینکه مشکل واقعاً همین است، تعداد instance های inotify فعال روی نود را بشمارید. اگر نزدیک سقف یا بالاتر از آن است، تشخیص تأیید می‌شود:

sudo find /proc/*/fd -lname anon_inode:inotify 2>/dev/null | wc -l
# خروجی نمونه: 124

گام ۲: افزایش موقت برای صحت‌سنجی سریع

اولین کار، بالا بردن مقدار در حافظه است تا مطمئن شوید مشکل حل می‌شود. این تغییر با ری‌استارت نود از دست می‌رود:

sudo sysctl -w fs.inotify.max_user_instances=8192
sudo sysctl -w fs.inotify.max_user_watches=524288
sudo sysctl -w fs.inotify.max_queued_events=32768

پادهای مشکل‌دار را دوباره راه‌اندازی کنید (با kubectl rollout restart) و لاگ‌ها را ببینید. اگر خطا رفت، گام بعدی را برای پایداری انجام دهید.

گام ۳: پایداری روی نود

یک فایل جداگانه در /etc/sysctl.d بسازید تا با هر بوت اعمال شود. مقدار پیشنهادی برای کلاسترهای متوسط و بزرگ:

# /etc/sysctl.d/99-inotify.conf
fs.inotify.max_user_instances = 8192
fs.inotify.max_user_watches = 524288
fs.inotify.max_queued_events = 32768

سپس:

sudo sysctl --system
sudo sysctl fs.inotify.max_user_instances

اگر نودها را با cloud-init، Ansible، Packer یا MachineConfig (در OpenShift) پروویژن می‌کنید، همین فایل باید بخشی از تصویر پایه نود باشد. برای EKS با Karpenter یا managed node group، از user-data و launch template استفاده کنید.

گام ۴: اعمال cluster-wide با DaemonSet (fallback)

وقتی به دلایلی نمی‌توانید ایمیج یا user-data نود را تغییر دهید (مثلاً کلاستر under حمایت تیم زیرساخت دیگری است)، این DaemonSet را با علم به ماهیت privileged آن به‌کار ببرید. روی هر نود، sysctl هاست را با nsenter از PID 1 تغییر می‌دهد:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: inotify-tuner
  namespace: kube-system
  labels:
    app.kubernetes.io/name: inotify-tuner
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: inotify-tuner
  template:
    metadata:
      labels:
        app.kubernetes.io/name: inotify-tuner
    spec:
      hostPID: true
      tolerations:
        - operator: Exists
      initContainers:
        - name: sysctl
          image: alpine:3.20
          securityContext:
            privileged: true
          command:
            - nsenter
            - --target=1
            - --mount
            - --uts
            - --ipc
            - --net
            - --pid
            - --
            - sysctl
            - -w
            - fs.inotify.max_user_instances=8192
            - fs.inotify.max_user_watches=524288
            - fs.inotify.max_queued_events=32768
      containers:
        - name: pause
          image: registry.k8s.io/pause:3.9

نکته مهم: این تغییر هم موقتی است و بعد از ری‌استارت نود از بین می‌رود؛ چون کارش sysctl -w است نه نوشتن در /etc/sysctl.d. برای پایداری واقعی، همان گام ۳ روی ایمیج نود ضروری است و این DaemonSet را فقط به‌عنوان پنچر عملیاتی در نظر بگیرید که بعد از هر بوت مجدد باید دوباره اجرا شود (که در واقعیت خودش می‌تواند منبع دردسر باشد، پس ترجیح دهید حل را در لایه پروویژنینگ نود انجام دهید).

گام ۵: اجرای مجدد workload مشکل‌دار

kubectl rollout restart deployment/grafana -n monitoring
kubectl rollout status deployment/grafana -n monitoring

بررسی اینکه مشکل واقعاً حل شده است

سه چک سریع، تصویر کامل می‌دهد:

  1. مقدار جدید باید از داخل خود پاد هم دیده شود، چون این سقف node-global است:
kubectl exec -it deploy/grafana -n monitoring -- cat /proc/sys/fs/inotify/max_user_instances
# خروجی باید 8192 باشد، نه 128
  1. در لاگ پاد، پیام fsnotify watcher نباید دیگر ظاهر شود:
kubectl logs deploy/grafana -n monitoring --since=30m | grep -i fsnotify || echo "OK: no fsnotify errors"
  1. شمارش تعداد instance های فعال روی نود باید الان به‌طور معناداری پایین‌تر از سقف جدید باشد:
sudo find /proc/*/fd -lname anon_inode:inotify 2>/dev/null | wc -l
# انتظار: عددی در محدوده چند صد، در حالی که سقف 8192 است

اگر خطا برگشت ولی سقف بالا رفته، احتمالاً مشکل از یک اپ با نشتی inotify است؛ در این حالت شمارش per-process را بررسی کنید و ببینید کدام پاد مداوم instance می‌سازد و رها نمی‌کند.

پیشگیری و بهترین روش‌ها

  • در ایمیج پایه نود بگذارید، نه در runtime. فایل /etc/sysctl.d/99-inotify.conf را در cloud-init، Packer، AMI پایه یا MachineConfig بگنجانید تا هر نود جدید با تنظیم درست بالا بیاید.
  • مقدار را بر اساس تعداد پاد پیش‌بینی کنید. برای هر پاد file-watcher چیزی بین ۵ تا ۲۰ instance در نظر بگیرید و سقف را با ضریب اطمینان بگذارید. برای اکثر کلاسترها 8192 برای instance و 524288 برای watch کافی است.
  • مصرف را مانیتور کنید. با Node Exporter و یک textfile collector، تعداد instance های فعال را به معیار تبدیل کنید و آلارم روی نسبت به سقف (مثلاً ۷۰٪) بگذارید. آلارم روی «کرش پاد» دیر است؛ باید قبلش متوجه شوید.
  • خود اپ را بهینه کنید. اگر Grafana یا هر سرویس دیگری Unified Alerting، hot reload یا file watching ندارد که واقعاً به آن نیاز دارید، غیرفعالش کنید. Prometheus را با --web.enable-lifecycle و بدون watch روی فایل‌های mount شده سنگین بالا نیاورید.
  • محدودیت را در سمت بار کاری هم در نظر بگیرید. قید securityContext.runAsUser با UID متفاوت برای اپ‌های پرحجم، سهمیه‌ی inotify را از root جدا می‌کند و از برخورد منابع در یک نود جلوگیری می‌کند.
  • مستند کنید. این تنظیم را به‌عنوان بخشی از «baseline نود» در runbook تیم نگه دارید تا هر کسی که کلاستر جدید می‌سازد آن را لحاظ کند؛ در غیر این‌صورت مشکل چند ماه بعد در محیط production دوباره سر باز می‌کند.

خطای fsnotify watcher در نگاه اول شبیه یک باگ اپلیکیشنی به‌نظر می‌رسد، اما در واقع یک تنظیم فراموش‌شده در لایه نود است. با بالا بردن سقف inotify در پروویژن نود و مانیتور کردن مصرف، این دسته از کرش‌ها را برای همیشه از سبد تیکت‌های عملیاتی حذف می‌کنید.

اشتراک‌گذاری:

نویسنده

تحریریه کلودیکپ

تیم محتوای کلودیکپ

تیم فنی و محتوای کلودیکپ؛ مهندسانی که هر روز با DevOps، Kubernetes و زیرساخت ابری کار می‌کنند و تجربه‌هایشان را اینجا می‌نویسند.

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

سوالات متداول این مقاله

متن «too many open files» معمولاً مهندس را به سمت ulimit و file descriptor هر پروسه می‌برد، اما این خطا در واقع از سقف inotify هسته لینوکس می‌آید که per-user (معمولاً root) است و بین همه پادهای روی همان نود مشترک است. بالا بردن limit پاد یا CPU/Memory بی‌فایده است.

هر watch در هسته چیزی در حدود یک کیلوبایت اشغال می‌کند؛ پس ۵۲۴۲۸۸ watch حدود چند صد مگابایت سقف مصرف دارد. چون این سقف است نه تخصیص، حافظه فقط در صورت استفاده واقعی مصرف می‌شود. برای اکثر کلاسترها بین ۱۲۸ تا ۵۱۲ هزار watch کاملاً امن است.

چون آن DaemonSet با sysctl -w مقدار را فقط در حافظه هسته اعمال می‌کند و در فایل پیکربندی نمی‌نویسد. برای پایداری باید فایل /etc/sysctl.d روی ایمیج نود یا در cloud-init قرار بگیرد تا با هر بوت اعمال شود.

ادامه مطالعه

مقالات مرتبط

تأخیر ۵ ثانیه‌ای DNS در Kubernetes؛ پای ndots:5 در میان است
Kubernetes و کانتینرمتوسط

تأخیر ۵ ثانیه‌ای DNS در Kubernetes؛ پای ndots:5 در میان است

هرچند وقت یک‌بار یک درخواست خروجی دقیقاً ۵ ثانیه طول می‌کشد و بعد موفق می‌شود؟ ریشه معمولاً گزینه ndots:5 در resolv.conf پادها و لیست search است. این مقاله علت، تشخیص و راه‌حل کامل را بررسی می‌کند.

تحریریه کلودیکپ ۷ دقیقه مطالعه
پاد Kubernetes در وضعیت Terminating گیر کرده؛ آزادسازی امن
Kubernetes و کانتینرمتوسط

پاد Kubernetes در وضعیت Terminating گیر کرده؛ آزادسازی امن

پاد در حالت Terminating گیر کرده و با kubectl delete پاک نمی‌شود؟ این مقاله علت واقعی (finalizer باقی‌مانده، نود NotReady، ولوم گیرکرده) و راه‌حل گام‌به‌گام امن را نشان می‌دهد.

تحریریه کلودیکپ ۶ دقیقه مطالعه
قطع شدن WebSocket در HAProxy هر ۵۰ ثانیه؛ علت و راه‌حل
Linux و مدیریت سرورمتوسط

قطع شدن WebSocket در HAProxy هر ۵۰ ثانیه؛ علت و راه‌حل

اگر WebSocket یا SSE پشت HAProxy هر ۵۰ ثانیه قطع می‌شود، مقصر تایماوت‌های پیش‌فرض فایل نمونه است. در این مقاله علت ریشه‌ای و پیکربندی درست timeout tunnel را بررسی می‌کنیم.

تحریریه کلودیکپ ۷ دقیقه مطالعه

در پیاده‌سازی به کمک نیاز دارید؟

تیم کلودیکپ همین کار را هر روز برای تیم‌های دیگر انجام می‌دهد. اگر جایی گیر کرده‌اید، با ما صحبت کنید.