SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-21

آموزش تنظیم وابستگی‌ها و شرایط در systemd

تفاوت دقیق Requires، Wants، After و Before را در systemd بیاموزید. این راهنما توضیح می‌دهد چرا سرویس شما هنگام بوت شکست می‌خورد و چگونه با Conditionها از اجرای بیهوده جلوگیری کنید.

عبارت Requires به معنای After نیست

وابستگی‌ها و شرایط در systemd چهار مکانیزم مجزا هستند که اکثر فایل‌های unit از آن‌ها به‌گونه‌ای استفاده می‌کنند که گویی یکی هستند. Requires= و Wants= تعیین می‌کنند که کدام unitهای دیگر باید فراخوانی شوند. After= و Before= ترتیب شروع unitها را مشخص می‌کنند. ExecStartPre= یک بررسی را اجرا می‌کند که می‌تواند منجر به شکست unit شود. خانواده‌های Condition و Assert تصمیم می‌گیرند که آیا unit اصلاً اجرا شود یا خیر. هر مکانیزم مستقل از دیگری است؛ بنابراین یک unit می‌تواند به unit دیگری نیاز (require) داشته باشد و همچنان در همان لحظه با آن شروع شود.

جمله آخر، همان باگی است که پشت تقریباً تمام گزارش‌های «وقتی دستی اجرا می‌کنم کار می‌کند، اما هنگام بوت شکست می‌خورد» قرار دارد.

[Unit]
Description=Inventory API
Requires=postgresql.service

[Service]
ExecStartPre=/usr/bin/pg_isready -h 127.0.0.1 -t 5
ExecStart=/usr/local/bin/inventory-api

Requires=postgresql.service سرویس PostgreSQL را به همان تراکنش شروع (start transaction) وارد می‌کند. اما منتظر آن نمی‌ماند. systemd هر دو job را به‌صورت موازی شروع می‌کند، بنابراین pg_isready زمانی اجرا می‌شود که PostgreSQL هنوز در حال باز کردن دایرکتوری داده‌های خود است. این سرویس با کد 2 خارج می‌شود زیرا هنوز چیزی روی پورت گوش نمی‌دهد و unit پیش از آنکه به ExecStart برسد، شکست می‌خورد. اجرای sudo systemctl start inventory-api یک ساعت بعد موفقیت‌آمیز است، زیرا تا آن زمان PostgreSQL بالا آمده است. هیچ‌چیز در فایل unit تغییر نکرده است و به همین دلیل فایل بی‌نقص به نظر می‌رسد.

راه‌حل تنها یک خط است.

[Unit]
Requires=postgresql.service
After=postgresql.service

جزئیات دقیق‌تری در همان بخش نهفته است. یک وابستگی Requires= که با شکست مواجه شده، تنها زمانی مانع از شروع unit شما می‌شود که After= را نیز روی آن تنظیم کرده باشید. بدون تعیین ترتیب (ordering)، systemd تا زمانی که unit دیگر شکست بخورد، unit شما را شروع کرده است؛ بنابراین چیزی برای لغو کردن باقی نمی‌ماند. Requires= به‌تنهایی آن حفاظتی را که کاربران تصور می‌کنند، فراهم نمی‌کند. در کنار هر Requires= و هر Wants=، عبارت After= را بنویسید، مگر آنکه دلیل خاصی برای عدم انجام این کار داشته باشید.

تعهدات Requires، Wants، Requisite و BindsTo

تمام این موارد تنظیمات وابستگی هستند. هیچ‌کدام از آن‌ها ترتیب اجرا را تعیین نمی‌کنند.

  • Wants=: واحد دیگر را فراخوانی می‌کند. اگر آن واحد با خطا مواجه شود یا وجود نداشته باشد، این واحد همچنان اجرا می‌شود. این همان چیزی است که systemctl enable به صورت یک symlink در داخل دایرکتوری .wants/ ایجاد می‌کند.
  • Requires=: واحد دیگر را فراخوانی می‌کند. اگر آن واحد با خطا مواجه شود و شما همچنین After= را برای آن تنظیم کرده باشید، این واحد اجرا نخواهد شد. اگر واحد دیگر بعداً به‌طور صریح متوقف شود، این واحد نیز همراه با آن متوقف می‌شود.
  • Requisite=: واحد دیگر را فراخوانی نمی‌کند. اگر آن واحد از قبل فعال نباشد، این واحد بلافاصله با خطا مواجه می‌شود.
  • BindsTo=: مشابه Requires= است، با این تفاوت که این واحد هر زمان که واحد دیگر به هر دلیلی متوقف شود (از جمله ناپدید شدن سخت‌افزار)، متوقف می‌گردد.
  • PartOf=: توقف و راه‌اندازی مجدد از واحد دیگر به این واحد منتقل می‌شود. دستور Start منتقل نمی‌شود.
  • Conflicts=: اجرای این واحد باعث توقف واحد دیگر می‌شود.

