لنفرض أنك في شركة ناشئة تبيع منتجاتها عبر الإنترنت، ولديها واجهة برمجية صغيرة للطلبات مكتوبة بلغة Python تعمل على سيرفر Linux واحد. والذي شغلها أول مرة دخل إلى السيرفر عبر SSH وكتب nohup python3 app.py & ثم أغلق الجلسة، وبقيت تعمل أسابيع دون مشاكل. ثم توقفت في ليلة ما بسبب خطأ نادر في الكود، ولم يعرف أحد حتى الصباح حين بدأت رسائل العملاء تصل، وبعد أسبوع أعاد مزود الاستضافة تشغيل السيرفر لتحديث أمني فلم تعد الواجهة إطلاقاً، لأن أحداً لم يخبر السيرفر أن هذا البرنامج يجب أن يعمل بعد الإقلاع. وعندما فتح الفريق الملف nohup.out ليعرف ماذا حدث وجده ملفاً كبيراً بلا أوقات ولا مستويات، وفي آخره سطر الخطأ دون أي إشارة إلى متى حدث.
وكل هذه المشاكل حلها موجود في كل سيرفر Linux حديث، واسمه systemd: البرنامج الذي يعمل كأول عملية في النظام، ويشغل الخدمات Services عند الإقلاع بالترتيب الصحيح، ويراقبها ويعيد تشغيلها عندما تتوقف، ويجمع كل ما تطبعه في سجل واحد Journal تبحث فيه بالأمر journalctl. وفي هذا الدليل سوف نأخذ نفس واجهة الطلبات من nohup إلى خدمة systemd كاملة خطوة بخطوة، وكل مخرج تراه هنا خرج من تشغيل حقيقي، بما في ذلك ثلاث مشاكل واجهتنا ولم نكن نتوقعها. وإذا كنت جديداً على الطرفية فابدأ بمقال سطر الأوامر في Linux للمبتدئين ثم ارجع إلى هنا.
وسوف نناقش في هذا المقال ما يلي:
- لماذا لا يكفي
nohup، وما هو systemd ولماذا يعمل كالعملية رقم 1. - كيف شغلنا systemd داخل Container للتجربة، ولماذا اخترنا ذلك بدلاً من جهاز افتراضي.
- كتابة ملف الوحدة Unit File سطراً سطراً، والفرق بين التشغيل Start والتفعيل Enable.
- مشكلة السجلات التي تصل متأخرة بوقت خاطئ، وكيف أصلحناها بملف إضافي Drop-in.
- إعادة التشغيل التلقائي Restart، وحد البدء Start Limit، ولماذا بقيت خدمتنا تحاول إلى ما لا نهاية.
- السجلات مع journald والأمر journalctl: البحث بالخدمة والمستوى والوقت والنص، وتحديد حجم السجلات.
- الترتيب والاعتماد بين الخدمات، وتقوية الخدمة Hardening، ولماذا لم تعمل إعدادات الحماية دون أن تخبرنا.
- الأخطاء الشائعة، وقائمة تحقق لكل خدمة جديدة.
لماذا لا يكفي nohup؟
قبل أن نكتب أي ملف إعداد لنرى الطريقة القديمة كما هي. والأمر nohup يفعل شيئاً واحداً فقط: يمنع وصول إشارة إغلاق الجلسة SIGHUP إلى البرنامج، فيبقى يعمل بعد أن تخرج من SSH، ويحول مخرجه إلى ملف. والتجربة التالية تشغل واجهة الطلبات بهذه الطريقة، ثم ترسل طلباً عادياً، ثم تطلب المسار /crash الذي يجعل البرنامج ينهي نفسه برمز خطأ كما يحدث في أي خطأ غير متوقع، ثم تحاول الوصول مرة أخرى:
cd /opt/orders
PORT=8080 API_KEY=x nohup python3 app.py > /root/nohup.out 2>&1 &
sleep 1
curl -s localhost:8080/
curl -s localhost:8080/crash
sleep 0.5
curl -s localhost:8080/ || echo "curl exit $?"
cat /root/nohup.out
pgrep -a -x python3 || echo "no orders process"والمخرج سوف يكون كما يلي:
orders ok pid=92
curl exit 7
--- nohup.out
<5>orders starting on port 8080 as uid=0
<6>GET / from 127.0.0.1
<2>crash requested, exiting with code 3
no orders processفي المخرج أعلاه لاحظ التالي:
- الطلب الأول نجح، والطلب بعد التوقف فشل برمز 7 من
curl، وهو رمز «لا يوجد من يستمع على هذا المنفذ»، ولم يحاول أي شيء إعادة البرنامج. - السطر الأول في الملف يقول
uid=0، أي أن البرنامج يعمل بالمستخدم root لأن الجلسة كانت root، وأي ثغرة في واجهة الطلبات تعطي المخترق السيرفر كله. - السطور لا تحمل أي وقت، فلا تعرف متى بدأ البرنامج ولا متى توقف، والأرقام بين الأقواس مثل
<2>مستويات سجل كتبها البرنامج ولكن لا أحد هنا يفهمها، وسوف نرى أن systemd يفهمها. - ولا يظهر في التجربة ما يحدث عند إعادة الإقلاع، والجواب معروف: لا شيء، فلا يوجد في النظام أي سجل يقول إن هذا البرنامج يجب أن يعمل.
وقد يتساءل البعض: لماذا لا نكتب سكربتاً صغيراً يعيد تشغيل البرنامج في حلقة، ونضيفه إلى crontab بالخيار @reboot؟ والإجابة أن هذا ممكن، وكثيرون فعلوه لسنوات، ولكنك بذلك تكتب نسخة ضعيفة من شيء موجود ومختبر في كل توزيعة: لن تعرف حالة البرنامج إلا بالبحث عن رقمه، ولن يكون لسجلاته وقت ولا حد، وإذا توقف السكربت نفسه فلا أحد يعيده، ولن تستطيع أن تقول «شغل هذا بعد أن تجهز الشبكة» أو «لا تعطه أكثر من 512 MB». والصورة التالية تجمع الفرق:

ما هو systemd، ولماذا يعمل كالعملية رقم 1؟
عندما تنتهي النواة من الإقلاع تشغل برنامجاً واحداً فقط، وهو العملية رقم 1 التي تسمى init، وكل عملية أخرى في النظام تولد منها أو من أبنائها. ولسنوات طويلة كانت هذه العملية في أغلب التوزيعات نظام SysV init، الذي يشغل سكربتات Shell طويلة من المجلد /etc/init.d واحداً بعد الآخر بترتيب أرقام في أسمائها. وكانت هذه الطريقة بطيئة لأنها تشغل كل شيء بالتتابع، وهشة لأن كل سكربت يكتب منطق التشغيل والإيقاف ومعرفة الحالة بنفسه، فيختلف سلوكها من خدمة إلى أخرى، ولا تراقب الخدمة بعد أن تبدأ. وفي عام 2010 نشر Lennart Poettering مقاله Rethinking PID 1 يعرض فيه فكرة systemd: ملفات إعداد قصيرة تصف ما تريد بدلاً من سكربتات تقول كيف، وتشغيل الخدمات بالتوازي التي لا يعتمد بعضها على بعض، ومراقبة دائمة لكل خدمة. وخلال السنوات التالية اعتمدته Fedora ثم Debian وUbuntu وأغلب التوزيعات الكبرى، وصار اليوم هو ما تجده على أي سيرفر جديد تقريباً.
والكلمة الأساسية في systemd هي الوحدة Unit، وهي أي شيء يديره systemd ويصفه ملف إعداد. وأشهر أنواع الوحدات:
- الخدمة Service: برنامج يعمل في الخلفية مثل nginx وPostgreSQL وواجهة الطلبات، والملف ينتهي بـ
.service. - الهدف Target: مجموعة من الوحدات تمثل حالة يصل إليها النظام، مثل
multi-user.targetوهو النظام الكامل دون واجهة رسومية، وهو ما يصل إليه أي سيرفر عند الإقلاع. - المؤقت Timer: يشغل خدمة في وقت محدد، وهو البديل الحديث لـ Cron، وسوف نخصص له مقالاً مستقلاً بإذن الله.
- التركيب Mount، والمقبس Socket، والجهاز Device، وغيرها، وكلها تدار بنفس الأوامر.
وكل ما يعرفه systemd عن الوحدات تجده في صفحة systemd.unit(5)، وكل ما يخص الخدمات في صفحة systemd.service(5)، وهما المرجع الذي نرجع إليه في كل إعداد في هذا المقال. والأداة التي تتحكم بها في كل هذا هي systemctl، والأداة التي تقرأ بها السجلات هي journalctl.
تجهيز المختبر: systemd داخل Container
الطريقة الطبيعية لتجربة systemd هي جهاز Linux حقيقي أو جهاز افتراضي Virtual Machine، فهناك يكون systemd هو العملية رقم 1 من الإقلاع. ولكن الجهاز الافتراضي يحتاج نظاماً كاملاً بعدة غيغابايتات وإقلاعاً كاملاً في كل مرة، بينما أردنا مختبراً يبدأ في ثوان ويحذف بأمر واحد. والمشكلة أن الـ Container في Docker لا يشغل systemd عادة، فالفكرة فيه أن يشغل برنامجاً واحداً يكون هو العملية رقم 1، كما يشرح توثيق Docker عن تشغيل أكثر من خدمة في Container. والحل الوسط أن نجعل systemd نفسه هو البرنامج الذي يشغله الـ Container، فيصبح هو العملية رقم 1 داخله ويدير الخدمات كما يفعل في السيرفر. وهذا ملف الـ Image:
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends systemd systemd-sysv dbus python3 procps iproute2 curl ca-certificates less && rm -rf /var/lib/apt/lists/* \
&& systemctl mask [email protected] console-getty.service systemd-logind.service \
&& rm -f /etc/systemd/system/multi-user.target.wants/* /lib/systemd/system/multi-user.target.wants/getty.target 2>/dev/null; true
STOPSIGNAL SIGRTMIN+3
CMD ["/lib/systemd/systemd"]وفي الملف أعلاه لاحظ التالي:
- السطر
CMDيجعل systemd هو أول ما يعمل في الـ Container، بدلاً من Shell أو تطبيق. - حجبنا بالأمر
systemctl maskخدمات الطرفيات وتسجيل الدخول، لأنه لا توجد شاشة ولا لوحة مفاتيح في الـ Container، وبدون ذلك تبقى هذه الخدمات تحاول وتملأ السجل بأخطاء لا تخصنا. - السطر
STOPSIGNAL SIGRTMIN+3يجعلdocker stopيرسل إلى systemd الإشارة التي يفهمها كطلب إيقاف منظم للنظام، فيوقف الخدمات واحدة واحدة كما يحدث عند إطفاء السيرفر.
وتشغيل الـ Container يحتاج بعض الخيارات، لأن systemd يتوقع أن يجد مجلدات معينة كما في نظام حقيقي، وهذا السكربت setup.sh كاملاً:
#!/bin/sh
# starts the lab: a container whose PID 1 is systemd, then installs the orders service files
set -e
docker run -d --name sbl-sd --tmpfs /run --tmpfs /run/lock --tmpfs /tmp \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw --cgroupns=host -p 19360:8080 sbl-systemd:1
sleep 3
docker exec sbl-sd mkdir -p /opt/orders /etc/orders
docker cp app.py sbl-sd:/opt/orders/app.py
docker cp orders.env sbl-sd:/etc/orders/orders.env
docker exec sbl-sd useradd --system --no-create-home --shell /usr/sbin/nologin orders
docker exec sbl-sd chown root:orders /etc/orders/orders.env
docker exec sbl-sd chmod 640 /etc/orders/orders.envوالخيارات --tmpfs تعطي systemd مجلدات /run و/tmp في الذاكرة كما يتوقعها، والخيار -v /sys/fs/cgroup مع --cgroupns=host يسمح له بإنشاء مجموعات التحكم cgroups التي يضع فيها كل خدمة ليعرف عملياتها ويحد مواردها. وهذا الإعداد مناسب لمختبر على جهازك فقط، لأنه يعطي الـ Container رؤية لمجموعات التحكم في السيرفر كله، فلا تستخدمه لتشغيل خدمات حقيقية. وبعد التشغيل تأكدنا أن systemd يعمل فعلاً كالعملية رقم 1:
systemd 257 (257.13-1~deb13u1)
PID COMMAND
1 systemdأما البرنامج نفسه فهو ملف app.py صغير يقدم واجهة HTTP على المنفذ 8080، ويرفض البدء إذا لم يجد المتغير API_KEY، وفيه مساران للتجربة: /crash الذي ينهي البرنامج برمز 3، و/slow الذي ينتظر 5 ثوان. ولاحظ أن كل سطر يطبعه يبدأ برقم بين قوسين مثل <3>، وسوف نعرف فائدة ذلك في قسم السجلات:
# orders API used in the lab: a tiny HTTP service with endpoints that fail on purpose
import os, sys, time
from http.server import BaseHTTPRequestHandler, HTTPServer
PORT = int(os.environ.get("PORT", "8080"))
API_KEY = os.environ.get("API_KEY")
if not API_KEY:
print("<3>API_KEY is not set, refusing to start")
sys.exit(2)
class H(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/crash":
print("<2>crash requested, exiting with code 3")
sys.stdout.flush()
os._exit(3)
if self.path == "/slow":
time.sleep(5)
print(f"<6>GET {self.path} from {self.client_address[0]}")
self.send_response(200); self.end_headers()
self.wfile.write(f"orders ok pid={os.getpid()}\n".encode())
def log_message(self, *a):
pass
print(f"<5>orders starting on port {PORT} as uid={os.getuid()}")
HTTPServer(("0.0.0.0", PORT), H).serve_forever()كتابة ملف الوحدة Unit File
ملفات الوحدات التي تكتبها أنت توضع في المجلد /etc/systemd/system/، أما المجلد /lib/systemd/system/ فهو للملفات التي تأتي مع الحزم، ولا تعدل فيه لأن أول تحديث للحزمة سوف يمسح تعديلك. وهذا ملف واجهة الطلبات كاملاً، واسمه orders.service:
[Unit]
Description=Orders API
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=orders
Group=orders
WorkingDirectory=/opt/orders
EnvironmentFile=/etc/orders/orders.env
ExecStart=/usr/bin/python3 /opt/orders/app.py
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.targetوالملف مقسم إلى ثلاثة أقسام، ولكل قسم سؤال يجيب عليه كما في الصورة التالية:

وفي الإعداد أعلاه لاحظ التالي:
Description: الاسم الذي تراه في كل سطر سجل وفيsystemctl status، فاكتبه بحيث يفهمه من يقرأ السجل في منتصف الليل.WantsوAfter: الأول يقول «اطلب تشغيل هدف الشبكة الجاهزة معي»، والثاني يقول «لا تبدأني قبل أن يجهز»، وسوف نرجع إلى الفرق بينهما في قسم الترتيب.Type=simple: يعني أن البرنامج الذي فيExecStartهو الخدمة نفسها ويبقى يعمل في المقدمة، وهذا هو الحال في أغلب التطبيقات الحديثة. والنوعforkingللبرامج القديمة التي تنسخ نفسها إلى الخلفية ثم يخرج الأصل، والنوعnotifyللبرامج التي تخبر systemd بنفسها أنها جاهزة عبر واجهة sd_notify، فلا يعتبرها جاهزة لحظة بدء العملية وإنما لحظة جهوزيتها فعلاً.UserوGroup: الخدمة تعمل بمستخدم نظام اسمهordersأنشأناه بالخيار--systemودون مجلد شخصي ودون Shell، فلا يمكن الدخول به، وأي خطأ في التطبيق يبقى محصوراً في صلاحيات هذا المستخدم. وقد شرحنا المستخدمين والصلاحيات في مقال صلاحيات الملفات Permissions في Linux.EnvironmentFile: المتغيرات السرية مثلAPI_KEYفي ملف منفصل صلاحياته 640، يقرؤه root والمجموعةordersفقط، ولا تكتبها داخل ملف الوحدة نفسه، والسبب أن ملفات الوحدات يقرؤها أي مستخدم على السيرفر، ويعرضها الأمرsystemctl catلأي أحد.ExecStart: الأمر بمسار كامل، لأن systemd لا يستخدم المتغير PATH الذي اعتدت عليه في الطرفية، وهذا من أكثر أسباب فشل الخدمات الجديدة.Restart=on-failureوRestartSec=2: أعد تشغيل الخدمة إذا توقفت بخطأ، وانتظر ثانيتين قبل كل محاولة، وهذا السطر هو الذي كان ينقص الفريق في تلك الليلة.WantedBy=multi-user.target: لا يعني شيئاً حتى تكتبsystemctl enable، وعندها تصبح الخدمة جزءاً من هذا الهدف فتبدأ مع كل إقلاع.
التشغيل والتفعيل وقراءة الحالة Status
بعد نسخ الملف نخبر systemd أن ملفات الوحدات تغيرت بالأمر daemon-reload، لأنه لا يراقب المجلد ولا يقرأ الملفات من جديد وحده، ثم نفعل الخدمة ونشغلها بأمر واحد، ثم نقرأ حالتها:
systemctl daemon-reload
systemctl enable --now orders
sleep 2
systemctl status orders --no-pager
curl -s localhost:8080/
journalctl -u orders --no-pagerوالمخرج:
Created symlink '/etc/systemd/system/multi-user.target.wants/orders.service' → '/etc/systemd/system/orders.service'.
● orders.service - Orders API
Loaded: loaded (/etc/systemd/system/orders.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-10-30 17:54:23 UTC; 2s ago
Invocation: 8d11895920dc427fa308e148ff9d3800
Main PID: 134 (python3)
Tasks: 1 (limit: 14999)
Memory: 8.4M
CGroup: /docker/d5300749f7270a34d54025a0c14c9b4b38db4a0da67a43ce9874861defa50493/system.slice/orders.service
└─134 /usr/bin/python3 /opt/orders/app.py
Oct 30 17:54:23 d5300749f727 systemd[1]: Started orders.service - Orders API.
---
orders ok pid=134
Oct 30 17:54:23 d5300749f727 systemd[1]: Started orders.service - Orders API.وهذا المخرج هو أول ما تنظر إليه في أي خدمة، فلنقرأه سطراً سطراً:
- السطر الأول يبين ما فعله
enableبالضبط: أنشأ رابطاً Symlink من مجلد الهدفmulti-user.target.wantsإلى ملف الخدمة، ولا شيء غير ذلك. وإلغاء التفعيل بالأمرdisableيحذف هذا الرابط. Loaded: أين وجد systemd الملف، والكلمةenabledتعني أنها مفعلة للإقلاع، والعبارةpreset: enabledتعني أن السياسة الافتراضية للتوزيعة تفعل الخدمات الجديدة.Active: active (running)مع وقت البدء والمدة، وهذا ما تبحث عنه أولاً. والحالات الأخرى التي سوف تراها:inactive (dead)أي متوقفة بشكل طبيعي، وfailedأي توقفت بخطأ، وactivating (auto-restart)أي تنتظر إعادة التشغيل.Invocation: رقم فريد لهذا التشغيل بعينه، يتغير مع كل إعادة تشغيل، ويفيد في فصل سجلات كل تشغيل عن غيره.Main PIDوTasksوMemory: رقم العملية الرئيسية، وعدد العمليات والخيوط، والذاكرة التي تستخدمها الخدمة كلها، وsystemd يعرف هذه الأرقام لأنه يضع كل خدمة في مجموعة تحكم cgroup خاصة بها.CGroup: مسار هذه المجموعة وشجرة العمليات داخلها، وهنا عملية واحدة، وفي خدمة مثل nginx أو PostgreSQL سوف ترى العملية الرئيسية وأبناءها كلهم.- آخر السطور من سجل الخدمة، وهنا يوجد سطر واحد من systemd نفسه يقول إن الخدمة بدأت.
والصورة التالية تفصل بين الأمرين اللذين يخلط بينهما كثيرون:

مشكلة واجهتنا: السجلات تتأخر بسبب التخزين المؤقت Output Buffering
ارجع إلى المخرج السابق ولاحظ ما ينقصه: البرنامج يطبع سطراً عند البدء وسطراً لكل طلب، وقد أرسلنا طلباً ونجح، ومع ذلك لم يظهر في سجل الخدمة إلا سطر systemd. ثم طلبنا المسار /crash وقرأنا السجل مرة أخرى:
Oct 30 17:54:23.158668 d5300749f727 systemd[1]: Started orders.service - Orders API.
Oct 30 17:54:39.354938 d5300749f727 python3[134]: orders starting on port 8080 as uid=996
Oct 30 17:54:39.354938 d5300749f727 python3[134]: GET / from 127.0.0.1
Oct 30 17:54:39.354938 d5300749f727 python3[134]: crash requested, exiting with code 3
Oct 30 17:54:39.357305 d5300749f727 systemd[1]: orders.service: Main process exited, code=exited, status=3/NOTIMPLEMENTED
Oct 30 17:54:39.357305 d5300749f727 systemd[1]: orders.service: Failed with result 'exit-code'.
Oct 30 17:54:41.561967 d5300749f727 systemd[1]: orders.service: Scheduled restart job, restart counter is at 1.
Oct 30 17:54:41.563513 d5300749f727 systemd[1]: Started orders.service - Orders API.ظهرت السطور الثلاثة فجأة، وكلها بنفس الوقت حتى الجزء من المليون من الثانية: 17:54:39.354938. والبرنامج بدأ فعلاً في 17:54:23، أي قبل 16 ثانية من الوقت المسجل. والسبب أن Python عندما يرى أن مخرجه ليس طرفية، وإنما أنبوباً يذهب إلى journald، يجمع ما تطبعه في ذاكرة مؤقتة Buffer ولا يرسله حتى تمتلئ أو حتى يطلب البرنامج التفريغ، وفي حالتنا لم يحدث التفريغ إلا في سطر sys.stdout.flush() قبل الخروج. وjournald يسجل وقت وصول السطر إليه لا وقت كتابته، فتحصل على سجل بأوقات خاطئة، وأسوأ من ذلك أنه لو توقف البرنامج بطريقة لا تفرغ الذاكرة، مثل SIGKILL، لضاعت هذه السطور كلها ولم تعرف أبداً ماذا كان يفعل قبل أن يتوقف:

والحل أن نطلب من Python عدم التخزين المؤقت للمخرج، إما بالخيار -u في سطر التشغيل أو بالمتغير PYTHONUNBUFFERED=1 كما يشرح توثيق سطر أوامر Python. ونفس المشكلة موجودة بأشكال أخرى في لغات غير Python، فابحث عن إعداد التفريغ في لغة تطبيقك. ولكي لا نعدل الملف الأصلي استخدمنا ملفاً إضافياً Drop-in، وهو ملف صغير في مجلد اسمه اسم الخدمة مع .d، يضيف إعدادات أو يغير إعدادات فوق الملف الأصلي. وهذا ما يفعله الأمر systemctl edit orders في السيرفر الحقيقي، حيث يفتح لك المحرر ويحفظ الملف في المكان الصحيح، وفي المختبر كتبناه مباشرة:
mkdir -p /etc/systemd/system/orders.service.d
printf "[Service]\nEnvironment=PYTHONUNBUFFERED=1\n" > /etc/systemd/system/orders.service.d/override.conf
systemctl daemon-reload
systemctl restart orders
systemctl cat orders --no-pagerوالأمر systemctl cat يعرض الملف الأصلي وكل الملفات الإضافية فوقه، وهو أول ما تكتبه عندما تريد أن تعرف الإعداد الفعلي لخدمة لم تكتبها أنت:
# /etc/systemd/system/orders.service
[Unit]
Description=Orders API
...
# /etc/systemd/system/orders.service.d/override.conf
[Service]
Environment=PYTHONUNBUFFERED=1وبعد الإصلاح أعدنا نفس التجربة، فصار لكل سطر وقته الحقيقي:
Oct 30 17:54:56.893475 d5300749f727 python3[175]: orders starting on port 8080 as uid=996
Oct 30 17:54:57.831674 d5300749f727 python3[175]: GET /health from 127.0.0.1
Oct 30 17:54:57.843570 d5300749f727 python3[175]: crash requested, exiting with code 3ولاحظ أيضاً أن السطر الأول يقول uid=996، أي أن البرنامج يعمل الآن بالمستخدم orders وليس root. والدرس هنا أعم من Python: الملفات الإضافية هي الطريقة الصحيحة لتعديل أي خدمة، خصوصاً الخدمات التي تأتي مع الحزم، لأن تحديث الحزمة يستبدل الملف الأصلي ويترك ملفك الإضافي كما هو.
إعادة التشغيل التلقائي Restart
ارجع الآن إلى السطور الأخيرة من سجل التوقف، فهي القصة كاملة كما يرويها systemd: Main process exited, code=exited, status=3/NOTIMPLEMENTED أي أن العملية خرجت بالرمز 3، والاسم NOTIMPLEMENTED مجرد اسم قياسي يعطيه systemd لهذا الرقم ولا علاقة له ببرنامجنا، ثم Failed with result 'exit-code' أي أن سبب الفشل رمز خروج غير صفر، ثم بعد ثانيتين تقريباً Scheduled restart job, restart counter is at 1، ثم Started من جديد. ومن طرف العميل لم يلاحظ أحد أكثر من ثانيتين من الانقطاع، وهذا ما كان ينقص الفريق في القصة. وعرض الأمر systemctl show نفس الأرقام:
MainPID=148
NRestarts=1
ExecMainStatus=0والقيمة NRestarts تعد مرات إعادة التشغيل التلقائي، وهي رقم يستحق المراقبة، فخدمة تعيد التشغيل كل ساعة تعمل في نظر status ولكن فيها مشكلة. وجربنا بعد ذلك حالتين أخريين: قتل العملية بالإشارة 9 كما يفعل قاتل العمليات OOM Killer عند نفاد الذاكرة، ثم إيقافها بالطريقة الرسمية:
P=$(systemctl show -p MainPID --value orders)
kill -9 $P
sleep 3
journalctl -u orders --no-pager -o short-precise --since "-4s"
systemctl stop orders
systemctl show orders -p ActiveState -p NRestartsOct 30 17:56:30.480200 d5300749f727 systemd[1]: orders.service: Main process exited, code=killed, status=9/KILL
Oct 30 17:56:30.480387 d5300749f727 systemd[1]: orders.service: Failed with result 'signal'.
Oct 30 17:56:32.554232 d5300749f727 systemd[1]: orders.service: Scheduled restart job, restart counter is at 1.
Oct 30 17:56:32.556147 d5300749f727 systemd[1]: Started orders.service - Orders API.
...
NRestarts=1
ActiveState=inactiveوالفرق بين السجلين مهم في التشخيص: code=exited يعني أن البرنامج أنهى نفسه برمز، وcode=killed يعني أن إشارة من الخارج أنهته، ومعها رقم الإشارة. وفي الحالتين أعاد systemd التشغيل لأن كلاهما فشل، أما systemctl stop فأوقف الخدمة ولم يعدها، لأنه طلب منك أنت. والقيم المتاحة للخيار Restart كما في صفحة systemd.service(5):
| القيمة | متى يعيد التشغيل | متى تستخدمها |
|---|---|---|
no | أبداً، وهي القيمة الافتراضية | مهام تعمل مرة واحدة |
on-failure | رمز خروج غير صفر، أو إشارة، أو انتهاء مهلة، أو فشل المراقبة Watchdog | أغلب الخدمات، وهي الأنسب للبداية |
on-abnormal | الإشارات والمهلات فقط، وليس رموز الخروج | برامج تخرج برمز خطأ عن قصد عندما يكون إعدادها خاطئاً |
always | مهما كان سبب التوقف، حتى الخروج الطبيعي برمز 0 | برامج يجب أن تعمل دائماً ولو أنهت نفسها بشكل طبيعي |
ولاحظ أن RestartSec الافتراضي 100 ملي ثانية فقط، وهذا يقودنا إلى المشكلة الثانية.
حد البدء Start Limit: الخدمة التي لم تتوقف عن المحاولة
إعادة التشغيل تنقذك من خطأ نادر، ولكن ماذا لو كان الخطأ في الإعداد نفسه، فالبرنامج يتوقف فور بدئه في كل مرة؟ لتجربة ذلك عطلنا السطر API_KEY في ملف المتغيرات، فصار البرنامج يخرج برمز 2 فوراً، ثم أعدنا التشغيل وانتظرنا 16 ثانية:
sed -i "s/^API_KEY=/#API_KEY=/" /etc/orders/orders.env
systemctl restart orders
sleep 16
systemctl status orders --no-pager● orders.service - Orders API
Loaded: loaded (/etc/systemd/system/orders.service; enabled; preset: enabled)
Drop-In: /etc/systemd/system/orders.service.d
└─override.conf
Active: activating (auto-restart) (Result: exit-code) since Fri 2026-10-30 17:55:30 UTC; 1s ago
Invocation: 4e2a3ca28db2483ebd6fba4f650b81ff
Process: 206 ExecStart=/usr/bin/python3 /opt/orders/app.py (code=exited, status=2)
Main PID: 206 (code=exited, status=2)كنا نتوقع أن يصل systemd إلى حد البدء ويعلن الفشل، فالحد الافتراضي كما رأيناه في systemctl show هو 5 محاولات في 10 ثوان:
StartLimitIntervalUSec=10s
StartLimitBurst=5ولكن الحالة كانت activating (auto-restart)، وبعد 20 ثانية أخرى سألنا عن العداد فوجدناه NRestarts=23 والخدمة ما زالت تحاول. والسبب في السجل نفسه: المحاولات كانت في 16.06 ثم 18.56 ثم 21.06 ثم 23.31 ثم 25.56، أي محاولة كل 2.25 ثانية تقريباً، والحد يحسب المحاولات داخل نافذة من 10 ثوان تبدأ من أول محاولة، فتدخل 5 محاولات في النافذة وهو المسموح بالضبط، ثم تبدأ نافذة جديدة. وبهذا الشكل فإن RestartSec=2 مع الحد الافتراضي يجعل الخدمة تحاول إلى ما لا نهاية، وهي تملأ السجل بنفس الخطأ كل ثانيتين، ولا تظهر في قائمة الخدمات الفاشلة لأنها ليست فاشلة رسمياً. ولكي نرى الحد يعمل غيرنا المدة إلى 100 ملي ثانية:
printf "[Service]\nEnvironment=PYTHONUNBUFFERED=1\nRestartSec=100ms\n" > /etc/systemd/system/orders.service.d/override.conf
systemctl daemon-reload
systemctl reset-failed orders
systemctl restart orders
sleep 4
systemctl list-units --failed --no-pagerOct 30 17:56:11.806572 d5300749f727 systemd[1]: orders.service: Scheduled restart job, restart counter is at 5.
Oct 30 17:56:11.806698 d5300749f727 systemd[1]: orders.service: Start request repeated too quickly.
Oct 30 17:56:11.806708 d5300749f727 systemd[1]: orders.service: Failed with result 'exit-code'.
Oct 30 17:56:11.806947 d5300749f727 systemd[1]: Failed to start orders.service - Orders API.
---
UNIT LOAD ACTIVE SUB DESCRIPTION
● orders.service loaded failed failed Orders APIهذه المرة توقف systemd بعد خمس محاولات في أقل من ثانية، وكتب الرسالة التي ستراها كثيراً: Start request repeated too quickly، وصارت الخدمة في قائمة --failed. ومن هنا لن يعيدها systemd ولو أصلحت الإعداد، حتى تكتب systemctl reset-failed orders أو تشغلها بنفسك. والصورة التالية تجمع الحالتين:

فأيهما الصحيح؟ لا توجد إجابة واحدة، ولكن يجب أن تختار عن قصد. فالحلقة التي لا تنتهي مناسبة لخدمة تعتمد على شيء خارجي قد يعود وحده، مثل قاعدة بيانات تعيد التشغيل، فتنتظر الخدمة حتى تعود. والتوقف عند الحد مناسب عندما يكون الخطأ غالباً في الإعداد، فلا فائدة من المحاولة كل ثانيتين لأسابيع، والأفضل أن تظهر الخدمة في قائمة الفشل فيراها نظام المراقبة. ويمكنك ضبط الحد نفسه بالخيارين StartLimitIntervalSec وStartLimitBurst في القسم [Unit]، وفي الإصدارات الحديثة يوجد أيضاً RestartSteps وRestartMaxDelaySec لزيادة المدة بين المحاولات تدريجياً، فتبدأ سريعة ثم تبطئ، وهو حل وسط جيد. وبعد التجربة أعدنا السطر API_KEY ورجعنا إلى الإعداد السابق:
sed -i "s/^#API_KEY=/API_KEY=/" /etc/orders/orders.env
printf "[Service]\nEnvironment=PYTHONUNBUFFERED=1\n" > /etc/systemd/system/orders.service.d/override.conf
systemctl daemon-reload
systemctl reset-failed orders
systemctl start ordersالسجلات Logs مع journald والأمر journalctl
كل ما تطبعه الخدمة على المخرج القياسي أو مخرج الأخطاء يذهب إلى خدمة اسمها systemd-journald، دون أن تكتب في برنامجك أي سطر خاص بالسجلات. وjournald لا يحفظ النص فقط، وإنما يضيف إلى كل سطر عشرات الحقول: الخدمة، ورقم العملية، والمستخدم، والوقت بدقة الجزء من المليون، ورقم الإقلاع، ومستوى الأهمية، ويحفظ كل ذلك في ملفات ثنائية مفهرسة، فتبحث فيها بسرعة بأي حقل:

ولكي ترى هذه الحقول طلبنا آخر سطر من سجل الخدمة بكل تفاصيله:
journalctl -u orders -n 1 -o json-pretty --no-pager{
"_UID" : "996",
"_CMDLINE" : "/usr/bin/python3 /opt/orders/app.py",
"_TRANSPORT" : "stdout",
"PRIORITY" : "6",
"_SYSTEMD_UNIT" : "orders.service",
"_SYSTEMD_INVOCATION_ID" : "ec47932a9d01447bae049b92fe6bdaa0",
"_BOOT_ID" : "b9d3ff9dede046a4a4921a3f645b0107",
"__REALTIME_TIMESTAMP" : "1793383016149176",
"SYSLOG_IDENTIFIER" : "python3",
"_PID" : "32",
"MESSAGE" : "GET / from 127.0.0.1",
...
}اختصرنا المخرج إلى أهم الحقول، والقائمة الكاملة ومعنى كل حقل في صفحة systemd.journal-fields(7). ولاحظ أن الحقول التي تبدأ بشرطة سفلية يكتبها journald نفسه ولا يستطيع البرنامج تزويرها، فالحقل _UID هو المستخدم الحقيقي للعملية مهما كتب البرنامج في رسالته. ولاحظ أيضاً الحقل PRIORITY بالقيمة 6، وهذا يعيدنا إلى الأرقام بين القوسين في بداية كل سطر.
مستوى الأهمية Priority: لماذا كتبنا <3> في بداية السطر؟
السجلات في Linux لها ثمانية مستويات موروثة من syslog، من 0 وهو emerg إلى 7 وهو debug، والمستوى 3 هو err والمستوى 6 هو info. وsystemd يفهم أي سطر يبدأ برقم بين قوسين، فيحذف الرقم من الرسالة ويجعله مستوى السطر، وهذا سلوك الخيار SyslogLevelPrefix المفعل افتراضياً كما في صفحة systemd.exec(5). ولذلك ظهرت الرسائل في السجل دون الأرقام، بينما ظهرت كما هي في nohup.out. والفائدة أنك تستطيع أن تطلب الأخطاء فقط:
journalctl -u orders --no-pager -p err -o short-preciseOct 30 17:54:39.354938 d5300749f727 python3[134]: crash requested, exiting with code 3
Oct 30 17:54:57.843570 d5300749f727 python3[175]: crash requested, exiting with code 3والخيار -p err يعني المستوى err وكل ما هو أخطر منه، ولهذا ظهر سطر التوقف الذي كتبناه بالمستوى 2 وهو crit، ولم تظهر سطور الطلبات العادية. وإذا كانت مكتبة السجلات في لغتك تكتب المستوى كنص مثل ERROR فكل السطور ستصل بالمستوى 6، وعندها إما أن تضيف البادئة الرقمية، أو تستخدم مكتبة تكتب إلى journald مباشرة بحقولها. وقد شرحنا كيف تكتب سجلات تفيدك فعلاً عند المشكلة في مقال سجلات تساعدك فعلاً.
البحث في السجل مع journalctl: الخدمة والوقت والنص والإقلاع
بعد أن أرسلنا ثلاثة طلبات إلى المسار /item بحثنا عنها بالنص، ثم طلبنا التحذيرات وما هو أخطر في آخر نصف ساعة:
journalctl -u orders --grep "GET /item" -o cat --no-pager
journalctl -u orders -p warning --since "-30min" --no-pager -o short-iso | tail -3GET /item/1 from 127.0.0.1
GET /item/2 from 127.0.0.1
GET /item/3 from 127.0.0.1
---
2026-10-30T17:56:11+00:00 d5300749f727 systemd[1]: Failed to start orders.service - Orders API.
2026-10-30T17:56:30+00:00 d5300749f727 systemd[1]: orders.service: Main process exited, code=killed, status=9/KILL
2026-10-30T17:56:30+00:00 d5300749f727 systemd[1]: orders.service: Failed with result 'signal'.والخيار -o cat يعرض الرسالة وحدها دون وقت ولا اسم، وهو مفيد عندما تمرر السجل إلى أداة أخرى، و-o short-iso يعرض الوقت بصيغة ISO مع المنطقة الزمنية، وهو الأنسب عندما تقارن سجلات من سيرفرات مختلفة. ولاحظ أن رسائل systemd عن فشل الخدمة نفسها لها مستوى warning أو أعلى، فهذا الأمر وحده يعطيك تاريخ كل مشاكل الخدمة دون الطلبات العادية. والجدول التالي يجمع الخيارات التي سوف تستخدمها كل يوم:
| الهدف | الأمر |
|---|---|
| سجل خدمة واحدة | journalctl -u orders |
| متابعة السجل لحظة بلحظة | journalctl -u orders -f |
| آخر 50 سطراً مع شرح الأخطاء المعروفة | journalctl -u orders -n 50 -x |
| الأخطاء فقط | journalctl -u orders -p err |
| نافذة زمنية | journalctl --since "2026-10-30 17:50" --until "18:00" |
| هذا الإقلاع فقط، أو الإقلاع السابق | journalctl -b 0 أو journalctl -b -1 |
| سجل النواة، مثل رسائل قاتل العمليات | journalctl -k |
| البحث في نص الرسالة | journalctl -u orders --grep "timeout" |
| كل الحقول بصيغة JSON | journalctl -u orders -o json-pretty |
حجم السجلات Retention: كم يحتفظ journald؟
المشكلة الثالثة في nohup.out أنه يكبر بلا حد حتى يملأ القرص. وjournald يضع حداً افتراضياً هو 10% من حجم نظام الملفات بحد أقصى 4 GB، وقد رأينا ذلك في أول سطر كتبه عند البدء في المختبر: System Journal (/var/log/journal/...) is 8M, max 4G, 3.9G free.. ولاحظ المسار: عندما يوجد المجلد /var/log/journal يحفظ journald السجلات على القرص فتبقى بعد إعادة الإقلاع، وعندما لا يوجد يحفظها في /run/log/journal في الذاكرة فتضيع مع كل إعادة تشغيل، وهذا سبب شائع لعبارة «السجل لا يحمل شيئاً قبل الإقلاع». ويمكنك تغيير الحد والمدة بملف إضافي، كما في journald.conf(5):
mkdir -p /etc/systemd/journald.conf.d
printf "[Journal]\nSystemMaxUse=200M\nMaxRetentionSec=30day\n" > /etc/systemd/journald.conf.d/size.conf
systemctl restart systemd-journald
journalctl -u systemd-journald -n 3 -o cat --no-pager
journalctl --vacuum-size=100MJournal stopped
Journal started
System Journal (/var/log/journal/2cf1b0be82de438b9426e2391a40af4a) is 8M, max 200M, 192M free.
Vacuuming done, freed 0B of archived journals from /run/log/journal.
Vacuuming done, freed 0B of archived journals from /var/log/journal.
Vacuuming done, freed 0B of archived journals from /var/log/journal/2cf1b0be82de438b9426e2391a40af4a.والسطر الثالث يؤكد أن الحد الجديد 200 MB صار فعالاً. والأمر --vacuum-size يحذف الملفات المؤرشفة القديمة حتى يصل الحجم إلى الرقم المطلوب، وهنا لم يحذف شيئاً لأن السجل كله 8 MB، والأمر journalctl --disk-usage يقول لك الحجم الحالي قبل أن تقرر. وإذا كنت تحتاج سجلات لأشهر، أو تحتاج البحث في سجلات عدة سيرفرات معاً، فهذا عمل نظام سجلات مركزي، وقد شرحنا مساره في مقال السجلات Logging للمطورين ومديري السيرفرات.
ماذا يحدث بعد إعادة الإقلاع Reboot؟
هذه هي المشكلة التي أوقفت الواجهة في القصة أسبوعاً. ولتجربتها أعدنا تشغيل الـ Container كاملاً بالأمر docker restart، فيوقف systemd كل الخدمات ثم يبدأ من جديد كما يحدث عند إعادة إقلاع السيرفر، ثم سألنا عن الخدمة:
systemctl is-enabled orders
systemctl is-active orders
curl -s localhost:8080/enabled
active
orders ok pid=32رجعت الخدمة وحدها، ورقم العملية الآن 32 لأن الأرقام بدأت من جديد مع النظام. وفي سجل الخدمة ظهر الإيقاف المنظم ثم البدء:
Oct 30 17:56:49 d5300749f727 systemd[1]: Stopping orders.service - Orders API...
Oct 30 17:56:49 d5300749f727 systemd[1]: orders.service: Deactivated successfully.
Oct 30 17:56:49 d5300749f727 systemd[1]: Stopped orders.service - Orders API.
Oct 30 17:56:50 d5300749f727 systemd[1]: Started orders.service - Orders API.
Oct 30 17:56:51 d5300749f727 python3[32]: orders starting on port 8080 as uid=996والكلمة Deactivated successfully تعني أن الخدمة توقفت عند طلب الإيقاف كما يجب. فعند الإيقاف يرسل systemd الإشارة SIGTERM إلى كل عمليات الخدمة، وينتظر حتى 90 ثانية، فإذا لم تنته أرسل SIGKILL، وهذه القيم ظهرت في الأمر systemctl show: KillSignal=15 وTimeoutStopUSec=1min 30s وKillMode=control-group، والأخيرة تعني أن الإشارة تصل إلى كل العمليات في مجموعة الخدمة وليس الرئيسية فقط، فلا تبقى عمليات يتيمة كما كان يحدث مع السكربتات القديمة. وتفاصيل ذلك في صفحة systemd.kill(5). وإذا كان برنامجك يحتاج وقتاً لينهي الطلبات الجارية قبل الإيقاف فعالج الإشارة SIGTERM فيه، ولا تعتمد على أن يقتله systemd بعد 90 ثانية.
journalctl --list-boots، والسبب أن رقم الإقلاع Boot ID يأتي من النواة، ونواة الجهاز لم تعد الإقلاع، وإنما أعيد تشغيل الـ Container فقط. وعلى سيرفر حقيقي يظهر كل إقلاع في سطر مستقل، ويعطيك الأمر journalctl -b -1 سجل الإقلاع السابق كاملاً، وهو أول ما تقرؤه بعد إعادة إقلاع غير متوقعة.الترتيب والاعتماد Dependencies بين الخدمات
في ملف الوحدة كتبنا سطرين عن الشبكة، والفرق بينهما من أكثر ما يسبب الالتباس في systemd. فالخيارات Wants وRequires تقول «شغل هذه الوحدة معي»، ولكنها لا تقول شيئاً عن الترتيب، فإذا كتبت Requires=postgresql.service وحده فقد يبدأ systemd الخدمتين في نفس اللحظة، ويحاول تطبيقك الاتصال بقاعدة البيانات قبل أن تجهز. أما After وBefore فتقول «إذا كانت الاثنتان ستبدآن، فابدأ هذه بعد تلك»، ولكنها لا تطلب تشغيل الأخرى. ولذلك تكتب الاثنين معاً في أغلب الحالات، كما فعلنا مع network-online.target:
Wants=XمعAfter=X: شغل X، وابدأني بعدها، وإذا فشلت X ابدأني على أي حال. وهذا مناسب عندما تستطيع الخدمة أن تعمل وتعيد المحاولة وحدها.Requires=XمعAfter=X: شغل X، وابدأني بعدها، وإذا فشلت X أو أوقفتها فلا تبدأني وأوقفني معها. وهذا اعتماد أقوى، فاستخدمه بحذر، لأن إعادة تشغيل قاعدة البيانات سوف توقف تطبيقك معها.network-online.targetوليسnetwork.target: الأول يعني أن الشبكة جاهزة فعلاً بعنوان، والثاني يعني فقط أن خدمات الشبكة بدأت، كما يشرح توثيق systemd عن الشبكة الجاهزة. وخدمة تتصل بسيرفر آخر عند بدئها تحتاج الأول.
ولكي ترى الترتيب الفعلي على سيرفرك، يعرض الأمر systemctl list-dependencies orders شجرة ما تعتمد عليه الخدمة، والأمر systemd-analyze critical-chain orders.service سلسلة الانتظار التي أخرت بدءها عند الإقلاع، وتجد كل أدوات التحليل في صفحة systemd-analyze(1). وأهم قاعدة هنا أن الترتيب في systemd لا يعني الجهوزية، فقاعدة البيانات «بدأت» عندما بدأت عمليتها، وقد تحتاج ثوان بعدها لتقبل الاتصالات، ولهذا يجب أن يعيد تطبيقك محاولة الاتصال عند البدء بدلاً من أن يفشل من أول محاولة.
تقوية الخدمة Hardening مع systemd-analyze security
تشغيل الخدمة بمستخدم خاص خطوة أولى، وsystemd يقدم عشرات الخيارات الأخرى التي تحد ما تستطيع الخدمة فعله لو اخترقها أحد: أن تكون ملفات النظام للقراءة فقط بالنسبة لها، وأن ترى مجلد /tmp خاصاً بها، وألا تستطيع زيادة صلاحياتها. والأمر systemd-analyze security يقيس هذه الخيارات ويعطي كل خدمة رقماً من 0 إلى 10، والرقم الأكبر يعني تعرضاً أكبر. وهذا ما قاله عن خدمتنا قبل أي تقوية:
✗ UMask= Files created by service are world-readable by default 0.1
→ Overall exposure level for orders.service: 9.2 UNSAFE :-{أضفنا أربعة خيارات في ملف إضافي ثان، ثم أعدنا التشغيل والقياس:
printf "[Service]\nEnvironment=PYTHONUNBUFFERED=1\nNoNewPrivileges=yes\nProtectSystem=strict\nProtectHome=yes\nPrivateTmp=yes\n" > /etc/systemd/system/orders.service.d/hardening.conf
systemctl daemon-reload
systemctl restart orders
systemd-analyze security orders --no-pager | tail -2→ Overall exposure level for orders.service: 8.3 EXPOSED :-(والمعنى: NoNewPrivileges يمنع الخدمة وأبناءها من الحصول على صلاحيات أعلى عبر برامج مثل sudo، وProtectSystem=strict يجعل كل نظام الملفات للقراءة فقط بالنسبة لها إلا ما تسمح به صراحة بالخيار ReadWritePaths، وProtectHome يخفي مجلدات المستخدمين، وPrivateTmp يعطيها /tmp خاصاً لا يراه غيرها. والرقم نزل من 9.2 إلى 8.3، وما زال عالياً لأن الأداة تقيس عشرات الخيارات الأخرى، والوصول إلى رقم منخفض يحتاج تجربة كل خيار مع تطبيقك.
المشكلة الثالثة: العزل Sandboxing الذي لم يعمل دون أن يخبرنا
بعد ذلك أردنا التأكد أن ProtectSystem=strict يعمل فعلاً، فطلبنا من systemd أن يشغل أمراً مؤقتاً بنفس الخيار يحاول الكتابة في /usr، عبر الأداة systemd-run:
systemd-run --wait -q -p ProtectSystem=strict touch /usr/local/probe
echo "exit $?"
ls /usr/local/probeexit 0
/usr/local/probeنجحت الكتابة، أي أن الحماية لم تطبق إطلاقاً، ولم نجد في السجل أي تحذير. والسبب أن هذه الخيارات تعتمد على مساحات أسماء التركيب Mount Namespaces في النواة، وإنشاؤها يحتاج الصلاحية CAP_SYS_ADMIN التي لا يعطيها Docker للـ Container افتراضياً، فتجاوزها systemd بصمت. وهذا ليس خطأ في systemd، فصفحة systemd.exec(5) تقول تحت كل خيار من هذه الخيارات إن تطبيقه قد يكون مستحيلاً، مثلاً عندما لا تتوفر مساحات أسماء التركيب، وإن الخدمة يجب أن تكتب بحيث لا تعتمد عليه وحده في الحماية. ولكي نتأكد من التفسير شغلنا Container ثانياً من نفس الـ Image مع الخيار --cap-add SYS_ADMIN، وأعدنا نفس الأمر:
exit 1
ls: cannot access '/usr/local/probe': No such file or directory
touch: cannot touch '/usr/local/probe': Read-only file system
run-r54631e1316284ace802b11ff89947fb7.service: Main process exited, code=exited, status=1/FAILUREهذه المرة رفضت النواة الكتابة برسالة Read-only file system، وهو السلوك المطلوب. وعلى سيرفر حقيقي أو جهاز افتراضي يكون systemd هو العملية رقم 1 بكل صلاحياته، فتعمل هذه الخيارات كما يجب. والدرس العملي أعم من المختبر: لا تثق برقم systemd-analyze security وحده، لأنه يقرأ ما كتبته في الملف لا ما طبق فعلاً، وجرب كل حماية مهمة بأمر يحاول خرقها. وإذا أردت قياس تقوية السيرفر كله فقد شرحنا ذلك في مقال تقوية السيرفر Hardening وقياسها.
الأوامر اليومية مع systemctl
| الهدف | الأمر | ملاحظة |
|---|---|---|
| تشغيل الآن والتفعيل للإقلاع | systemctl enable --now orders | بعد daemon-reload إذا كان الملف جديداً |
| إعادة التشغيل بعد تغيير الكود | systemctl restart orders | يوقف ثم يشغل، فيوجد انقطاع قصير |
| إعادة قراءة الإعداد دون إيقاف | systemctl reload orders | فقط إذا كان في الملف ExecReload |
| الحالة وآخر السطور | systemctl status orders | انظر إلى Active وMain PID |
| الإعداد الفعلي مع الملفات الإضافية | systemctl cat orders | أول خطوة مع خدمة لم تكتبها |
| تعديل دون لمس الملف الأصلي | systemctl edit orders | ينشئ override.conf |
| قيمة إعداد واحد | systemctl show orders -p NRestarts | مناسب للسكربتات والمراقبة |
| كل الخدمات الفاشلة | systemctl list-units --failed | اجعله جزءاً من المراقبة |
| مسح حالة الفشل | systemctl reset-failed orders | بعد الوصول إلى حد البدء |
| منع تشغيل خدمة نهائياً | systemctl mask orders | أقوى من disable، ويلغيه unmask |
أخطاء شائعة
- نسيان
systemctl daemon-reloadبعد تعديل ملف الوحدة: يبقى systemd يعمل بالنسخة القديمة، ويحذرك فيstatusبعبارة أن الملف تغير على القرص. - الخلط بين
startوenable: الخدمة تعمل اليوم ولا تعود بعد أول إعادة إقلاع، وهي مشكلة القصة بالضبط. - تعديل الملفات في
/lib/systemd/system/: أول تحديث للحزمة يمسح تعديلك، والمكان الصحيح ملف إضافي بالأمرsystemctl edit. - كتابة أمر بلا مسار كامل في
ExecStart، أو الاعتماد على متغيرات بيئة الطرفية مثل PATH وما تضيفه في.bashrc: الخدمة لا ترى بيئتك أنت. - كتابة الأسرار في ملف الوحدة نفسه: يقرؤه أي مستخدم ويعرضه
systemctl cat، والمكان الصحيح ملف متغيرات بصلاحيات محدودة. - تشغيل برنامج يذهب إلى الخلفية بنفسه بالنوع
Type=simple: يظن systemd أن الخدمة انتهت لحظة خروج العملية الأصلية، فاستخدمforkingأو شغل البرنامج في المقدمة. - ترك التخزين المؤقت للمخرج مفعلاً: سجلات بأوقات خاطئة أو سجلات تضيع عند القتل، كما رأينا مع Python.
- اختيار
RestartSecدون التفكير في حد البدء: إما حلقة لا تنتهي تملأ السجل، أو توقف نهائي بعد أقل من ثانية. - استخدام
RequiresدونAfter: الخدمتان تبدآن معاً، ويفشل تطبيقك في الاتصال بقاعدة بيانات لم تجهز بعد. - الاعتماد على رقم
systemd-analyze securityدون تجربة الحماية، خصوصاً داخل Container.
قائمة تحقق Checklist لكل خدمة Service جديدة
- الملف في
/etc/systemd/system/، والتعديلات على خدمات الحزم في ملفات إضافية. Descriptionواضح، وExecStartبمسار كامل، وWorkingDirectoryمحدد.- مستخدم نظام خاص بالخدمة بلا Shell ولا مجلد شخصي، وليس root.
- الأسرار في
EnvironmentFileبصلاحيات 600 أو 640، وليست في ملف الوحدة. Restart=on-failure، معRestartSecوحد بدء اخترتهما عن قصد.WantsوAfterمعاً لكل اعتماد، وnetwork-online.targetإذا كانت الخدمة تتصل بالخارج عند البدء.- المخرج دون تخزين مؤقت، ومستويات السجل مفهومة لـ journald.
systemctl enable --now، ثم تجربة إعادة الإقلاع فعلاً مرة واحدة قبل الإنتاج.- تجربة التوقف:
kill -9على العملية الرئيسية، والتأكد أنها تعود وأن السجل يشرح ما حدث. - حد لحجم السجلات في
journald.conf، والمجلد/var/log/journalموجود حتى تبقى السجلات بعد الإقلاع. systemctl list-units --failedوNRestartsضمن ما تراقبه.
حذف المختبر Cleanup
أوقفنا الـ Container الثاني بعد تجربة الصلاحية مباشرة، وحذف المختبر كله بأمرين:
docker rm -f sbl-sd
docker rmi sbl-systemd:1الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- الأمر
nohupيبقي البرنامج بعد الخروج من الجلسة فقط، ولا يعيده ولا يشغله بعد الإقلاع، ويعطي سجلاً بلا وقت ولا حد. - systemd هو العملية رقم 1، وكل خدمة عنده وحدة يصفها ملف من ثلاثة أقسام، و
enableرابط للإقلاع وstartتشغيل الآن. - Python خزن المخرج مؤقتاً فوصلت ثلاثة سطور بوقت واحد بعد 16 ثانية من أولها، والحل
PYTHONUNBUFFERED=1في ملف إضافي. Restart=on-failureأعاد الخدمة بعد ثانيتين من الخروج برمز خطأ ومن القتل بالإشارة 9، ولم يعدها بعدsystemctl stop.- الحد الافتراضي 5 محاولات في 10 ثوان، ومع
RestartSec=2لم يصل إليه أبداً ووصل العداد إلى 23، ومع 100 ملي ثانية توقفت الخدمة نهائياً بعد خمس محاولات. - journald يضيف إلى كل سطر الخدمة والمستخدم والمستوى والوقت، والبادئة
<3>تجعل السطر خطأ يظهر مع-p err. - خيارات الحماية مثل
ProtectSystemتجاوزها systemd بصمت داخل Container بلاCAP_SYS_ADMIN، فجرب كل حماية بأمر يحاول خرقها.
وفي مقالات قادمة بإذن الله سوف نستخدم نفس هذه الأدوات في تشخيص سيرفر معطل، ونقارن بين Cron ومؤقتات systemd. وهذه مقالات مرتبطة:
دليلنظام التشغيل Operating System: ماذا يفعل بين برامجك والجهاز؟خريطة للمبتدئين تريك بأوامر حقيقية كيف يدير نظام التشغيل العمليات والذاكرة والملفات، ولماذا لا يسقط جهازك كله عندما يسقط برنامج واحد.
دليلسطر الأوامر في Linux للمبتدئين: أول ما تحتاجه على السيرفركيف تتنقل بين الملفات على السيرفر، وتربط الأوامر ببعضها، وتتحكم في العمليات Processes دون أن تحذف شيئاً بالخطأ.
دليلصلاحيات الملفات Permissions في Linux وأخطاؤها الشائعةماذا تعني الحروف rwx، ولماذا تظهر رسالة Permission denied، وكيف تصلحها دون أن تلجأ إلى chmod 777.
دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.
دليلتحليل أداء سيرفر Linux في 60 ثانية: ماذا تكتب أولاً؟ماذا تكتب أول ما تدخل إلى سيرفر بطيء، وكيف تعرف خلال دقيقة هل المشكلة في المعالج أم الذاكرة أم القرص أم الشبكة.سجل التحديثات
- أكتوبر 2026: كتابة الدليل، وتنفيذ التجارب على systemd 257.13 داخل Container من Debian 13 (trixie) مع Python 3.13.5، على نواة Linux 5.15 (WSL2) وDocker Engine 29.8.0.