systemd: أعد تشغيل الخدمة يوميًا — بلا سكربتات cron
أعد تشغيل خدمة systemd يوميًا عبر مؤقت لا عبر cron: OnCalendar=daily وRandomizedDelaySec وPersistent. مختبر على systemd 262 — والإثباتات في المتن.
المؤقّت (timer) هو الطريقة المدمجة لإعادة تشغيل خدمة systemd وفق جدول. OnCalendar=daily يختار منتصف الليل، وRandomizedDelaySec يوزّع الحمل بعد 00:00، وPersistent=true يعوّض ما فات بعد التوقف. يستطيع cron تنفيذ إعادة التشغيل نفسها — لكنه أعمى: في منتصف الليل بالضبط، جنبًا إلى جنب مع كل مهام الجهاز الأخرى، ومن دون أي سجل لآخر مرة أطلق فيها. على منصّة الاختبار الليلة أطلق المؤقّت ثلاث مرات في خمس وتسعين ثانية، والسجل سمّى كل واحدة بدقّة عُشر الثانية.
كيف تبدو وصفة إعادة التشغيل اليومية؟
ملفان. الأول لا يعرف خدعة واحدة — إعادة تشغيل التطبيق. والثاني يقرّر المتى. ولا يمسّ أيٌّ منهما وحدة التطبيق نفسها.
# /etc/systemd/system/app-restart.service
[Unit]
Description=scheduled restart of app
[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl try-restart app.service
# /etc/systemd/system/app-restart.timer
[Unit]
Description=restart app once a day
[Timer]
OnCalendar=daily
RandomizedDelaySec=45m
Persistent=true
[Install]
WantedBy=timers.target
تحقّق: sudo systemctl daemon-reload && sudo systemctl enable --now app-restart.timer && systemctl list-timers app-restart.timer — عمود NEXT ممتلئ يعني أن المؤقّت صار مالك الجدول.
try-restart لا restart: إذا صادف أن التطبيق متوقف، فإن restart العادي سيشغّله بصمت — ربما يُعيد إحياء ما أوقفته عن قصد. أما try-restart فلا يمسّ إلا ما يعمل أصلًا. الخدمات التي يجب ألا تبقى متوقفة تبدّلها إلى restart وتنام مرتاحة.
لماذا لا تستخدم cron فحسب؟
لأن عكاز cron يعمل مرة واحدة ثم يتكئ عليك. السطر @daily systemctl restart app سطر واحد في crontab الجذر — ثم تبدأ الأسئلة: متى أطلق آخر مرة؟ هل أطلق أصلًا بينما الجهاز مطفأ؟ لماذا ختمت كل مهام الجهاز 00:00:00 معًا؟ صفحة الدليل لـ cron تعرّف @daily بأنها منتصف الليل تمامًا؛ فتمرّ كل الخدمات المبرمجة هكذا من البوابة نفسها في الثانية نفسها، وأول من يمرّ هو القرص.
الدفتر الصادق: cron يعيد تشغيل التطبيق، وهذا كل ما يفعله. لا عمود list-timers، ولا ختم على القرص، ولا تعويض بعد التوقف، ولا وحدة في السجل تقرأها — بفرض أن خفي cron مثبّت أصلًا. صورة الجهاز الأساسية في منصّة الاختبار لدينا لا تتضمنه. أما على جهاز Arch حديث فالمؤقّتات موجودة فعلًا تحت PID 1، تنتظر.
| السؤال | cron @daily | المؤقّت |
|---|---|---|
| متى أطلق آخر مرة؟ | grep في السجلات، ربما | list-timers، عمود LAST |
| الجهاز مطفأ في منتصف الليل؟ | تخطٍّ صامت | Persistent يعوّض عند الإقلاع |
| متى يعمل بالضبط؟ | 00:00:00 تمامًا | 00:00 زائد التأخير المسحوب |
| خفي إضافي يشغّله؟ | cron أو crond | لا شيء — PID 1 يفعل ذلك أصلًا |
| أين السجلات؟ | بالبريد، إن وُجدت | journalctl -u، مهيكلة |
الحالة العامة — متى يتفوّق المؤقّت على سطر cron ولماذا — تسكن مقالتنا السابقة، cron مقابل مؤقّتات systemd: cron ينفّذ أسطرًا والمؤقّتات تنفّذ وحدات. وهذه مجرد الحالة الخاصة بإعادة التشغيل الليلية.
كيف أوقف تدافع منتصف الليل؟
سطر واحد: RandomizedDelaySec=45m. بعد كل استحقاق في التقويم يسحب المؤقّت تأخيرًا عشوائيًا جديدًا، فتهبط دزينة من إعادة التشغيل الليلية في أي موضع داخل النافذة بدل لحظة واحدة. جرّبت المنصّة نسخة مصغّرة بدقيقة من الوصفة — تقويم عند :30 وتأخير محدود بعشرين ثانية — وكل سحبة هبطت في موضع آخر:
Nov 03 05:13:48.974739 bench-arch systemd[1]: Starting scheduled restart of app...
RandomizedDelayUSec=20s
LastTriggerUSec=Tue 2026-11-03 05:14:40 UTC
NextElapseUSecRealtime=Tue 2026-11-03 05:15:43 UTCاقرأ الحساب من الأسطر: تقويم 30، حدّ 20 — خرجت السحب عند 18.97 و10.42 و13 ثانية. ثلاثة إطلاقات، ثلاث ثوانٍ مختلفة، كلها تحت الحدّ. cron لا يسحب؛ صفحة دليله تبرمج إلى الثانية الثابتة، إلى الأبد. أعِد الرفع إلى 45m فتتوزّع إعادة تشغيلك الليلية على النافذة، ولا يلتقي القرص بحشد أبدًا.
وماذا لو كان الجهاز مطفأ في موعد إعادة التشغيل؟
Persistent=true هو بند التعويض. بعد كل إطلاق يلمس المؤقّت ختمًا من صفر بايت — /var/lib/systemd/timers/stamp-app-restart.timer — ويقارنه systemd عند الإقلاع بالتقويم. الجهاز مطفأ طوال الليل والإقلاع في 09:00: إعادة التشغيل الفائتة تعمل فورًا، مرة واحدة. من دون هذا السطر، يُتخطّى الإطلاق الفائت بصمت وينتظر التطبيق يومًا كاملًا إعادة الضبط. الختم وعدٌ من صفر بايت: عليّ لك إعادة تشغيل واحدة بعد الإقلاع التالي.
-rw-r--r-- 1 root root 0 Nov 3 05:13 stamp-app-restart.timer
-rw-r--r-- 1 root root 0 Oct 24 10:42 stamp-archlinux-keyring-wkd-sync.timerالسطر الثاني ليس لنا: صورة الجهاز الأساسية تتضمن ذلك المؤقّت ممكّنًا ومختومًا سلفًا. النمط هو عادة المجدول الافتراضي نفسه — تحديثات keyring في Arch تعوّض ما بعد التوقف بالطريقة نفسها التي سيفعلها تطبيقك.
أليس Restart=always الشيء نفسه؟
لا، والخلط بينهما هكذا تُختلق أعطال منتصف الليل. Restart=always يقوده الفشل: يخرج العملية الرئيسية فيعيدها systemd بعد RestartSec. ما دام التطبيق يجري سليمًا، لا يطلق أبدًا — لا شيء يتفاعل معه. لا يستطيع القيام بصيانة تقويمية، لأن السلامة ليست تاريخًا. المؤقّت يقوده التقويم: تحين 00:00 فتُنفَّذ إعادة التشغيل، سليمًا كان أم لا. احتفظ بالاثنين إن احتجت الاثنين — Restart=always للانهيارات والمؤقّت لإعادة الضبط الليلية. يراقبان ساعتين مختلفتين ولا يتصارعان أبدًا.
كيف أتحقق أنه أطلق فعلًا؟
systemctl list-timers app-restart.timer --no-pager
journalctl -u app-restart.service --since today --no-pager
تحقّق: بعد منتصف الليل يمتلئ NEXT لليوم التالي ويجلس LAST داخل نافذة تأخيرك. سطر السجل المهم يقرأ Starting scheduled restart of app... — يليه إيقاف التطبيق وتشغيله ذاته:
Nov 03 05:13:48.990989 bench-arch systemd[1]: Stopping the app that gets restarted nightly...
Nov 03 05:13:48.993916 bench-arch systemd[1]: Started the app that gets restarted nightly.إعادة التشغيل الليلية اعترافٌ: شيئًا ما يتسرّب، ولم يجد أحد الثغرة بعد.
بدّل خدمتك الليلة عبر القائمة، واترك المؤقّت يحمل الاعتراف حتى يصلح أحدٌ التسرّب حقًا — ثم احذف الوحدتين ودَع الجهاز ينام عبر منتصف الليل. تلك هي حالة الخروج، وملف الختم لن يشتاق إليك.
منهجية المنصّة، لتقرأ الإيصالات الخام بنفسك
جهاز Arch مؤقت (صورة cloud من quickemu، بلا تغييرات على المضيف): خدمة app.service واحدة تنفّذ sleep infinity، ووصفة المؤقّت مصغّرة إلى OnCalendar=*-*-* *:*:30 مع RandomizedDelaySec=20 ليقع الإطلاق داخل دقيقة انتظار، ثم خمس وتسعون ثانية من مراقبة السجل. ثلاثة إطلاقات وملف الختم وأعمدة list-timers ونسخة systemd، كلها في سجل إيصالات واحد طُبع بلا تحرير أعلاه. الإطلاق الثالث على المنصّة وقع على ساعة كانت الآلة تضبطها بعد عند الإقلاع؛ حسابه هو السطر الوحيد الذي لا يدافع عنه هذا المقال، والسجل يحتفظ به.