برای یک daemon که با daemon دیگری در ارتباط است، ترکیب Wants= و After= معمولاً جفت مناسبی است. Requires= طول عمر آن‌ها را به هم گره می‌زند: اگر دیتابیس را برای نگهداری متوقف کنید، برنامه شما نیز همراه با آن از کار می‌افتد و با بازگشت دیتابیس، برنامه به‌طور خودکار برنمی‌گردد. ترکیب Wants= و After= ترتیب اجرای هنگام بوت را بدون آن وابستگی شدید فراهم می‌کند و یک سیاست restart، موردی را که وابستگی بعداً از دسترس خارج می‌شود، مدیریت می‌کند.

شما همچنین وابستگی‌هایی را به ارث می‌برید که هرگز ننوشته‌اید. با DefaultDependencies=yes که مقدار پیش‌فرض است، یک سرویس معمولی به‌طور خودکار Requires=sysinit.target، After=sysinit.target basic.target و Conflicts=shutdown.target را دریافت می‌کند. به همین دلیل است که سرویسی با بخش [Unit] تقریباً خالی، همچنان در اواخر فرآیند بوت شروع به کار کرده و در هنگام خاموش شدن به‌طور تمیز متوقف می‌شود.

ترتیب After و Before در تراکنش، نه چیز دیگر

After= و Before= صرفاً برای تعیین ترتیب هستند. آن‌ها هیچ پیش‌نیازی ایجاد نمی‌کنند. استفاده از After=redis.service در یک unit که هیچ چیز دیگری Redis را به درون آن نمی‌کشد، یک عملیات بی‌اثر (no-op) است: اگر redis.service بخشی از تراکنش نباشد، چیزی برای انتظار وجود ندارد و unit شما بلافاصله شروع به کار می‌کند.

این نکته ارزش دو بار گفتن را دارد، زیرا دقیقاً همان الگوی اشتباه network-online.target است که در ادامه به آن اشاره شده است. ترتیب‌بندی فقط منتظر unitهایی می‌ماند که در همان تراکنش در حال شروع شدن هستند.

این جفت متقارن است. نوشتن After=b.service در a.service همان معنای نوشتن Before=a.service در b.service را دارد؛ بنابراین یکی از آن‌ها را انتخاب کنید و در unit تحت کنترل خود قرار دهید. ترتیب‌بندی در هنگام خاموش شدن به‌طور خودکار معکوس می‌شود، بنابراین After=b.service همچنین به این معنی است که unit شما پیش از b.service متوقف می‌شود.

پارامتر After= منتظر «شروع‌شدن» می‌ماند و Type= تعیین می‌کند که این عبارت دقیقاً به چه معناست

After= تا زمانی که واحد دیگر کار راه‌اندازی خود را به پایان نرسانده باشد، منتظر می‌ماند. این‌که «پایان راه‌اندازی» دقیقاً به چه معناست، کاملاً توسط Type= همان واحد تعیین می‌شود.

  • Type=simple: به محض اینکه systemd فرآیند را fork کند. ممکن است برنامه هنوز فایل پیکربندی خود را پارس نکرده باشد، چه برسد به اینکه سوکتی باز کرده باشد.
  • Type=exec: به محض اینکه execve() با موفقیت انجام شود. کمی قوی‌تر است، اما همچنان درباره آماده‌به‌کار بودن چیزی نمی‌گوید.
  • Type=forking: زمانی که فرآیند والد اصلی خارج شود.
  • Type=oneshot: زمانی که فرآیند خارج شود. در اینجا «شروع‌شدن» واقعاً به معنای پایان یافتن کار است.
  • Type=notify: زمانی که سرویس پیام READY=1 را روی سوکت اطلاع‌رسانی خود ارسال کند. این تنها نوعی است که آمادگی واقعی را گزارش می‌دهد.

