فایل unit در systemd: بخش‌ها، ری‌استارت و وابستگی

راهنمای دقیق ساختار فایل unit در systemd: بخش‌های Unit، Service و Install، سیاست‌های Restart، ترتیب وابستگی‌ها و خطاهای رایجی که سرویس را بی‌صدا از کار می‌اندازند.

۷ دقیقه به‌روزرسانی ۹ مهر ۱۴۰۵

سرویسی که با systemctl start بالا می‌آید ولی بعد از چند دقیقه بی‌صدا می‌میرد، تقریباً همیشه مشکلش در فایل unit است، نه در خود برنامه. علامت مشخصش هم این است: systemctl status وضعیت inactive (dead) نشان می‌دهد و journalctl -u myservice هیچ خطای واضحی ندارد. یعنی پروسه با کد خروج صفر یا سیگنال تمام شده و systemd هم طبق سیاستی که شما تعیین نکرده‌اید، تصمیم گرفته دوباره بلندش نکند. این مقاله همان فایل unit را بخش‌به‌بخش باز می‌کند.

فایل unit کجا می‌نشیند و کدام نسخه برنده است

systemd سه مسیر را به ترتیب اولویت می‌خواند: /etc/systemd/system/ بالاترین اولویت، بعد /run/systemd/system/ و در آخر /usr/lib/systemd/system/. اگر یک فایل با نام یکسان در دو مسیر باشد، آن که اولویت بالاتری دارد برنده است و نسخه پایین‌تر کاملاً نادیده گرفته می‌شود. این یعنی می‌توانید فایل vendor را دست‌نخورده بگذارید و نسخه خودتان را در /etc بنویسید؛ آپدیت بسته دیگر تغییرات شما را پاک نمی‌کند.

بعد از هر تغییر، دو دستور لازم است و یکی کافی نیست:

systemctl daemon-reload
systemctl restart myservice

اگر daemon-reload را نزنید، systemd همان نسخه قدیمی را در حافظه نگه می‌دارد و شما فکر می‌کنید تغییرتان اثر ندارد. این‌جا اشتباه می‌کنند: کاربر فایل را ویرایش می‌کند، restart می‌زند، هیچ اتفاقی نمی‌افتد، و بعد ساعت‌ها دنبال باگ در کد برنامه می‌گردد. نشانه‌اش این است که systemctl cat myservice چیزی متفاوت از فایلی که همین الان ذخیره کردید نشان می‌دهد.

سه بخش فایل unit و کلیدهایی که واقعاً استفاده می‌شوند

هر فایل unit از بخش‌های [Unit]، [Service] و [Install] ساخته می‌شود. بخش چهارم [Unit] برای سرویس‌های socket-activated وجود دارد که بحث جداگانه‌ای است.

بخش [Unit]: هویت و ترتیب

  • Description= متنی که در systemctl status دیده می‌شود. کوتاه و بدون کاراکتر غیرASCII بنویسید.
  • After=network-online.target یعنی این سرویس بعد از آماده‌شدن شبکه اجرا شود. توجه کنید After فقط ترتیب است، نه نیازمندی.
  • Requires= یعنی اگر آن unit بالا نیاید، این یکی هم اجرا نمی‌شود. Wants= نسخه ضعیف‌ترش است: تلاش می‌کند، ولی شکست آن مانع اجرا نیست.
  • BindsTo= سخت‌گیرانه‌تر از Requires است؛ اگر unit مقابل متوقف شود، این سرویس هم متوقف می‌شود.

تفاوت After و Requires را جدی بگیرید. خیلی‌ها فقط After=mysql.service می‌نویسند و انتظار دارند سرویسشان وقتی MySQL بالا نیست اجرا نشود. نمی‌شود. After فقط می‌گوید «اگر هر دو در صف بودند، اول آن یکی». برای وابستگی واقعی به Requires یا Wants هم نیاز دارید.

بخش [Service]: قلب ماجرا

مهم‌ترین کلید این بخش Type= است و انتخاب اشتباهش منبع بیشترین سردرگمی‌هاست:

Typeچه زمانی استفاده شودنشانه اشتباه بودن
simpleپیش‌فرض؛ پروسه در foreground می‌ماندسرویس بلافاصله active می‌شود ولی کار نمی‌کند
forkingبرنامه خودش daemonize می‌کندsystemd پروسه اصلی را گم می‌کند و سرویس dead می‌شود
notifyبرنامه با sd_notify آماده‌بودن را اعلام می‌کندسرویس تا ابد activating می‌ماند
oneshotکار یک‌باره، مثل اسکریپت راه‌اندازیسرویس بعد از پایان کار dead می‌شود که طبیعی است

کلیدهای اجرایی که تقریباً همیشه لازم می‌شوند:

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
Environment=NODE_ENV=production
EnvironmentFile=-/etc/myapp/env

دو نکته در همین بلوک. اول، EnvironmentFile با علامت منفی یعنی اگر فایل نبود خطا نده؛ بدون آن، نبودن فایل باعث شکست کامل سرویس می‌شود. دوم، LimitNOFILE را جدی بگیرید: پیش‌فرض systemd معمولاً 1024 است و برنامه‌های شبکه‌ای زیر بار واقعی با خطای Too many open files می‌خوابند. اگر لود سرور بالا رفته و علتش را نمی‌دانید، این یکی از جاهایی است که باید چک کنید؛ راهنمای تشخیص علت لود بالای سرور مسیر بررسی را قدم‌به‌قدم پوشش می‌دهد.

بخش [Install]: چه زمانی فعال شود

WantedBy=multi-user.target یعنی این سرویس در بوت معمولی فعال شود. اگر این خط نباشد، systemctl enable کار می‌کند ولی هشدار می‌دهد و سرویس در بوت بالا نمی‌آید. برای timerها WantedBy=timers.target می‌نویسید.

سیاست ری‌استارت: Restart= و آنچه واقعاً اتفاق می‌افتد

مقادیر پرکاربرد Restart= این‌ها هستند: no (پیش‌فرض)، on-failure، always، on-abnormal و on-abort. تفاوت on-failure و always در یک چیز است: اگر پروسه با کد خروج صفر تمام شود، on-failure آن را موفق می‌داند و دوباره اجرا نمی‌کند، ولی always می‌کند. برای سرویسی که باید همیشه زنده باشد، always انتخاب درست است.

اما این‌جا یک تله هست که کمتر کسی به آن اشاره می‌کند: Restart=always بدون محدودیت، سرویس خراب را در حلقه بی‌پایان می‌اندازد. systemd به‌طور پیش‌فرض جلوی این را با StartLimitBurst و StartLimitIntervalSec می‌گیرد؛ پیش‌فرض رایج 5 بار شروع در 10 ثانیه است. اگر از این حد بگذرد، سرویس وارد وضعیت failed می‌شود و دیگر تلاش نمی‌کند. پیام دقیق در ژورنال این است:

start request repeated too quickly for myservice.service

وقتی این را دیدید، systemctl reset-failed myservice وضعیت را پاک می‌کند، ولی مشکل اصلی را حل نمی‌کند. اول علت خروج را پیدا کنید، بعد سیاست را تنظیم کنید. RestartSec=5 هم کم‌اهمیت نیست؛ مقدار خیلی کوچک فشار روی منابع را در زمان خرابی چند برابر می‌کند.

وابستگی میان سرویس‌ها: ترتیب، نیازمندی و توقف

سه محور مستقل وجود دارد و اشتباه رایج، قاطی‌کردنشان است:

  1. ترتیب شروع: After= و Before=. فقط نوبت را تعیین می‌کند.
  2. نیازمندی: Requires=، Wants=، BindsTo=. تعیین می‌کند اصلاً اجرا شود یا نه.
  3. انتشار توقف: PartOf= و PropagatesReloadTo=. تعیین می‌کند وقتی سرویس مادر ری‌استارت شد، این یکی هم ری‌استارت شود.

الگوی درست برای سرویسی که به دیتابیس نیاز دارد، ترکیب هر سه است:

[Unit]
Description=My App
After=network-online.target mysql.service
Wants=network-online.target
Requires=mysql.service
PartOf=mysql.service

[Service]
Type=simple
ExecStart=/opt/myapp/bin/server
Restart=always
RestartSec=5

با PartOf=mysql.service، هر بار که MySQL ری‌استارت شود، اپلیکیشن هم خودکار ری‌استارت می‌شود و کانکشن‌های مرده باقی نمی‌مانند. این خط را خیلی‌ها جا می‌اندازند و بعد از هر ری‌استارت دیتابیس، اپلیکیشن با کانکشن‌های کهنه خطا می‌دهد تا کسی دستی آن را بالا بیاورد.

