176 lines
13 KiB
Markdown
176 lines
13 KiB
Markdown
# 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 شده؟
|
||
|
||
```bash
|
||
# وضعیت و 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» را روشن کنید.
|
||
|
||
مستندات:
|
||
- <https://coolify.io/docs/knowledge-base/notifications>
|
||
- <https://coolify.io/docs/knowledge-base/health-checks>
|
||
|
||
## ۵. تمدید گواهی 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.ir` (و `www.clinic-pro.ir` اگر DNS دارد) باید دقیقاً با `https://` روی سرویس `app` ثبت باشد. دامنهای که DNS آن به سرور اشاره میکند ولی در Coolify ثبت نیست، دقیقاً همین الگوی 404 را میسازد.
|
||
2. تاریخ انقضای گواهی فعلی را چک کنید:
|
||
|
||
```bash
|
||
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.
|
||
|
||
### تأیید روی سرور — اسکریپت آماده
|
||
|
||
فایل [`docs/collect-diagnostics.sh`](collect-diagnostics.sh) را روی **هاست** کپی و اجرا کنید (همه شواهد را یکجا جمع میکند):
|
||
|
||
```bash
|
||
scp docs/collect-diagnostics.sh root@SERVER:/tmp/ && ssh root@SERVER sh /tmp/collect-diagnostics.sh
|
||
```
|
||
|
||
یا دستورهای کلیدی بهصورت دستی:
|
||
|
||
```bash
|
||
# آیا کانتینر با 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
|
||
```
|
||
|
||
### جدول تفسیر خروجی اسکریپت
|
||
|
||
| شاهد در خروجی | تشخیص | اقدام |
|
||
|---|---|---|
|
||
| بخش ۱: `Out of memory: Killed process ... php-fpm` | نشت حافظه fpm | فیکسهای `pm.max_requests`/`memory_limit` این repo + redeploy کافی است |
|
||
| بخش ۱: `Killed process ... mariadbd` یا `redis` | فشار کل RAM سرور | Redis `maxmemory` + Memory Limit در Coolify + swap |
|
||
| بخش ۴: `OOMKilled=true` | سقف حافظه خود کانتینر | سقف را بالاتر ببرید یا مصرف را کم کنید |
|
||
| بخش ۳: دیسک ≥ ۹۰٪ یا `*-json.log` چند GB | دیسک پر | log rotation در `daemon.json` + پاکسازی |
|
||
| بخش ۷: `maxmemory: 0` و `used_memory` بزرگ/رشدکننده | Redis بیسقف | `maxmemory 256mb` + `maxmemory-policy volatile-lru` |
|
||
| بخش ۶: `memory_limit` هنوز `1024M` | image قدیمی در حال اجراست | Redeploy نشده — دوباره deploy کنید |
|
||
| لاگ worker: `[critical] ... "socket error on read socket"` (Connection.php) | اتصال Redis وسط کار قطع شده — تقریباً همیشه یعنی خود Redis مرده/ریاستارت شده (OOM) | بخش ۷ اسکریپت: `uptime_in_seconds` کوچک Redis = ریاستارت شده؛ `maxmemory 256mb` + `volatile-lru` بگذارید و `docker logs <redis>` را ببینید |
|
||
| هیچکدام | فرضیه حافظه/دیسک رد شد | خروجی کامل اسکریپت + ۵۰ خط آخر لاگ قبل از down را برای تحلیل بفرستید |
|
||
|
||
### چرا این اتفاق میافتاد (و فیکس اعمالشده)
|
||
|
||
- `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)
|
||
|
||
0. **اول Redeploy** — همه فیکسهای این repo (php.ini، zz-pool.conf، supervisord.conf) فقط با build/deploy جدید اعمال میشوند؛ و در env واقعی Coolify به `MESSENGER_TRANSPORT_DSN` پارامتر `?stream_max_entries=20000` را اضافه کنید (نمونه در `.env.coolify.example`).
|
||
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`:
|
||
```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** روی سرور اثر میکند.
|