بنابراین After= در یک دیمون Type=simple یک وعده ضعیف است و این همان نیمه دوم مسابقه (race condition) در مثال اول است. اگر واحدی که به آن وابسته‌اید از نوع Type=simple باشد، قرار دادن آن در بخش After به این معنا نیست که سرویس در حال پذیرش اتصالات است. دو راهکار صادقانه وجود دارد: یا به جای خود سرویس، بعد از واحد سوکت (socket unit) آن دستور After را قرار دهید تا هسته سیستم‌عامل اتصالات ورودی را در صف نگه دارد تا زمانی که دیمون بالا بیاید، یا سرویس خود را طوری تنظیم کنید که تلاش مجدد (retry) انجام دهد و اجازه دهید سیاست restart کار را پیش ببرد. اینکه یک واحد از چه نوعی استفاده می‌کند در systemctl cat قابل مشاهده است و مطالعه تنظیمات Type= و معنای هر مقدار برای systemd پیش از تکیه بر ترتیب‌بندی واحدها، ارزشمند است.

دستور ExecStartPre دروازه‌ای است که می‌تواند باعث شکست unit شود

ExecStartPre= پیش از ExecStart= اجرا می‌شود. اگر خروجی این دستور غیرصفر باشد، فعال‌سازی متوقف شده و unit به وضعیت failed می‌رود. ExecStart= هرگز اجرا نخواهد شد. این مکانیزم دلیل اصلی شکست بسیاری از unitها بدون نمایش پیام از سوی برنامه اصلی است، زیرا برنامه هرگز اجرا نشده است.

نکاتی که اغلب باعث سردرگمی می‌شوند:

  • این یک shell نیست. هیچ pipe، تغییر مسیر (redirection)، glob یا && در آن پشتیبانی نمی‌شود. اولین توکن باید یک مسیر مطلق (absolute path) باشد. اگر به سینتکس shell نیاز دارید، خط را درون /bin/sh -c '...' قرار دهید.
  • پیشوند - باعث می‌شود خروجی غیرصفر، کشنده (fatal) نباشد: ExecStartPre=-/usr/bin/optional-check.
  • هر ExecStartPre= باید پیش از اجرای دستور بعدی به پایان برسد. این دستور نمی‌تواند یک پردازش طولانی‌مدت را آغاز کند.
  • تمام خطوط ExecStartPre= در TimeoutStartSec= با ExecStart= مشترک هستند. یک بررسی پیش‌نیاز که در حلقه‌ای منتظر دیتابیس می‌ماند، زمان timeout شروع را مصرف می‌کند و unit پس از ظاهر شدن start operation timed out. Terminating. در journal، با خطای Result: timeout شکست می‌خورد.

خطای شکست، نام پردازش کنترلی را ذکر می‌کند، نه پردازش اصلی را:

inventory-api.service: Control process exited, code=exited, status=2/INVALIDARGUMENT
inventory-api.service: Failed with result 'exit-code'.

این نام نمادین را با دقت بخوانید. systemd کدهای خروجی کوچک را از طریق یک جدول ثابت نگاشت می‌کند، بنابراین 2 همیشه به صورت INVALIDARGUMENT چاپ می‌شود، فارغ از اینکه برنامه چه معنایی برای آن در نظر گرفته باشد. status=203/EXEC کدی است که اطلاعات واقعی را حمل می‌کند: systemd اصلاً نتوانسته باینری را اجرا کند، زیرا مسیر اشتباه است یا فایل قابل‌اجرا نیست.

برای ایجاد دایرکتوری‌ها از ExecStartPre= استفاده نکنید. RuntimeDirectory=، StateDirectory=، LogsDirectory= و CacheDirectory= آن‌ها را با مالک و دسترسی (mode) صحیح ایجاد می‌کنند و RuntimeDirectory= هنگام توقف سرویس پاکسازی می‌شود. این دستورات همچنین تحت DynamicUser= به درستی عمل می‌کنند، در حالی که یک mkdir که دستی نوشته شده باشد، چنین نیست.