یک هشدار عملی: Requires دوطرفه نیست. اگر MySQL را دستی stop کنید، سرویس شما هم متوقف می‌شود، ولی اگر سرویس شما کرش کند، MySQL دست‌نخورده می‌ماند. اگر رفتار دوطرفه می‌خواهید، BindsTo را ببینید، با این هزینه که یک خطای گذرا در سرویس مقابل، کل زنجیره را پایین می‌آورد.

خطاهایی که در عمل زیاد دیده می‌شوند

ExecStart با مسیر نسبی. systemd مسیر مطلق می‌خواهد؛ ExecStart=./server با خطای Exec format error یا No such file or directory رد می‌شود، حتی اگر فایل وجود داشته باشد. همیشه مسیر کامل بنویسید.

نبود User=. بدون آن، سرویس با کاربر root اجرا می‌شود. این نه فقط مسئله امنیتی است؛ فایل‌هایی که سرویس می‌سازد مالک root می‌شوند و بعداً کاربر اپلیکیشن نمی‌تواند بخواندشان. اگر روی سرور اختصاصی یا سرور ابری چند سرویس را کنار هم می‌چرخانید، این یکی از رایج‌ترین دلایل خطاهای دسترسی عجیب است.

نادیده‌گرفتن ExecReload. اگر آن را تعریف نکنید، systemctl reload عملاً همان restart کامل است و کانکشن‌های فعال قطع می‌شوند. برای سرویس‌هایی مثل Nginx یا HAProxy که reload بدون قطعی دارند، این تفاوت مهم است.

و یک نکته درباره سرویس‌های کانتینری: اگر برنامه‌تان داخل Docker اجرا می‌شود، معمولاً نیازی به فایل unit دستی ندارید و Restart=always خود Docker کافی است. راهنمای راه‌اندازی داکر روی VPS تفاوت این دو لایه را توضیح می‌دهد؛ ترکیب هر دو بدون دلیل، فقط عیب‌یابی را سخت می‌کند.

پرسش‌های پرتکرار

چرا بعد از ویرایش فایل unit تغییرات اعمال نمی‌شود؟

چون systemd نسخه فایل را در حافظه کش می‌کند و تا systemctl daemon-reload نخورید، همان نسخه قدیمی را اجرا می‌کند. بعد از reload، سرویس را هم ری‌استارت کنید؛ reload تنها، پروسه در حال اجرا را با تنظیمات جدید بالا نمی‌آورد.

تفاوت After و Requires در فایل unit چیست؟

After فقط ترتیب شروع را تعیین می‌کند و اگر unit مقابل اجرا نشود، سرویس شما همچنان بالا می‌آید. Requires وابستگی واقعی می‌سازد؛ اگر آن unit شکست بخورد، سرویس شما هم اجرا نمی‌شود. برای وابستگی کامل به هر دو کنار هم نیاز دارید.

چرا سرویس با Restart=always وارد وضعیت failed می‌شود؟

چون systemd جلوی حلقه ری‌استارت را می‌گیرد. اگر سرویس بیش از StartLimitBurst بار در بازه StartLimitIntervalSec شروع شود، متوقف می‌شود و پیام start request repeated too quickly در ژورنال می‌آید. با systemctl reset-failed وضعیت پاک می‌شود، ولی علت خروج باید جداگانه رفع شود.

فایل unit را کجا بسازم که با آپدیت بسته پاک نشود؟

در /etc/systemd/system/. فایل‌های /usr/lib/systemd/system/ متعلق به بسته‌های نصب‌شده هستند و ممکن است در آپدیت بازنویسی شوند. اگر می‌خواهید فقط بخشی از یک unit موجود را تغییر دهید، به‌جای کپی کامل، از systemctl edit myservice استفاده کنید تا drop-in در /etc/systemd/system/myservice.service.d/ ساخته شود.

قدم بعدی مشخص است: روی سرویسی که همین الان مشکل دارد systemctl cat بزنید، بخش [Service] را با آنچه این‌جا آمد مقایسه کنید، و اول Type= و Restart= را درست کنید. بیشتر سرویس‌هایی که «خودبه‌خود می‌خوابند» فقط همین دو کلید را اشتباه دارند.

آیا این مطلب برایتان مفید بود؟