اپلیکیشنتان را با nohup ./app & بالا آوردهاید، SSH را بستهاید و نیم ساعت بعد سایت 502 میدهد. یا بدتر: پروسه هنوز زنده است ولی کسی نمیداند کجاست، چه کسی آن را اجرا کرده و چرا بعد از ریاستارت سرور برنگشته. این دقیقاً همان جایی است که باید یک unit فایل systemd بنویسید و از شر اسکریپتهای شل خلاص شوید.
چرا nohup و screen جواب نمیدهند
هر دو ابزار پروسه را زنده نگه میدارند، ولی هیچکدام مسئولیتپذیر نیستند. اگر پروسه کرش کند، کسی آن را برنمیگرداند. اگر سرور ریاستارت شود، هیچچیز بالا نمیآید. اگر لاگ بخواهید، باید دنبال فایلهای پراکنده در /tmp بگردید. systemd این سه کار را با یک فایل متنی انجام میدهد و علاوه بر آن، محدودیت منابع، ترتیب بوت و وابستگی به سرویسهای دیگر را هم مدیریت میکند.
ساخت اولین unit فایل
فایل را در /etc/systemd/system/myapp.service بسازید. مسیر مهم است: unitهای داخل /etc/systemd/system بر هر چیزی که پکیجمنیجر نصب کرده اولویت دارند و با آپدیت پکیج پاک نمیشوند.
[Unit]
Description=My Node API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/node /srv/myapp/server.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
EnvironmentFile=-/etc/myapp/env
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
چند خط این فایل بیشتر از بقیه مهماند. After=network-online.target تضمین میکند اپ قبل از آماده شدن شبکه اجرا نشود؛ بدون آن، سرویسی که به دیتابیس روی شبکه وصل میشود در بوت با خطای اتصال میمیرد و شما فکر میکنید کد خراب است. EnvironmentFile با آن خط تیره ابتدای مسیر یعنی اگر فایل نبود، systemd خطا ندهد. و Restart=on-failure فقط وقتی پروسه با کد خروج غیرصفر یا سیگنال بمیرد آن را برمیگرداند؛ اگر اپ شما خودش با کد صفر خارج میشود و انتظار دارید دوباره بالا بیاید، باید Restart=always بگذارید.
Type=simple یا Type=exec؟
برای اکثر اپها Type=simple کافی است. اگر میخواهید systemd مطمئن شود فایل اجرایی واقعاً وجود دارد و اجرا شده، Type=exec را انتخاب کنید؛ تفاوت عملیاش این است که خطای «فایل پیدا نشد» را در همان لحظه در وضعیت سرویس میبینید، نه در لاگ. برای سرویسهایی که خودشان daemonize میکنند (مثل PHP-FPM) از Type=forking استفاده کنید، ولی اگر اپ خودتان را مینویسید، daemonize نکنید؛ systemd از این کار بدش میآید و ردیابی پروسه را سخت میکند.
اجرا با کاربر غیر روت
اینجا اشتباه میکنند: unit را بدون User= مینویسند و اپ با root بالا میآید. علامتش را بعداً میبینند، وقتی یک آسیبپذیری ساده در اپ به دسترسی کامل سرور تبدیل میشود. کاربر بسازید و مالکیت دایرکتوری را بدهید:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuser
sudo chown -R appuser:appuser /srv/myapp
sudo chmod 750 /srv/myapp
اگر اپ روی پورت زیر 1024 گوش میدهد (مثل 80 یا 443)، کاربر غیر روت اجازه bind ندارد. راه درست این است که جلوی اپ یک reverse proxy مثل Nginx بگذارید و اپ روی پورت بالا مثل 3000 گوش بدهد. راه دوم، دادن AmbientCapabilities=CAP_NET_BIND_SERVICE به سرویس است که کمترین دسترسی لازم را میدهد. راه سوم، اجرا با root، غلط است.
فعالسازی و ریاستارت خودکار
بعد از هر تغییر در فایل unit، systemd باید دوباره بخواند:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.service
خط enable --now هم سرویس را برای بوت بعدی فعال میکند و هم همین حالا اجرا. اگر فقط start بزنید، بعد از ریاستارت سرور سرویس بالا نمیآید و این یکی از رایجترین دلایل «دیروز کار میکرد» است.
برای تست ریاستارت خودکار، پروسه را عمداً بکشید و ببینید systemd چند ثانیه بعد برش میگرداند:
sudo pkill -f server.js
sleep 6
systemctl status myapp.service | grep -E "Active|Main PID"
اگر سرویس در حالت activating (auto-restart) گیر کرده، یعنی تعداد ریاستارتها از حد گذشته. systemd بهطور پیشفرض اگر سرویسی در بازه کوتاه چند بار پشت سر هم بمیرد، از تلاش دست میکشد. با StartLimitIntervalSec=60 و StartLimitBurst=5 در بخش [Unit] این رفتار را کنترل کنید. این محدودیت را عمداً نگذارید بینهایت باشد؛ اپی که در حلقه کرش میکند و مدام بالا میآید، منابع سرور را بیسروصدا میخورد.
لاگها با journalctl
خروجی اپ بهطور خودکار در journal ثبت میشود، به شرطی که StandardOutput=journal را ست کرده باشید. پرکاربردترین دستورها:
journalctl -u myapp.service -n 100 --no-pager
journalctl -u myapp.service -f
journalctl -u myapp.service --since "1 hour ago"
journalctl -u myapp.service -p err -b
سوئیچ -f مثل tail -f عمل میکند، -b فقط لاگهای بوت فعلی را میآورد و -p err سطح خطا و بالاتر را فیلتر میکند. یک نکتهای که خیلیها را غافلگیر میکند: journal بهطور پیشفرض روی دیسک ذخیره نمیشود و با ریاستارت پاک میشود. اگر میخواهید لاگها بمانند، در /etc/systemd/journald.conf مقدار Storage=persistent را بگذارید و systemctl restart systemd-journald بزنید. برای کنترل حجم، SystemMaxUse=500M را هم ست کنید تا journal کل دیسک را پر نکند.
وقتی اپ لاگ را در فایل خودش مینویسد
اگر اپ شما به هر دلیلی باید در فایل جداگانه لاگ بنویسد، از StandardOutput=append:/var/log/myapp/app.log استفاده کنید (systemd نسخه 240 به بالا). این بهتر از ریدایرکت دستی در ExecStart است، چون چرخش لاگ و مالکیت فایل را خودتان کنترل میکنید و systemd فایل را با کاربر سرویس باز میکند.
محدودیت منابع و سختسازی
یکی از مزیتهایی که هیچ اسکریپت شلی نمیدهد، محدود کردن سرویس است. چند خط که واقعاً ارزش دارند:
MemoryMax=512M
CPUQuota=80%
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/srv/myapp/uploads
NoNewPrivileges=true
MemoryMax سرویس را وقتی از حد گذشت میکشد و در لاگ با پیام oom-kill ثبت میشود؛ همین پیام را اگر در journal دیدید، بدانید مشکل کد نیست، سقف حافظه است. ProtectSystem=strict کل فایلسیستم را read-only میکند و فقط مسیرهایی که در ReadWritePaths میآورید قابل نوشتن میمانند. این تنظیم عالی است، ولی هزینه دارد: اگر اپ شما جایی خارج از آن مسیرها فایل موقت میسازد، با خطای Read-only file system میمیرد و پیدا کردنش وقت میبرد. اول با ProtectSystem=full شروع کنید و بعد سختترش کنید.
قبل از رفتن سراغ production
سه چیز را چک کنید. اول، systemctl enable را فراموش نکنید. دوم، یک بار سرور را ریاستارت کنید و ببینید سرویس خودش بالا میآید یا نه؛ تست نکردن این مورد بیشترین وقت را در ساعات بحرانی میگیرد. سوم، مطمئن شوید اپ با کاربر درست اجرا میشود: ps -o user= -p $(systemctl show -p MainPID --value myapp.service) باید نام کاربر سرویس را برگرداند، نه root.
اگر هنوز روی سرور اختصاصی کار میکنید و میخواهید همین ساختار را روی یک محیط مدیریتشده پیاده کنید، هاست لینوکس نقطه شروع مناسبی است چون دسترسی کامل به systemd و journalctl دارید. برای اپهای وردپرسی هم اگر کرونتان بهجای wp-cron واقعی اجرا نمیشود، همان منطق unit فایل جواب میدهد؛ نمونهاش را در wp-cron وردپرس؛ چرا اجرا نمیشود نوشتهایم. اگر سرویس بالا میآید ولی کاربر خطای HTTP میگیرد، کد وضعیت HTTP: راهنمای عملی خطاهای رایج را ببینید تا سریعتر به ریشه برسید. و اگر تازه به لینوکس سرور قدم گذاشتهاید، مستندات و پایگاه دانش سرورنت چند نمونه unit آماده دارد که میتوانید کپی و ویرایش کنید.
پرسشهای پرتکرار
چرا سرویس systemd من بعد از بستن SSH میمیرد؟
سرویس systemd هرگز به session شما وابسته نیست و بستن SSH نباید آن را بکشد. اگر میمیرد، احتمالاً اپ را با systemctl start اجرا نکردهاید و همان پروسه nohup قدیمی هنوز در حال اجراست. با systemctl status myapp بررسی کنید که Main PID با پروسهای که در ps aux میبینید یکی باشد.
تفاوت systemctl restart و reload چیست؟
restart پروسه را کامل میکشد و از نو بالا میآورد، پس چند ثانیه قطعی دارد. reload سیگنال SIGHUP میفرستد و اگر اپ شما از آن پشتیبانی کند، بدون قطعی تنظیمات را دوباره میخواند. اگر اپ SIGHUP را مدیریت نکند، reload بیاثر است یا پروسه را میکشد؛ برای اطمینان ExecReload=/bin/kill -HUP $MAINPID را صریح بنویسید.
چطور بفهمم سرویس با کدام کاربر اجرا میشود؟
دستور systemctl show -p MainPID --value myapp.service شماره پروسه اصلی را میدهد و ps -o user= -p PID نام کاربر را. اگر root برگشت، یعنی خط User= در فایل unit نیست یا بعد از تغییر آن daemon-reload نزدهاید.
لاگهای journalctl بعد از ریاستارت پاک میشوند؛ چه کنم؟
پیشفرض journal روی برخی توزیعها در حافظه نگه داشته میشود. در /etc/systemd/journald.conf مقدار Storage=persistent را فعال کنید و سرویس journald را ریاستارت کنید. با SystemMaxUse هم سقف حجم تعیین کنید تا لاگها دیسک را پر نکنند.
دیدگاهها ۰
هنوز دیدگاهی ثبت نشده؛ اولین نفر باشید!