شرط‌ها بی‌سروصدا شکست می‌خورند. Assertها با سروصدا شکست می‌خورند.

خانواده‌های Condition و Assert تست‌های یکسانی را اجرا می‌کنند. تفاوت آن‌ها فقط در اتفاقی است که هنگام شکست یک تست رخ می‌دهد.

یک Condition...= شکست‌خورده، unit را نادیده می‌گیرد. کارِ شروع (start job) به عنوان موفقیت‌آمیز گزارش می‌شود. unit در وضعیت inactive (dead) باقی می‌ماند، هیچ‌چیز به عنوان شکست‌خورده علامت‌گذاری نمی‌شود، هیچ هشداری ارسال نمی‌شود و journal تنها یک خط را ثبت می‌کند:

Condition check resulted in Inventory API being skipped.

در systemd نسخه 250 و جدیدتر، systemctl status دلیل را مستقیماً چاپ می‌کند:

     Active: inactive (dead)
  Condition: start condition unmet at Thu 2026-08-20 09:14:02 UTC; 2min ago

خط تورفته در زیر آن، دقیقاً همان دستوری (directive) را نام می‌برد که شکست خورده است، برای مثال ConditionPathExists=/etc/inventory/api.conf was not met.

یک Assert...= شکست‌خورده، unit را با شکست مواجه می‌کند. journal عبارت Assertion failed for Inventory API. را ثبت می‌کند و unit در وضعیت failed (Result: assert) قرار می‌گیرد که برای مانیتورینگ به اندازه کافی واضح است.

برای انتخاب بین این دو، از خود بپرسید که شکست یک تست چه معنایی دارد. Condition یعنی «این unit روی این ماشین کاربرد ندارد». Assert یعنی «این شرط باید برقرار باشد و اگر نیست، به کسی اطلاع بده». اکثر unitها به Condition نیاز دارند. تنها زمانی به سراغ Assert بروید که بی‌سروصدا انجام نشدن کار، بدتر از شکست خوردن unit باشد.

دو دام در خانواده Condition وجود دارد.

نخست، یک شرط شکست‌خورده، unitهایی که به آن وابسته‌اند را با شکست مواجه نمی‌کند. اگر a.service دارای Requires=b.service باشد و b.service به دلیل یک شرط نادیده گرفته شود، کارِ شروع برای b.service همچنان به عنوان انجام‌شده در نظر گرفته می‌شود، بنابراین a.service به‌طور عادی در دنیایی شروع می‌شود که b در آن در حال اجرا نیست. یک شرط فقط از unitای محافظت می‌کند که در آن نوشته شده است.

دوم، شرط‌ها هر بار که unit شروع می‌شود، در لحظه اجرای کار ارزیابی می‌شوند. یک unit که توسط یک systemd timer روی یک VPS فعال می‌شود، ممکن است صد بار پشت سر هم نادیده گرفته شود و هرگز وضعیت شکست‌خورده نشان ندهد. این همان نوع عملیات بی‌سروصدایی است که در یک cron job که اجرا می‌شود اما کاری انجام نمی‌دهد دیده می‌شود، و روش یافتن آن نیز یکسان است: به جای اعتماد به وضعیت خروجی (exit state)، journal مربوط به آن unit را بخوانید.

شرط‌هایی که ارزش دانستن روی یک سرور را دارند:

  • ConditionPathExists=/etc/inventory/api.conf و نقیض آن ConditionPathExists=!/etc/inventory/api.conf.
  • ConditionFileNotEmpty= و ConditionDirectoryNotEmpty=، برای یک فایل پیکربندی یا دایرکتوری داده که یک پکیج ایجاد کرده اما خالی رها کرده است.
  • ConditionVirtualization=، تا یک unit که به یک رابط هسته (kernel interface) واقعی نیاز دارد، بتواند ConditionVirtualization=!container را حمل کند. بررسی کنید که سیستم شما با systemd-detect-virt چه چیزی را گزارش می‌دهد.
  • ConditionHost= با hostname یا machine ID مطابقت دارد؛ این همان روشی است که یک فایل unit مشترک روی دو سرور متفاوت رفتار می‌کند.
  • ConditionKernelCommandLine= و ConditionKernelVersion=، برای unitهایی که به یک پارامتر بوت یا حداقل نسخه هسته وابسته هستند.

