Files
clinicpro/docs/ops-restarts.md
T

10 KiB

Runbook — تشخیص «ری‌استارت» سرور: recycle عادی یا خرابی واقعی؟

این سند برای وقتی است که در لاگ‌های production (Coolify) خطوطی مثل exited: worker-... دیده می‌شود و به‌نظر می‌رسد «سرور مدام ری‌استارت می‌شود».

خلاصه یک‌خطی

خروج ساعتیِ workerها با exit status 0; expected طراحی‌شده و بی‌ضرر است. خرابی واقعی یعنی exit غیرصفر، FATAL، یا قطع شدن پاسخ /health.


۱. چرا workerها هر ساعت restart می‌شوند؟

هر دو consumer پیام (async و scheduler) با این فلگ‌ها اجرا می‌شوند:

messenger:consume async --time-limit=3600 --memory-limit=128M
messenger:consume scheduler_default --time-limit=3600
  • --time-limit=3600 → worker بعد از ۱ ساعت خودش با exit code 0 خارج می‌شود.
  • supervisord (یا restart: unless-stopped در compose) بلافاصله دوباره آن را بالا می‌آورد (زیر ۱ ثانیه).
  • هدف: جلوگیری از نشت حافظه PHP در پروسه‌های طولانی و تازه‌کردن اتصال‌های کهنه به MariaDB/Redis. الگوی استاندارد و توصیه‌شده Symfony Messenger است.

به همین دلیل این لاگ عادی است و هیچ اقدامی لازم ندارد:

INFO exited: worker-scheduler (exit status 0; expected)
INFO spawned: 'worker-scheduler' with pid 1464
INFO success: worker-scheduler entered RUNNING state, process has stayed up for > than 1 seconds

کلیدواژه expected یعنی supervisor از قبل می‌دانست این exit برنامه‌ریزی‌شده است.

۲. نشانه‌های خرابی واقعی

اگر هر یک از این‌ها را دیدید، مشکل واقعی است:

نشانه در لاگ معنی
exited: ... (exit status 1) یا هر عدد غیرصفر worker با خطا مرده
entered FATAL state, too many start retries too quickly worker بالا نمی‌آید (مثلاً DB/Redis در دسترس نیست)
گپ چند دقیقه‌ای در لاگ‌های GET /health ... 200 وب‌سرور واقعاً پاسخ نمی‌داده
GET /health با status غیر از 200 اپ boot نمی‌شود

۳. دستورهای تشخیص روی سرور

سؤال اصلی: آیا خود کانتینر ری‌استارت شده یا فقط پروسه worker داخل آن recycle شده؟

# وضعیت و uptime کانتینرها — Status باید «Up X hours (healthy)» باشد
docker ps --format 'table {{.Names}}\t{{.Status}}'

# تعداد ری‌استارت واقعی و زمان آخرین start کانتینر
docker inspect --format '{{.RestartCount}} {{.State.StartedAt}}' <container-name>

# رویدادهای restart/die در ۲۴ ساعت گذشته
docker events --since 24h --filter event=restart --filter event=die

تفسیر:

  • RestartCount = 0 و StartedAt قدیمی (مثلاً چند روز پیش) → هیچ ری‌استارت واقعی‌ای رخ نداده؛ فقط recycle داخلی worker است.
  • RestartCount بالا یا StartedAt تازه بدون deploy → کانتینر واقعاً crash/restart می‌شود؛ لاگ‌های قبل از مرگ را ببینید: docker logs --tail 200 <container>.

۴. اطلاع‌رسانی خودکار در Coolify

برای اینکه down شدن واقعی بلافاصله خبر داده شود (به‌جای پایش دستی لاگ):

  1. در Coolify → Notifications، یک کانال (Telegram / Email / Discord) فعال کنید.
  2. رویدادهای «container stopped / unhealthy» و «deployment failed» را روشن کنید.

مستندات:

۵. تمدید گواهی TLS (ACME) — مهم‌ترین ریسک down شدن واقعی

علامت مشکل

در access log کانتینر app:

GET /.well-known/acme-challenge/xxxx HTTP/1.1" 404 ... "Let's Encrypt validation server"

چالش HTTP-01 لتس‌انکریپت باید توسط Traefik (پراکسی Coolify) پاسخ داده شود و اصلاً نباید به کانتینر app برسد. رسیدن آن به app و گرفتن 404 یعنی صدور/تمدید گواهی برای آن دامنه دارد شکست می‌خورد. اگر رها شود، بعد از انقضای گواهی فعلی، سایت با خطای TLS از دسترس خارج می‌شود — و این همان «سرور down شد» است.

چک‌لیست رفع

  1. در Coolify → resource اپ → تب Domains: دامنه clinic-pro.irwww.clinic-pro.ir اگر DNS دارد) باید دقیقاً با https:// روی سرویس app ثبت باشد. دامنه‌ای که DNS آن به سرور اشاره می‌کند ولی در Coolify ثبت نیست، دقیقاً همین الگوی 404 را می‌سازد.

  2. تاریخ انقضای گواهی فعلی را چک کنید:

    echo | openssl s_client -connect clinic-pro.ir:443 -servername clinic-pro.ir 2>/dev/null \
      | openssl x509 -noout -dates
    
  3. لاگ پراکسی را برای خطاهای acme ببینید: Coolify → Servers → Proxy → Logs (جستجوی acme).

