المستوى: متوسط — الشرح ده موجّه لمهندس عنده خدمة شغّالة على سيرفر لينكس (VPS) ويعرف أساسيات الطرفية وsystemctl. مش محتاج خبرة عميقة في الـ DevOps، بس تكون سبق شغّلت خدمة بـ systemd.
في آخر الدليل ده هيكون عندك خدمة بترجّع نفسها تلقائيًا خلال ثوانٍ بعد أي كراش، بتحمي نفسها من لوب إعادة التشغيل، وبتبعتلك تنبيه لحظة ما تستسلم فعلًا. من غير ما تصحى الساعة 3 الفجر.
الشفاء الذاتي للخدمات على لينكس بـ systemd
المشكلة باختصار
خدمتك وقعت الساعة 2 بالليل. العميل اكتشفها الساعة 9 الصبح. إنت اكتشفتها من شكوى العميل. الفجوة دي — من لحظة الكراش لحظة ما حد يعمل حاجة — اسمها MTTR (متوسط زمن الاستعادة). وأغلب الوقت بيضيع في إن محدش عارف إن فيه حاجة وقعت أصلًا.
الطريقة الشائعة إن الناس بتفتكر إن systemctl start مرة واحدة كفاية. غلط. الخدمة أول ما تكرش، بتفضل ميتة لحد ما حد يدخل يدويًا. الحل مش سكربت cron بيتأكد كل دقيقة — ده بيضيف تأخير ومشاكل. الحل إن systemd نفسه يراقب الخدمة ويصلّحها، وده شغله الأصلي.
المثال الأول: الحارس اللي بيفتح الباب كل ما يتقفل
تخيّل حارس واقف على باب. كل ما الباب يتقفل بالغلط، بيفتحه تاني على طول. بس لو الباب اتقفل 5 مرات في دقيقة، الحارس بيفهم إن فيه مشكلة حقيقية (مش صدفة)، فبيبطّل يفتح ويرنّ جرس ينبّه المدير. ده بالظبط اللي بيعمله systemd: إعادة تشغيل سريعة للأعطال العابرة، ووقفة وتنبيه للأعطال المتكررة.
علميًا: systemd هو مدير العمليات (init/PID 1) في أغلب توزيعات لينكس. هو اللي بيشغّل خدمتك ويراقب حالتها. لمّا العملية بتخرج بكود غير صفري أو بتتقتل بإشارة، systemd بيقدر يعيد تشغيلها تلقائيًا حسب سياسة إنت بتحددها في ملف الوحدة (unit file).
الطبقة 1: إعادة التشغيل التلقائي مع back-off
أضف السطرين Restart وRestartSec في قسم [Service]. Restart=on-failure بيعيد التشغيل فقط عند الفشل (خروج غير صفري، إشارة قتل، timeout، أو Watchdog)، ومش بيعيد لو الخدمة خرجت بنجاح بكود صفر — وده اللي إنت عايزه.
# /etc/systemd/system/myapp.service
[Unit]
Description=My App API
StartLimitIntervalSec=60
StartLimitBurst=5
OnFailure=notify-failure@%n.service
[Service]
Type=notify
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Restart=on-failure
RestartSec=2
WatchdogSec=30
User=myapp
[Install]
WantedBy=multi-user.target
RestartSec=2 بتخلّي systemd يستنى ثانيتين قبل كل محاولة، بدل ما يفضل يحاول في نفس اللحظة ويخنق السيرفر. ده الـ back-off البسيط.
الطبقة 2: احرس اللوب — امنع عاصفة إعادة التشغيل
الخطر الحقيقي: خدمة بتكرش فورًا كل ما تقوم (مثلًا بسبب config غلط). من غير حارس، systemd هيفضل يقوم ويقع آلاف المرات في الثانية ويحرق المعالج. هنا بييجي دور StartLimitIntervalSec وStartLimitBurst في قسم [Unit].
المعنى: لو الخدمة اتعادت أكتر من 5 مرات (StartLimitBurst=5) خلال 60 ثانية (StartLimitIntervalSec=60)، systemd بيستسلم ويحطها في حالة failed ويبطّل يحاول. ده بيمنع الـ crash loop من إسقاط السيرفر كله، وبيدّيك إشارة واضحة إن فيه عطل جذري محتاج تدخّل بشري.
الطبقة 3: الـ Watchdog — اكتشف الخدمة "الحيّة الميتة"
أصعب حالة: الخدمة مش واقعة، العملية موجودة، بس معلّقة (deadlock أو حلقة لا نهائية) ومش بترد على أي طلب. من بره شكلها حيّة، من جوه ميتة. إعادة التشغيل العادية مش هتكتشفها لأن العملية ما خرجتش.
الحل: WatchdogSec. الخدمة بتبعت "نبضة قلب" لـ systemd كل فترة عبر sd_notify. لو systemd ما وصلتوش نبضة خلال المدة المحددة، بيعتبر الخدمة معلّقة، بيقتلها، وبيعيد تشغيلها (لأن Restart=on-failure بيغطّي حالة الـ watchdog كمان). لاحظ إن الوحدة فوق فيها Type=notify وWatchdogSec=30.
القاعدة: ابعت النبضة كل أقل من نص المدة. يعني مع WatchdogSec=30، ابعت كل 10-15 ثانية.
# app.py — pip install sdnotify
import time
from sdnotify import SystemdNotifier
n = SystemdNotifier()
n.notify("READY=1") # قول لـ systemd إنك جاهز
while True:
do_work() # شغلك الحقيقي
n.notify("WATCHDOG=1") # نبضة القلب
time.sleep(10)
الطبقة 4: نبّهني وقت ما تستسلم — OnFailure
لاحظت السطر OnFailure=notify-failure@%n.service فوق؟ ده بيقول لـ systemd: أول ما الخدمة تدخل حالة failed، شغّل وحدة تانية تبعتلي تنبيه. الـ %n بيتحوّل لاسم الخدمة اللي فشلت. اعمل قالب وحدة (template) واحد يخدم كل خدماتك:
# /etc/systemd/system/notify-failure@.service
[Unit]
Description=Send alert when %i failed
[Service]
Type=oneshot
EnvironmentFile=/etc/notify-failure.env
ExecStart=/usr/local/bin/notify-failure.sh %i
#!/usr/bin/env bash
# /usr/local/bin/notify-failure.sh (chmod +x)
UNIT="$1"
HOST="$(hostname)"
STAMP="$(date -u +%FT%TZ)"
TEXT="[ALERT] الخدمة ${UNIT} فشلت على ${HOST} في ${STAMP}"
curl -s -X POST -H 'Content-Type: application/json' \
-d "{\"text\":\"${TEXT}\"}" "$SLACK_WEBHOOK_URL" > /dev/null
حط رابط الـ webhook في /etc/notify-failure.env بصيغة SLACK_WEBHOOK_URL=https://hooks.slack.com/.... نفس الفكرة بتشتغل مع Telegram أو أي API تنبيهات.
التحقق من إنه شغّال
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
# اقتل العملية بالقوة كأنها كرشت:
sudo systemctl kill -s SIGKILL myapp.service
# راقب اللوج وهي بترجّع نفسها:
journalctl -u myapp.service -f
# عدّ كام مرة اتعادت:
systemctl show myapp.service -p NRestarts
الأرقام: قبل وبعد
سيناريو واقعي: عندك API على VPS بيخدم حوالي 8000 طلب/يوم، وبيكرش مرة كل أسبوع بسبب تسريب ذاكرة. من غير أوتوميشن، متوسط الوقت لحد ما حد يلاحظ ويعيد التشغيل ~7 دقايق (420 ثانية) — وده لو حد صاحي أصلًا. مع الطبقة 1، الخدمة بترجّع في ~2-4 ثواني (RestartSec=2). يعني الـ MTTR نزل من ~420 ثانية لـ ~3 ثواني، تحسّن حوالي 140 ضعف. الأرقام دي تقديرية وبتختلف حسب حالتك، بس رتبة الحجم واقعية.
الـ trade-offs
إعادة التشغيل التلقائي بتكسب سرعة استعادة هائلة، بتخسر إنها بتداري السبب الجذري: الباج اللي سبب الكراش لسه موجود، وإنت مش حاسس بيه لأن الخدمة بترجّع بصمت. عشان كده الطبقة 4 (التنبيه) مش رفاهية.
الـ Watchdog بيكسب اكتشاف التعليق، بيخسر تعقيد في الكود (لازم تضيف sd_notify) وخطر إنه يقتل خدمة بطيئة لسبب شرعي (مثلًا وقفة GC طويلة) لو خليت WatchdogSec قصير أوي. الافتراض إن نبضتك أسرع من نص المدة بمسافة أمان.
متى لا تستخدم هذه الطريقة
- لو الكراش بسبب فساد بيانات على القرص: إعادة التشغيل هتدخلك crash loop. الـ StartLimit هيوقف اللوب، بس الخدمة هتفضل down لحد ما تصلّح البيانات بإيدك.
- لو خدمتك بتشتغل جوه Kubernetes: الـ liveness و readiness probes بتعمل نفس الدور. متعملش الطبقتين، هتتعارض.
- للمهام لمرة واحدة (batch/one-shot jobs): استخدم
Restart=no. مش المفروض ترجّع نفسها. - لو الحالة كلها في الذاكرة من غير persistence: إعادة التشغيل هتفقد الحالة. حل التخزين الأول، وبعدين فعّل الشفاء الذاتي.
الخطوة التالية
افتح unit file لأهم خدمة عندك دلوقتي، ضيف Restart=on-failure وRestartSec=2، وفي قسم [Unit] ضيف StartLimitBurst=5 وStartLimitIntervalSec=60. اعمل systemctl daemon-reload، وبعدين اقتل العملية بـ systemctl kill -s SIGKILL وشوف systemctl show -p NRestarts. لو رجعت لوحدها، الطبقة الأولى بقت شغّالة، وبعدها ضيف التنبيه.
المصادر
- systemd.service(5) — توثيق
Restart،RestartSec،WatchdogSec،Type=notifyالرسمي. - systemd.unit(5) — توثيق
StartLimitIntervalSec،StartLimitBurst، وOnFailure. - sd_notify(3) — بروتوكول النبضة
WATCHDOG=1وREADY=1.