یک مقداردهی خالی، لیست را پاک می‌کند؛ این همان روشی است که یک drop-in، شرطی که توسط یک پکیج ارائه شده را حذف می‌کند:

[Unit]
ConditionPathExists=
ConditionPathExists=/srv/inventory/api.conf

چرا network.target به معنای متصل بودن شبکه نیست

network.target یک نقطه همگام‌سازی است، نه یک وضعیت. در زمان بوت، قرار گرفتن در ترتیب پس از آن به این معناست که نرم‌افزار مدیریت شبکه اجرا شده است. این به معنای آن نیست که یک اینترفیس دارای آدرس است یا مسیری به اینترنت وجود دارد. این تارگت عمدتاً برای جهت معکوس وجود دارد: یک واحد که پس از After=network.target مرتب شده باشد، پیش از قطع شدن شبکه در زمان خاموش کردن سیستم، متوقف می‌شود.

network-online.target همان چیزی است که منتظر می‌ماند. این تارگت توسط یک سرویس wait-online پشتیبانی می‌شود که متعلق به هر مدیر شبکه‌ای است که اجرا می‌کنید:

  • systemd-networkd-wait-online.service زمانی که systemd-networkd لینک‌ها را مدیریت می‌کند، که حالت معمول در سرورهای Ubuntu پیکربندی‌شده با netplan است.
  • NetworkManager-wait-online.service تحت NetworkManager.

تنظیمات قدیمی‌تر ifupdown همین اثر را از طریق networking.service دریافت می‌کنند. هر کدام را که داشته باشید، استفاده صحیح از این تارگت به دو خط نیاز دارد، نه یک خط.

[Unit]
Wants=network-online.target
After=network-online.target

network-online.target بخشی از تراکنش پیش‌فرض بوت نیست و هیچ‌چیز آن را برای شما فراخوانی نمی‌کند. اگر فقط After= را بنویسید، در حال مرتب‌سازی بر اساس واحدی هستید که هرگز در صف قرار نگرفته است، بنابراین این ترتیب هیچ کاری انجام نمی‌دهد. این همان عملیات بی‌اثر (no-op) است که پیش‌تر توصیف شد، در پرهزینه‌ترین شکل خود. خط Wants= همان چیزی است که تارگت را وارد تراکنش می‌کند تا خط After= چیزی برای انتظار داشته باشد.

نکته دوم این است که "آنلاین بودن" توسط پیاده‌سازی wait-online تعریف می‌شود، نه توسط systemd. systemd-networkd-wait-online زمانی برمی‌گردد که لینک‌های تحت مدیریت آن به وضعیت پیکربندی‌شده برسند. این سرویس بررسی نمی‌کند که آیا DNS پاسخ می‌دهد یا خیر، و بررسی نمی‌کند که آیا هیچ میزبان راه دوری در دسترس است یا خیر.

این تعریف باعث یک خرابی رایج در VPS می‌شود. ماشینی با یک اینترفیس دوم برای شبکه خصوصی که در netplan اعلام شده اما هرگز آدرسی به آن اختصاص نیافته است، باعث می‌شود سرویس انتظار تا زمان تسلیم شدن در همان‌جا بماند:

systemd-networkd-wait-online[612]: Timeout occurred while waiting for network connectivity.
systemd-networkd-wait-online.service: Failed with result 'exit-code'.

بوت دو دقیقه بیشتر طول می‌کشد زیرا تایم‌اوت پیش‌فرض 120 ثانیه است. دو راه حل وجود دارد. اینترفیس استفاده‌نشده را در فایل netplan به صورت optional: true علامت‌گذاری کنید تا networkd از انتظار برای آن دست بکشد. یا یک drop-in روی سرویس انتظار اضافه کنید که لینک مورد نظر شما را با --interface= مشخص کند، یا --any را ارسال کنید تا به محض بالا آمدن یک لینک، بازگردد.