مستندات: https://coolify.io/docs/knowledge-base/proxy/traefik/overview

۶. Healthcheckها — رفتار انتظاری

  • کانتینر app: docker/healthcheck.sh مسیر واقعی /health را از داخل می‌زند. پارامترها در Dockerfile و docker-compose.yml یکسان‌اند: interval=15s, timeout=5s, retries=5, start_period=60s. یعنی برای unhealthy شدن باید ۵ بار پیاپی (~۷۵ ثانیه) fail شود.
  • کانتینرهای worker (حالت compose): healthcheck فقط وجود پروسه messenger:consume را با ps | grep چک می‌کند (interval=30s, retries=3). در لحظه exit ساعتیِ worker ممکن است یک چک fail شود — بی‌اهمیت است؛ برای unhealthy شدن ۳ شکست پیاپی (~۹۰ ثانیه) لازم است و worker در همان چند ثانیه اول برمی‌گردد. unhealthy شدن worker فقط وقتی رخ می‌دهد که consumer واقعاً بالا نیاید (مثلاً Redis در دسترس نباشد).

۷. down شدن بعد از چند ساعت — کمبود حافظه (OOM)

الگو: workerها چند بار recycle عادی می‌شوند و بعد از چند ساعت کل سرویس down می‌شود. مظنون اول: پر شدن حافظه سرور و کشته شدن پروسه‌ها توسط OOM-killer.

تأیید روی سرور

# آیا کانتینر با OOM کشته شده؟
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}} {{.State.FinishedAt}}' <container>

# ردپای OOM-killer در کرنل (مهم‌ترین دستور)
journalctl -k --since "24 hours ago" | grep -i -E "oom|killed process"

# مصرف لحظه‌ای هر کانتینر
docker stats --no-stream

# حافظه و swap کل سرور
free -h

اگر OOMKilled=true یا در journalctl خط Out of memory: Killed process ... (php-fpm|mariadbd) دیدید، تشخیص قطعی است.

چرا این اتفاق می‌افتاد (و فیکس اعمال‌شده)

  • memory_limit هر request برابر 1024M بود → به 256M کاهش یافت (docker/php/php.ini).
  • pool فقط clear_env=no داشت؛ pm.max_requests تنظیم نشده بود → پروسه‌های php-fpm هرگز recycle نمی‌شدند و نشت‌های کوچک حافظه در طول ساعت‌ها جمع می‌شد. حالا pm.max_requests=500 + سقف pm.max_children=8 (docker/php/zz-pool.conf).
  • workerهای messenger از قبل با --time-limit=3600 و --memory-limit=128M محافظت می‌شدند — مشکل از آن‌ها نبود.

اقدامات تکمیلی روی سرور (خارج از repo)

  1. در Coolify برای resource اپ Memory Limit بگذارید (مثلاً 1G) تا در بدترین حالت فقط همان کانتینر ری‌استارت شود، نه کل سرور.
  2. اگر سرور swap ندارد، 1-2G swap اضافه کنید: fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile.
  3. اسکن‌های بات (درخواست‌های GET /xxx.php → 404 پشت‌سرهم) توسط nginx مستقیم 404 می‌شوند و به Symfony نمی‌رسند — عامل مرگ نیستند، فقط نویز لاگ.
  4. Redis بدون سقف حافظه: کش اپ (cache.adapter.redis) و صف messenger هر دو روی یک Redis resource هستند و Redis پیش‌فرض maxmemory=0 (رشد بی‌نهایت) دارد — یکی از عوامل خورده شدن تدریجی RAM. روی Redis resource در Coolify ست کنید:
    maxmemory 256mb
    maxmemory-policy volatile-lru
    
    ⚠️ allkeys-lru ممنوع — پیام‌های صف messenger (بدون TTL) را حذف می‌کند؛ volatile-lru فقط کلیدهای کش (TTL‌دار) را evict می‌کند. در DSN صف هم ?stream_max_entries=20000 بگذارید (نمونه در .env.coolify.example).
  5. چرخش لاگ Docker: اگر daemon محدودیت ندارد، فایل‌های *-json.log تا پر شدن دیسک رشد می‌کنند. چک: df -h و du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail. فیکس در /etc/docker/daemon.json:
    { "log-driver": "json-file", "log-opts": { "max-size": "20m", "max-file": "3" } }
    
    (بعدش systemctl restart docker — کانتینرها باید recreate شوند تا اعمال شود.)
  6. جدول messenger_messages (failed): پیام‌های شکست‌خورده (مثلاً SMS بعد از ۳ retry) برای همیشه در DB می‌مانند. هر چند وقت: php bin/console messenger:failed:show و پاکسازی با messenger:failed:remove.

۸. اعمال تغییرات config

فایل‌های docker/supervisord.conf، docker-compose.yml و supervisor.conf باید آینه هم بمانند (workerهای یکسان، فلگ‌های یکسان). هر تغییری در آن‌ها فقط بعد از Redeploy در Coolify روی سرور اثر می‌کند.