عیبیابی خطاهای Systemd و تحلیل کدهای خروجی
با دستور systemctl status علت اجرا نشدن unit را بیابید. این راهنما معنای کدهای 203/EXEC و 226/NAMESPACE و دلایل توقف ناگهانی سرویسها را پس از اجرا بررسی میکند.
چرا یک unit در systemd اجرا نمیشود
یک unit در systemd که اجرا نمیشود، دلیل آن را در یک فیلد مشخص میکند. دستور systemctl status <unit> را اجرا کنید و به دنبال code= و status= در خطی که خطا را گزارش میکند، بگردید. یک کد وضعیت در محدوده 200 نشان میدهد که systemd هرگز به برنامه شما نرسیده است: این خطا هنگام ساخت محیطی که در فایل unit خود درخواست کردهاید، رخ داده است. یک کد وضعیت کمتر از 200 به این معنی است که برنامه شما اجرا شده و خودش خارج شده است، بنابراین فایل unit احتمالاً درست است و مشکل از خود برنامه است.
این تفکیک، مسیر تصمیمگیری شماست. تمام موارد زیر بر اساس همین منطق و به ترتیبی که اعداد ظاهر میشوند، بررسی میشوند.
سه دستوری که به این پرسش پاسخ میدهند، به ترتیب کدامند
systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
systemd-analyze verify /etc/systemd/system/myapp.servicesystemctl status نتیجه را اعلام میکند. ابتدا خط Loaded: را بخوانید، زیرا فایلی که systemd واقعاً تجزیه کرده است را نام میبرد و میگوید که آیا unit فعال (enabled)، مسدود (masked) یا اصلاً پیدا نشده است. سپس خط Active: و جفت code= و status= زیر آن را بخوانید.
journalctl -u myapp.service -b --no-pager جزئیات را ارائه میدهد. -u خروجی را به همان unit خاص محدود میکند، -b خروجی را به boot فعلی محدود میکند تا مجبور نباشید خطاهای هفته گذشته را بخوانید، و --no-pager خروجی را مستقیماً به ترمینال میفرستد تا بتوانید آن را با pipe به grep منتقل کنید. status فقط چند خط آخر لاگ را نشان میدهد و خطوط طولانی را کوتاه میکند. journal هر آنچه برنامه پیش از توقف چاپ کرده است را نشان میدهد که معمولاً همان خطای اصلی است. برای تاریخچه بیشتر -n 100 را اضافه کنید، یا آن را با -f در یک ترمینال دوم همزمان با restart کردن unit اجرا کنید.
systemd-analyze verify یک فایل unit را بدون اجرا کردن آن بارگذاری میکند. این دستور در مورد بخشها و دستورالعملهای ناشناخته هشدار میدهد و دستوراتی که در ExecStart= هستند و قادر به اجرایشان نیست را علامتگذاری میکند. این کار دو دسته از خطاهای خاموش را شناسایی میکند: یک کلید با املای اشتباه که systemd در زمان بارگذاری با هشداری که اکثر افراد نمیخوانند نادیده میگیرد، و مسیری که وجود ندارد.
پس از ویرایش هر فایل unit، دستور sudo systemctl daemon-reload را اجرا کنید. تا زمانی که این کار را انجام ندهید، systemd به استفاده از نسخهای که قبلاً بارگذاری کرده ادامه میدهد و systemctl status هشداری مبنی بر تغییر فایل روی دیسک اضافه میکند. اصلاحی که «هیچ تأثیری نداشته» اغلب اصلاحی است که systemd هنوز آن را نخوانده است.
دو دستور دیگر نیز جایگاه خود را دارند. systemctl cat myapp.service فایل unit مؤثر را چاپ میکند، یعنی فایل اصلی به اضافه تمام drop-inها در مسیر /etc/systemd/system/myapp.service.d/. systemctl show myapp.service -p ExecStart -p User -p WorkingDirectory آن مقادیر را همانطور که توسط systemd تجزیه شدهاند چاپ میکند، که همان چیزی است که در نهایت اجرا خواهد شد.
خطای status=203/EXEC به چه معناست؟
203/EXEC به این معنی است که systemd مراحل راهاندازی را به پایان رسانده، execve() را فراخوانی کرده، اما هسته سیستمعامل (kernel) درخواست را رد کرده است. برنامه شما حتی یک خط از کد خود را هم اجرا نکرده است. چهار دلیل تقریباً تمام موارد را پوشش میدهند:
- مسیر در
ExecStart=اشتباه است یا به صورت مطلق (absolute) نوشته نشده است. آن را باls -lدر برابر رشته دقیق موجود در فایل unit بررسی کنید. - فایل مجوز اجرا (execute bit) ندارد.
sudo chmod +x /opt/myapp/run.shاین مشکل را حل میکند. فایلی که از یک آرشیو استخراج شده یا از ماشین دیگری کپی شده، اغلب این مجوز را از دست میدهد. - خط shebang خراب است. هسته سیستمعامل خط اول اسکریپت را میخواند و مفسر نامبرده در آن را اجرا میکند؛ بنابراین
#!/usr/bin/env python3زمانی شکست میخورد که PATH سرویس شاملpython3نباشد، یا فایلی که با انتهای خط ویندوزی ذخیره شده، به دنبال مفسری به نام/bin/bash\rبگردد که وجود ندارد. - فایل چیزی نیست که این ماشین بتواند اجرا کند: معماری اشتباه است، یا یک فایل متنی بدون هیچ shebang است.
پیش از تغییر هر چیزی، آن را به صورت دستی و با کاربریِ سرویس بازتولید کنید.
sudo -u appuser /opt/myapp/run.sh
file /opt/myapp/run.sh
head -1 /opt/myapp/run.sh | cat -Afile معماری را نام میبرد و در صورتی که مشکل از انتهای خط باشد، عبارت "with CRLF line terminators" را گزارش میدهد. cat -A همان مورد را به صورت یک ^M در انتهای خط نشان میدهد. آنها را با sed -i 's/\r$//' /opt/myapp/run.sh حذف کنید.
یک نکته مهم در مورد این بازه: عدد 200 به بالا یک قرارداد است، نه یک تضمین. برنامه خودتان آزاد است که با کد 203 خارج شود و systemd نمیتواند تفاوت این دو را تشخیص دهد. systemd-analyze exit-status 203 نام و کلاس هر کد را چاپ میکند که به شما در خواندن جدول کمک میکند، اما اگر برنامه شما کدهای خروجی بالای 199 را انتخاب میکند، آنها را تغییر دهید.
چرا خطای 217/USER یا 216/GROUP دریافت میکنم؟
217/USER به این معناست که حساب کاربری مشخصشده در User= در لحظه شروع سرویس وجود ندارد. 216/GROUP نیز همان خطا برای Group= یا SupplementaryGroups= است. این موضوع را با اجرای دستورات زیر بررسی کنید:
getent passwd appuser
getent group appgroupهر یک از این دستورات یا خروجی چاپ میکنند و یا در صورت عدم وجود، خروجی خالی برگردانده و کد وضعیت غیر صفر میدهند. خروجی خالی به این معناست که نام کاربری برای سیستم ناشناخته است؛ بنابراین systemd نمیتواند به آن کاربر سوئیچ کند و پیش از اجرای سرویس متوقف میشود. راهحل، ایجاد حساب کاربری است، نه حذف User=root. اجرای سرویس تحت یک حساب کاربری سیستمی اختصاصی با حداقل دسترسی، هدف اصلی این دستورالعمل است.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuserDynamicUser=yes با اختصاص یک حساب کاربری موقت توسط systemd برای هر بار اجرا، این مشکل را دور میزند. این روش برای سرویسی که هیچ وضعیت (state) پایداری را ذخیره نمیکند مناسب است. هر سرویسی که فایلی مینویسد، به StateDirectory= در کنار آن نیاز دارد؛ زیرا شناسه کاربری (UID) در هر بار اجرا تغییر میکند و فایلهای موجود در مسیرهای معمولی، متعلق به حسابی خواهند بود که دیگر وجود ندارد.
خطای 226/NAMESPACE چیست؟
226/NAMESPACE ناشی از دستورالعملهای sandboxing است. هنگامی که یک unit گزینههای ProtectSystem=، ProtectHome=، PrivateTmp=، ReadWritePaths= یا هر مورد مشابهی را تنظیم میکند، systemd پیش از اجرای برنامه، یک mount namespace اختصاصی برای آن سرویس ایجاد میکند. namespace در اینجا به معنای یک نمای خصوصی از سیستم فایل برای یک پردازش است. اگر هر mount در این طرح با شکست مواجه شود، سرویس با خطای 226 متوقف میشود و برنامه شما هرگز اجرا نخواهد شد.
دلیل معمول این خطا، وجود مسیری در ReadWritePaths= است که وجود خارجی ندارد. ProtectSystem=strict کل سیستم فایل را به صورت read-only mount میکند و ReadWritePaths= مسیرهای نامگذاریشده را برای نوشتن بازگشایی میکند. systemd نمیتواند دایرکتوریای را که وجود ندارد، بازگشایی کند. دو راه حل مناسب وجود دارد. اجازه دهید systemd دایرکتوری را با StateDirectory= ایجاد کند که در هر بار شروع، /var/lib/<name> را میسازد و آن را به کاربر سرویس اختصاص میدهد، یا از پیشوند - در ابتدای مسیر استفاده کنید که به systemd میگوید در صورت نبود منبع، آن ورودی را نادیده بگیرد. راه حل اشتباه، حذف تنظیمات امنیتی (hardening) است که یک مشکل 5 دقیقهای را با یک مشکل دائمی جایگزین میکند.
[Service]
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
ReadWritePaths=-/srv/uploadsهنگامی که نمیتوانید تشخیص دهید کدام خط مسئول بروز مشکل است، کل بلوک hardening را حذف کنید، تنظیمات را reload کرده و سرویس را start کنید. اگر سرویس بالا آمد، خطوط را یکییکی اضافه کرده و پس از هر بار اضافه کردن، سرویس را restart کنید. دو مورد مشابه در این خانواده 233/RUNTIME_DIRECTORY و 238/STATE_DIRECTORY هستند. این خطاها به این معناست که systemd نتوانسته دایرکتوری نامبردهشده در RuntimeDirectory= یا StateDirectory= را ایجاد کند یا مالکیت آن را به دست بگیرد؛ معمولاً به این دلیل که آن مسیر از قبل وجود دارد و متعلق به کاربر دیگری است.
چرا با وجود صحیح بودن WorkingDirectory، خطای 200/CHDIR ظاهر میشود؟
200/CHDIR به این معناست که chdir() به مسیر WorkingDirectory= با شکست مواجه شده است. این دایرکتوری وجود ندارد یا کاربر سرویس اجازه ورود به آن را ندارد. ورود به یک دایرکتوری نیازمند دسترسی execute روی آن دایرکتوری و تمامی دایرکتوریهای والد آن است؛ بنابراین یک /home/deploy/app که کاملاً قابل خواندن است، در صورتی که /home/deploy دارای مجوز 700 باشد و سرویس با کاربر appuser اجرا شود، غیرقابل دسترس خواهد بود.
sudo -u appuser test -x /srv/myapp && echo ok
namei -l /srv/myappnamei -l مالک و مجوز هر بخش از مسیر را چاپ میکند که سریعترین راه برای یافتن دایرکتوری مسدودکننده است. نوشتن WorkingDirectory=-/srv/myapp باعث میشود نبود یک دایرکتوری منجر به توقف سرویس نشود. این تنظیم برای برنامهای که محل شروع اجرای آن اهمیتی ندارد مناسب است، اما برای برنامهای که فایلها را با مسیر نسبی باز میکند، اشتباه است.
چرا سرویس شروع میشود و یک ثانیه بعد متوقف میگردد؟
در اینجا هیچ کد وضعیت سری 200 وجود ندارد و اغلب هیچ متن خطایی نیز دیده نمیشود. واحد بلافاصله پس از شروع وضعیت inactive (dead) را نشان میدهد یا در چرخه activating (auto-restart) قرار میگیرد. systemd محیط را بهدرستی ساخته است. عدم تطابق بین کاری که برنامه شما انجام میدهد و آنچه Type= وعده داده بود، وجود دارد.
حالت Type=simple که پیشفرض است، میگوید برنامه در پیشزمینه (foreground) باقی میماند. اگر به آن دیمونی بدهید که fork شده و به پسزمینه میرود و سپس خارج میشود، systemd پایان پردازش اصلی را میبیند و سرویس را تمامشده تلقی میکند. اکثر دیمونها پرچمی برای ماندن در پیشزمینه دارند، مانند nginx -g 'daemon off;'.
حالت Type=forking میگوید اولین پردازش پس از آماده شدن فرزندش خارج میشود. اگر به آن برنامهای در پیشزمینه بدهید، job شروع منتظر میماند تا TimeoutStartSec= تمام شود (بهطور پیشفرض 90 ثانیه)، سپس systemd آن را میکشد و یک timeout ثبت میکند.
حالت Type=notify میگوید برنامه برای اعلام آمادگی، sd_notify() را فراخوانی میکند. برنامهای که از این قابلیت پشتیبانی نمیکند، چیزی اعلام نمیکند؛ بنابراین زمان شروع به پایان میرسد و journal نتیجه را به عنوان یک خطای پروتکل ثبت میکند.
نوع مناسب را بر اساس عملکرد واقعی برنامه انتخاب کنید. تفاوت بین simple، forking، oneshot و notify تصمیمی است که این دسته از خطاها را بهطور کامل حل میکند.
وقتی یک سرویس بارها و بارها خارج میشود، systemd تلاش مجدد را متوقف کرده و گزارش میدهد که درخواست شروع بیش از حد سریع تکرار شده است. واحد تا زمانی که پنجره محدودیت نرخ بگذرد یا شما sudo systemctl reset-failed myapp.service را اجرا نکنید، در وضعیت failed باقی میماند. افزایش این محدودیت فقط نشانه را پنهان میکند. journal را از اولین خطا به جای آخرین خطا بخوانید و پیش از تغییر آن، ببینید Restart=on-failure واقعاً چه چیزی را دوباره امتحان میکند.
چرا unit غیرفعال است اما هیچ خطایی گزارش نمیشود؟
یک unit ممکن است بهجای اجرا، نادیده گرفته شود. دستورالعملهای Condition* ذاتاً بیصدا هستند: وقتی شرط بررسی برقرار نباشد، systemd عملیات را موفقیتآمیز تلقی کرده و هیچ کاری انجام نمیدهد. unitای که دارای ConditionPathExists=/etc/myapp/config.yml باشد، تا زمانی که آن فایل وجود نداشته باشد هرگز اجرا نمیشود و هیچ خطایی هم گزارش نخواهد کرد.
systemctl show myapp.service -p ConditionResult -p ConditionTimestamp
journalctl -u myapp.service -b --no-pager | grep -i conditionدستور ConditionResult=no نادیده گرفتهشدن unit را تأیید میکند و journal نام بررسیای که برقرار نبوده را ذکر میکند. زمانی که فقدان یک پیشنیاز باید منجر به شکستِ آشکار شود، از دستورالعمل Assert* استفاده کنید. شرطها، تأییدیهها و ترتیببندی unitها توضیح میدهد که هر بررسی در کجا باید قرار بگیرد.
چند مورد بیصدای دیگر نیز وجود دارد. خطای "could not be found" معمولاً به این معنی است که فایل در دایرکتوری اشتباه قرار دارد یا شما تنظیمات را reload نکردهاید: فایلهای unit که خودتان مینویسید باید در /etc/systemd/system/ قرار بگیرند. یک unit ماسکشده (masked) تا زمانی که sudo systemctl unmask myapp.service آن را آزاد نکند، از هرگونه اجرا امتناع میورزد. همچنین systemctl enable روی unitای که فاقد بخش [Install] باشد شکست میخورد، بنابراین برای آن WantedBy=multi-user.target تعریف کنید.
اگر پردازش به جای شکست خوردن، کشته شده باشد چه؟
code=killed با code=exited تفاوت دارد. چیزی از خارج، پردازش را خاتمه داده است. status=9/KILL به قاتل حافظه (OOM killer) اشاره میکند و journal نام پردازشی که انتخاب شده را ذکر میکند. محدودیتی که خودتان تعیین کردهاید نیز همین کار را در داخل cgroup (گروه کنترل) انجام میدهد، بنابراین حافظه آزاد روی میزبان را با free -m بررسی کنید و unit را برای MemoryMax= چک کنید. MemoryMax, CPUQuota و سایر محدودیتهای cgroup توضیح میدهد که کدام محدودیت باعث کشتن پردازش و کدامیک فقط باعث کند شدن آن میشود.
status=15/TERM بلافاصله پس از تلاش برای شروع، معمولاً به این معنی است که systemd زمان شروع را تمامشده تلقی کرده و پردازش را خاتمه داده است، که شما را به Type= بازمیگرداند.
دو عادت که از اکثر این خرابیها جلوگیری میکند
همهجا از مسیرهای مطلق (Absolute Paths) استفاده کنید. systemd شل ورود شما را اجرا نمیکند، بنابراین خبری از .bashrc، .profile و محیطهای مجازی فعالشده نیست. $PATH برای یک سرویس سیستمی، یک لیست کوتاه و پیشفرض است که شامل /opt یا شیمهای (shims) مدیریتکننده نسخه زبان نخواهد بود. /usr/bin/python3 یا /opt/myapp/venv/bin/python را بهطور کامل بنویسید. دستور command -v myapp در شل شما، مسیر دقیق را برای کپی کردن چاپ میکند. همین قانون در مورد WorkingDirectory=، EnvironmentFile= و هر مسیر دیگری در ReadWritePaths= نیز صدق میکند.
ExecStart= یک شل نیست. systemd خط دستور را به کلمات مجزا تقسیم کرده و مستقیماً execve() را فراخوانی میکند. پایپها (pipes)، تغییر مسیرها (redirections)، گلابها (globs)، &&، بکتیکها و ~ هیچ معنایی ندارند: آنها بهعنوان آرگومانهای متنی به برنامه شما ارسال میشوند. ExecStart=/usr/bin/myapp --flag > /tmp/out.log مقادیر > و /tmp/out.log را به myapp میدهد که در نهایت با یک خطای استفاده (usage error) خارج میشود؛ خطایی که اصلاً شبیه به یک مشکل در systemd نیست. هر زمان به قابلیتهای شل نیاز داشتید، صراحتاً از یک شل استفاده کنید.
ExecStart=/bin/sh -c '/usr/bin/myapp --flag | /usr/bin/tee -a /var/log/myapp.log'برای خروجی گرفتن، نیازی به این کار ندارید. خروجی سرویس بهصورت پیشفرض به journal ارسال میشود و StandardOutput=append:/var/log/myapp.log بدون دخالت هیچ شلی، در فایل مینویسد.
بسط متغیرها (Variable expansion) نیز به همین شکل محدود است. $MYVAR و ${MYVAR} از Environment= و EnvironmentFile= جایگزین میشوند و هیچ چیز دیگری بسط داده نمیشود. $HOME برای یک سرویس سیستمی تنظیم نمیشود مگر اینکه خودتان آن را تنظیم کنید. یک EnvironmentFile= نیز اسکریپت شل نیست: export در آن جایگاهی ندارد، قوانین کوتیشنگذاری آن با bash متفاوت است و نبود یک فایل، یک خطای مهلک محسوب میشود مگر اینکه مسیر را با - پیشوند کنید.
کار روی یک سرور زنده
کد را بخوانید، علت را اثبات کنید، یک مورد را تغییر دهید و سپس سرویس را restart کنید. این ترتیب اهمیت بیشتری از دانستن تکتک اعداد دارد، زیرا مانع از آن میشود که سه تغییر حدسی را همزمان اعمال کنید و ندانید کدامیک مشکل را حل کرده است. همین مسیر برای unitهایی که خودتان ننوشتهاید نیز کارآمد است. تایمری که هرگز اجرا نمیشود، در واقع سرویسی است که هرگز شروع نشده است؛ بنابراین ابتدا سرویس را عیبیابی کنید: یک systemd timer و سرویسی که آن را فعال میکند دقیقاً به همان روشهای بالا دچار خطا میشود، با این تفاوت که تایمر خروجی را پنهان میکند تا زمانی که آن را از journal درخواست کنید.
FAQ
خطای status=203/EXEC در systemctl status به چه معناست؟
سیستم systemd تمام پیشنیازهای واحد (unit) را آماده کرده، اما فراخوانی execve() با شکست مواجه شده است؛ بنابراین برنامه شما هرگز اجرا نشده است. این چهار مورد را به ترتیب بررسی کنید: مسیر موجود در ExecStart= باید وجود داشته و مطلق (absolute) باشد، فایل باید مجوز اجرا (execute bit) داشته باشد، مفسر ذکر شده در خط اول (shebang) باید در PATH سرویس موجود باشد، و فایل باید از انتهای خط یونیکس (Unix line endings) استفاده کند. دستور file برای مورد آخر عبارت "with CRLF line terminators" را گزارش میدهد که باعث میشود نام مفسر به /bin/bash\r تغییر کرده و هسته سیستمعامل از اجرای آن خودداری کند.
چرا سرویس من بلافاصله پس از شروع متوقف میشود؟
فایل واحد (unit file) رفتاری را وعده میدهد که برنامه آن را ندارد. در حالت Type=simple، سیستم systemd انتظار دارد برنامه در پیشزمینه (foreground) باقی بماند؛ بنابراین دیمونی که با fork شدن به پسزمینه میرود، در لحظه fork شدن از نظر سیستم پایانیافته تلقی میشود. در حالت Type=forking، سیستم systemd منتظر خروج اولین پردازش میماند؛ بنابراین یک برنامه پیشزمینه باعث میشود عملیات شروع تا زمان اتمام TimeoutStartSec= معلق بماند. مقدار Type= را با رفتار برنامه مطابقت دهید و اگر برنامه پرچم (flag) اجرای در پیشزمینه را دارد، از آن به همراه مقدار پیشفرض Type=simple استفاده کنید.
چگونه میتوانم به جای خروجی کوتاه وضعیت، خطای واقعی را ببینم؟
دستور systemctl status فقط چند خط آخر ژورنال را چاپ کرده و خطوط طولانی را کوتاه میکند. برای مشاهده تمام لاگهای ثبتشده توسط واحد در طول این بوت، دستور journalctl -u myapp.service -b --no-pager را اجرا کنید، برای مشاهده پنجره بزرگتر از -n 200 استفاده کنید، یا خروجی را به grep پایپ (pipe) کنید. اگر برنامه فایل لاگ اختصاصی خود را مینویسد، آن را نیز مطالعه کنید، زیرا systemd تنها مواردی را ثبت میکند که برنامه به خروجی استاندارد (stdout) و خطای استاندارد (stderr) میفرستد.
چرا واحد (unit) من بدون هیچ پیام خطایی غیرفعال است؟
در بیشتر موارد، یک دستورالعمل Condition* باعث نادیده گرفته شدن آن شده است. این بررسیها بیصدا هستند: یک شرط شکستخورده، عملیات شروع را موفقیتآمیز علامتگذاری میکند. دستور systemctl show myapp.service -p ConditionResult را اجرا کرده و به دنبال ConditionResult=no بگردید، سپس خطی از ژورنال که نام آن بررسی را ذکر کرده است، بخوانید. دلیل رایج دیگر، masked بودن واحد است که تا زمان اجرای دستور sudo systemctl unmask، از هرگونه شروع مجدد جلوگیری میکند.
آیا پس از هر تغییر در فایل واحد به daemon-reload نیاز دارم؟
بله، برای هر ویرایش در فایل واحد یا فایلهای drop-in. دستور sudo systemctl daemon-reload باعث میشود systemd فایلها را دوباره از دیسک بخواند و سپس sudo systemctl restart myapp.service آنها را روی سرویس در حال اجرا اعمال کند. پس از اجرای systemctl edit نیازی به این کار نیست، زیرا این دستور عملیات reload را برای شما انجام میدهد؛ همچنین پس از تغییر فایل پیکربندی که متعلق به خود برنامه است (نه متعلق به systemd)، نیازی به این کار نخواهید داشت.