بهتر از آن، از نیاز به این تارگت اجتناب کنید. بسیاری از سرویس‌ها تنها به این دلیل پس از network-online.target مرتب می‌شوند که یک آدرس خاص را bind می‌کنند و در زمان بوت با خطایی مانند این شکست می‌خورند:

nginx: [emerg] bind() to 203.0.113.10:443 failed (99: Cannot assign requested address)

کرنل به دلیل اینکه آن آدرس هنوز بالا نیامده است، bind را رد می‌کند. تنظیم net.ipv4.ip_nonlocal_bind=1 به یک پردازش اجازه می‌دهد آدرسی را bind کند که سیستم هنوز آن را در اختیار ندارد، و یک restart policy بقیه کار را پوشش می‌دهد. به تأخیر انداختن کل فرآیند بوت به دلیل آمادگی شبکه، ابزار سنگینی برای مشکلی است که معمولاً مربوط به یک سوکت است.

نحوه خواندن وابستگی‌های واقعی systemd در یک سیستم در حال اجرا

هرگز تنها بر اساس فایل unit قضاوت نکنید. فایل‌های drop-in، لینک‌های نمادین .wants/ و وابستگی‌های پیش‌فرض ضمنی، همگی پیوندهایی ایجاد می‌کنند که در فایل اصلی دیده نمی‌شوند.

systemctl cat inventory-api.service

این دستور فایل unit و تمام drop-inها را به ترتیبی که اعمال می‌شوند، همراه با مسیر منبع در بالای هر بلوک چاپ می‌کند. ابتدا این دستور را اجرا کنید. یک override پنج‌خطی در /etc/systemd/system/inventory-api.service.d/ بر فایل بسته‌بندی‌شده اولویت دارد و در غیر این صورت نامرئی می‌ماند.

systemctl show inventory-api.service -p Requires -p Wants -p After -p Before -p ConditionResult -p AssertResult

این دستور مقادیر نهایی (resolved) را پس از اعمال drop-inها و وابستگی‌های ضمنی که systemd اضافه کرده است، چاپ می‌کند. ConditionResult=no پاسخ مستقیم به این پرسش است که «چرا unit گزارش موفقیت می‌دهد اما هیچ کاری انجام نمی‌دهد».

systemctl list-dependencies inventory-api.service
systemctl list-dependencies --reverse inventory-api.service
systemctl list-dependencies --after inventory-api.service
systemctl list-dependencies --before inventory-api.service

حالت ساده، درخت Requires= و Wants= را به سمت پایین پیمایش می‌کند. --reverse نشان می‌دهد کدام unitها، unit شما را فراخوانی می‌کنند؛ این همان روشی است که متوجه می‌شوید کدام target باعث شروع آن در زمان بوت می‌شود. --after و --before ترتیب اجرا را نشان می‌دهند و این همان جفتی است که باید هنگام بررسی تأخیر در اجرا مطالعه کنید.

journalctl -b -u inventory-api.service --no-pager
journalctl -b -o short-precise -u inventory-api.service -u postgresql.service

دستور دوم، دو unit را با برچسب‌های زمانی میلی‌ثانیه‌ای با هم ترکیب می‌کند. این روشی است که به جای حدس و گمان، یک race condition در ترتیب اجرا را اثبات می‌کند. خطای pg_isready پیش از لاگ‌های PostgreSQL در database system is ready to accept connections رخ می‌دهد و فاصله زمانی بین آن‌ها دقیقاً در خروجی قابل مشاهده است.

systemd-analyze verify /etc/systemd/system/inventory-api.service
systemd-analyze critical-chain inventory-api.service

verify فایل unit را دقیقاً به همان شکلی که systemd بارگذاری می‌کند، می‌خواند و دستورالعمل‌های ناشناخته، وابستگی به unitهای ناموجود، چرخه‌های وابستگی و سینتکس‌های غیرقابل‌تجزیه را گزارش می‌دهد. این دستور هیچ تغییری در سیستم ایجاد نمی‌کند. critical-chain زنجیره ترتیبی که باعث تأخیر در اجرای unit شده را به همراه زمان فعال شدن هر مرحله چاپ می‌کند و فقط برای unitهایی کار می‌کند که در طول بوت فعلی اجرا شده‌اند.

پس از ویرایش هر فایل unit، دستور sudo systemctl daemon-reload را اجرا کنید. برای تغییر یک unit بسته‌بندی‌شده، از sudo systemctl edit inventory-api.service استفاده کنید که یک drop-in برای شما ایجاد می‌کند. ویرایش فایل اصلی در مسیر /usr/lib/systemd/system/ تا زمان به‌روزرسانی بعدی بسته که آن را جایگزین می‌کند، کار می‌کند. همین مکانیزم drop-in روشی است که می‌توانید محدودیت‌های حافظه و CPU را به یک سرویس بدون دستکاری فایل‌های متعلق به بسته، اعمال کنید.

ترتیب چرخه‌ها و ردی که در ژورنال باقی می‌گذارند

اگر ترتیب را در هر دو جهت اضافه کنید، systemd با حذف یکی از وظایف، چرخه را می‌شکند:

systemd[1]: Found ordering cycle on inventory-api.service/start
systemd[1]: Job postgresql.service/start deleted to break ordering cycle starting with inventory-api.service/start

systemd انتخاب می‌کند که کدام وظیفه حذف شود و ممکن است آن چیزی نباشد که شما انتظار دارید. نتیجه به‌صورت سرویسی ظاهر می‌شود که پس از برخی از rebootها غایب است و پس از برخی دیگر حضور دارد؛ وضعیتی که عیب‌یابی آن از بیرون بسیار دشوار است. بیشتر چرخه‌ها از unitهایی ناشی می‌شوند که DefaultDependencies=no را تنظیم کرده و سپس خود را در مقابل basic.target مرتب می‌کنند، یا از افزودن Before= به unitای که قبلاً After= را داشت و به سمت شما اشاره می‌کرد. systemd-analyze verify آن‌ها را بدون نیاز به reboot پیدا می‌کند.

واحد ثابت

[Unit]
Description=Inventory API
Wants=postgresql.service network-online.target
After=postgresql.service network-online.target
ConditionPathExists=/etc/inventory/api.conf

[Service]
Type=notify
StateDirectory=inventory
ExecStart=/usr/local/bin/inventory-api
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

هر خط یک وظیفه مشخص دارد. دستور Wants= هر دو وابستگی را به تراکنش وارد می‌کند، بدون آنکه طول عمر این واحد را به آن‌ها گره بزند. دستور After= وظیفه انتظار را بر عهده دارد و باید هر دو نام را تکرار کند، زیرا وابستگی و ترتیب‌بندی تنظیمات مجزایی هستند. دستور ConditionPathExists= باعث می‌شود سیستمی که بسته را دارد اما پیکربندی آن را ندارد، بدون ایجاد هشدار از این واحد عبور کند؛ این رفتار برای سرویسی که بر اساس پیکربندی کار می‌کند، صحیح است. دستور Type=notify به این معناست که هر چیزی که پس از این واحد ترتیب‌بندی شده، به جای انتظار برای یک fork، منتظر آماده‌سازی واقعی می‌ماند. دستور Restart=on-failure قطع شدن دیتابیس در زمانی طولانی پس از بوت را پوشش می‌دهد، زیرا ترتیب‌بندی فقط برای اولین شروع اعمال می‌شود. اینکه این تلاش مجدد چقدر تهاجمی باشد، چیزی است که تنظیمات Restart= و RestartSec= کنترل می‌کنند.

پیش از اعتماد به آن، بررسی‌اش کنید:

sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemctl list-dependencies --after inventory-api.service
sudo systemctl start inventory-api.service
systemctl show inventory-api.service -p ConditionResult -p ActiveState -p Result

یک واحد سالم، ConditionResult=yes را با ActiveState=active می‌خواند و Result=success تأیید می‌کند که در آخرین اجرا هیچ خطایی رخ نداده است. مشاهده ConditionResult=no در کنار ActiveState=inactive به این معناست که واحد نادیده گرفته شده است و خط ژورنالی که شرط را نام می‌برد، به شما می‌گوید کدام تست با شکست مواجه شده است.

FAQ

آیا Requires= منتظر شروع شدن واحد دیگر می‌ماند؟

خیر. Requires= و After= تنظیمات مجزایی هستند. Requires= واحد دیگر را به همان تراکنش وارد می‌کند و سپس systemd هر دو کار را به‌صورت موازی شروع می‌کند. برای انتظار، After= را با نام همان واحد اضافه کنید. دلیل دومی هم برای اضافه کردن آن وجود دارد: وابستگی Requires= که در صورت شکست، فقط زمانی از شروع واحد شما جلوگیری می‌کند که After= نیز تنظیم شده باشد، زیرا بدون ترتیب‌بندی، واحد شما تا زمانی که دیگری شکست بخورد، قبلاً شروع شده است.

آیا باید بعد از network.target یا network-online.target ترتیب‌بندی کنم؟

در زمان بوت، network.target فقط به این معنی است که نرم‌افزار مدیریت شبکه شروع شده است، بنابراین هیچ تضمینی در مورد آدرس‌ها یا مسیرها نمی‌دهد. زمانی که سرویس شما در هنگام شروع به یک آدرس فعال نیاز دارد، از network-online.target استفاده کنید و هر دو مورد Wants=network-online.target و After=network-online.target را بنویسید، زیرا این target در تراکنش پیش‌فرض بوت نیست و After= به‌تنهایی منتظر واحدی می‌ماند که کسی آن را در صف قرار نداده است. اگر سرویس فقط به این دلیل شکست می‌خورد که به یک IP خاص متصل می‌شود، net.ipv4.ip_nonlocal_bind=1 همراه با Restart=on-failure سبک‌تر از به تأخیر انداختن بوت است.

چرا واحد من گزارش موفقیت می‌دهد اما هرگز اجرا نمی‌شود؟

یک تست Condition...= ناموفق، واحد را نادیده می‌گیرد و کار شروع را موفق گزارش می‌کند، بنابراین هیچ‌چیز به‌عنوان شکست‌خورده علامت‌گذاری نمی‌شود. دستور systemctl show <unit> -p ConditionResult را اجرا کنید و ConditionResult=no آن را تأیید می‌کند. سپس journalctl -b -u <unit> را برای خط Condition check resulted in <description> being skipped بخوانید. در نسخه 250 و جدیدتر systemd، دستور systemctl status <unit> دقیقاً همان دستوری (directive) که برآورده نشده است را نیز نام می‌برد.

تفاوت بین Condition و Assert چیست؟

آن‌ها تست‌های یکسانی را اجرا می‌کنند. یک Condition ناموفق، واحد را بی‌سروصدا نادیده می‌گیرد و کار همچنان موفقیت‌آمیز باقی می‌ماند. یک Assert ناموفق، واحد را با شکست مواجه می‌کند، Assertion failed for <description>. را لاگ می‌کند و آن را در وضعیت failed (Result: assert) باقی می‌گذارد. از Condition برای مواردی استفاده کنید که «این واحد روی این ماشین کاربرد ندارد»، که تقریباً تمام موارد واقعی را پوشش می‌دهد. از Assert فقط زمانی استفاده کنید که یک پیش‌شرطِ ازدست‌رفته باید برای هر کسی که واحدهای شکست‌خورده را مانیتور می‌کند، قابل مشاهده باشد.

چرا ExecStartPre با وضعیت status=203/EXEC شکست می‌خورد؟

203/EXEC به این معنی است که systemd اصلاً نتوانسته دستور را اجرا کند. دلایل معمول عبارتند از: مسیری که مطلق نیست، باینری که روی آن ماشین وجود ندارد، فایلی که بیت اجرایی ندارد، یا اسکریپتی که خط #! آن به یک مفسرِ موجود اشاره نمی‌کند. سایر کدهای کوچک systemd از یک جدول ثابت می‌آیند، بنابراین status=2/INVALIDARGUMENT فقط به این معنی است که دستور با کد 2 خارج شده و هیچ اطلاعاتی درباره آرگومان‌ها نمی‌دهد. به یاد داشته باشید که ExecStartPre= از طریق shell اجرا نمی‌شود، بنابراین pipeها و globها به /bin/sh -c '...' نیاز دارند.

#systemd#units#dependencies#ordering#troubleshooting