> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# إدارة الخدمات Services والسجلات Logs مع systemd
- URL: https://arabroot.io/articles/الخدمات-services-والسجلات-logs-مع-systemd-و-journalctl/
- Published: 2026-10-30T17:34:08.000Z
- Updated: 2026-10-30T19:46:09.000Z
- Description: ننقل واجهة برمجية من nohup إلى خدمة systemd تعود وحدها بعد التوقف والإقلاع، ونقرأ سجلاتها مع journalctl، ونرى ثلاث مشاكل حقيقية واجهتنا في الطريق.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, Linux من الصفر إلى السيرفر, لينكس والسيرفرات

لنفرض أنك في شركة ناشئة تبيع منتجاتها عبر الإنترنت، ولديها واجهة برمجية صغيرة للطلبات مكتوبة بلغة Python تعمل على سيرفر Linux واحد. والذي شغلها أول مرة دخل إلى السيرفر عبر SSH وكتب `nohup python3 app.py &` ثم أغلق الجلسة، وبقيت تعمل أسابيع دون مشاكل. ثم توقفت في ليلة ما بسبب خطأ نادر في الكود، ولم يعرف أحد حتى الصباح حين بدأت رسائل العملاء تصل، وبعد أسبوع أعاد مزود الاستضافة تشغيل السيرفر لتحديث أمني فلم تعد الواجهة إطلاقاً، لأن أحداً لم يخبر السيرفر أن هذا البرنامج يجب أن يعمل بعد الإقلاع. وعندما فتح الفريق الملف `nohup.out` ليعرف ماذا حدث وجده ملفاً كبيراً بلا أوقات ولا مستويات، وفي آخره سطر الخطأ دون أي إشارة إلى متى حدث.

وكل هذه المشاكل حلها موجود في كل سيرفر Linux حديث، واسمه systemd: البرنامج الذي يعمل كأول عملية في النظام، ويشغل الخدمات Services عند الإقلاع بالترتيب الصحيح، ويراقبها ويعيد تشغيلها عندما تتوقف، ويجمع كل ما تطبعه في سجل واحد Journal تبحث فيه بالأمر `journalctl`. وفي هذا الدليل سوف نأخذ نفس واجهة الطلبات من `nohup` إلى خدمة systemd كاملة خطوة بخطوة، وكل مخرج تراه هنا خرج من تشغيل حقيقي، بما في ذلك ثلاث مشاكل واجهتنا ولم نكن نتوقعها. وإذا كنت جديداً على الطرفية فابدأ بمقال [سطر الأوامر في Linux للمبتدئين](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) ثم ارجع إلى هنا.

وسوف نناقش في هذا المقال ما يلي:

- لماذا لا يكفي `nohup`، وما هو systemd ولماذا يعمل كالعملية رقم 1.
- كيف شغلنا systemd داخل Container للتجربة، ولماذا اخترنا ذلك بدلاً من جهاز افتراضي.
- كتابة ملف الوحدة Unit File سطراً سطراً، والفرق بين التشغيل Start والتفعيل Enable.
- مشكلة السجلات التي تصل متأخرة بوقت خاطئ، وكيف أصلحناها بملف إضافي Drop-in.
- إعادة التشغيل التلقائي Restart، وحد البدء Start Limit، ولماذا بقيت خدمتنا تحاول إلى ما لا نهاية.
- السجلات مع journald والأمر journalctl: البحث بالخدمة والمستوى والوقت والنص، وتحديد حجم السجلات.
- الترتيب والاعتماد بين الخدمات، وتقوية الخدمة Hardening، ولماذا لم تعمل إعدادات الحماية دون أن تخبرنا.
- الأخطاء الشائعة، وقائمة تحقق لكل خدمة جديدة.

## لماذا لا يكفي nohup؟

قبل أن نكتب أي ملف إعداد لنرى الطريقة القديمة كما هي. والأمر `nohup` يفعل شيئاً واحداً فقط: يمنع وصول إشارة إغلاق الجلسة SIGHUP إلى البرنامج، فيبقى يعمل بعد أن تخرج من SSH، ويحول مخرجه إلى ملف. والتجربة التالية تشغل واجهة الطلبات بهذه الطريقة، ثم ترسل طلباً عادياً، ثم تطلب المسار `/crash` الذي يجعل البرنامج ينهي نفسه برمز خطأ كما يحدث في أي خطأ غير متوقع، ثم تحاول الوصول مرة أخرى:

```bash
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"
```

والمخرج سوف يكون كما يلي:

```text
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». والصورة التالية تجمع الفرق:

![مقارنة بين تشغيل الخدمة بالأمر nohup من جلسة SSH بالمستخدم root ومخرج في ملف nohup.out، حيث لا أحد يعيد تشغيلها ولا تعود بعد الإقلاع، وبين تشغيلها عبر systemd كالعملية رقم 1 الذي يراقبها ويعيدها ويرسل سجلاتها إلى journald ويشغلها بمستخدم خاص uid=996 ويعيدها بعد كل إقلاع](https://arabroot.io/content/images/2026/10/linux-systemd-01-nohup-vs-systemd.webp)

مع systemd يبقى من يراقب الخدمة بعد أن تخرج من الجلسة

## ما هو systemd، ولماذا يعمل كالعملية رقم 1؟

عندما تنتهي النواة من الإقلاع تشغل برنامجاً واحداً فقط، وهو العملية رقم 1 التي تسمى init، وكل عملية أخرى في النظام تولد منها أو من أبنائها. ولسنوات طويلة كانت هذه العملية في أغلب التوزيعات نظام SysV init، الذي يشغل سكربتات Shell طويلة من المجلد `/etc/init.d` واحداً بعد الآخر بترتيب أرقام في أسمائها. وكانت هذه الطريقة بطيئة لأنها تشغل كل شيء بالتتابع، وهشة لأن كل سكربت يكتب منطق التشغيل والإيقاف ومعرفة الحالة بنفسه، فيختلف سلوكها من خدمة إلى أخرى، ولا تراقب الخدمة بعد أن تبدأ. وفي عام 2010 نشر Lennart Poettering مقاله [Rethinking PID 1](https://0pointer.de/blog/projects/systemd.html?ref=arabroot.io) يعرض فيه فكرة systemd: ملفات إعداد قصيرة تصف ما تريد بدلاً من سكربتات تقول كيف، وتشغيل الخدمات بالتوازي التي لا يعتمد بعضها على بعض، ومراقبة دائمة لكل خدمة. وخلال السنوات التالية اعتمدته Fedora ثم Debian وUbuntu وأغلب التوزيعات الكبرى، وصار اليوم هو ما تجده على أي سيرفر جديد تقريباً.

والكلمة الأساسية في systemd هي الوحدة Unit، وهي أي شيء يديره systemd ويصفه ملف إعداد. وأشهر أنواع الوحدات:

- الخدمة Service: برنامج يعمل في الخلفية مثل nginx وPostgreSQL وواجهة الطلبات، والملف ينتهي بـ `.service`.
- الهدف Target: مجموعة من الوحدات تمثل حالة يصل إليها النظام، مثل `multi-user.target` وهو النظام الكامل دون واجهة رسومية، وهو ما يصل إليه أي سيرفر عند الإقلاع.
- المؤقت Timer: يشغل خدمة في وقت محدد، وهو البديل الحديث لـ Cron، وسوف نخصص له مقالاً مستقلاً بإذن الله.
- التركيب Mount، والمقبس Socket، والجهاز Device، وغيرها، وكلها تدار بنفس الأوامر.

وكل ما يعرفه systemd عن الوحدات تجده في صفحة [systemd.unit(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html?ref=arabroot.io)، وكل ما يخص الخدمات في صفحة [systemd.service(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html?ref=arabroot.io)، وهما المرجع الذي نرجع إليه في كل إعداد في هذا المقال. والأداة التي تتحكم بها في كل هذا هي [systemctl](https://www.freedesktop.org/software/systemd/man/latest/systemctl.html?ref=arabroot.io)، والأداة التي تقرأ بها السجلات هي [journalctl](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html?ref=arabroot.io).

## تجهيز المختبر: systemd داخل Container

الطريقة الطبيعية لتجربة systemd هي جهاز Linux حقيقي أو جهاز افتراضي Virtual Machine، فهناك يكون systemd هو العملية رقم 1 من الإقلاع. ولكن الجهاز الافتراضي يحتاج نظاماً كاملاً بعدة غيغابايتات وإقلاعاً كاملاً في كل مرة، بينما أردنا مختبراً يبدأ في ثوان ويحذف بأمر واحد. والمشكلة أن الـ Container في Docker لا يشغل systemd عادة، فالفكرة فيه أن يشغل برنامجاً واحداً يكون هو العملية رقم 1، كما يشرح [توثيق Docker عن تشغيل أكثر من خدمة في Container](https://docs.docker.com/engine/containers/multi-service%5Fcontainer/?ref=arabroot.io). والحل الوسط أن نجعل systemd نفسه هو البرنامج الذي يشغله الـ Container، فيصبح هو العملية رقم 1 داخله ويدير الخدمات كما يفعل في السيرفر. وهذا ملف الـ Image:

```bash
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 getty@.service 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` كاملاً:

```bash
#!/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:

```text
systemd 257 (257.13-1~deb13u1)
  PID COMMAND
    1 systemd
```

أما البرنامج نفسه فهو ملف `app.py` صغير يقدم واجهة HTTP على المنفذ 8080، ويرفض البدء إذا لم يجد المتغير `API_KEY`، وفيه مساران للتجربة: `/crash` الذي ينهي البرنامج برمز 3، و`/slow` الذي ينتظر 5 ثوان. ولاحظ أن كل سطر يطبعه يبدأ برقم بين قوسين مثل `<3>`، وسوف نعرف فائدة ذلك في قسم السجلات:

```python
# 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`:

```ini
[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
```

والملف مقسم إلى ثلاثة أقسام، ولكل قسم سؤال يجيب عليه كما في الصورة التالية:

![ملف الوحدة orders.service إلى اليمين، وإلى اليسار شرح أقسامه الثلاثة: القسم Unit يصف الخدمة وترتيبها مع الشبكة بالخيارين Wants وAfter، والقسم Service يحدد الأمر والمستخدم وملف المتغيرات وسياسة الإعادة، والقسم Install يحدد أنها تبدأ مع الهدف multi-user.target](https://arabroot.io/content/images/2026/10/linux-systemd-02-unit-file.webp)

كل قسم في ملف الوحدة يجيب عن سؤال واحد

وفي الإعداد أعلاه لاحظ التالي:

- `Description`: الاسم الذي تراه في كل سطر سجل وفي `systemctl status`، فاكتبه بحيث يفهمه من يقرأ السجل في منتصف الليل.
- `Wants` و`After`: الأول يقول «اطلب تشغيل هدف الشبكة الجاهزة معي»، والثاني يقول «لا تبدأني قبل أن يجهز»، وسوف نرجع إلى الفرق بينهما في قسم الترتيب.
- `Type=simple`: يعني أن البرنامج الذي في `ExecStart` هو الخدمة نفسها ويبقى يعمل في المقدمة، وهذا هو الحال في أغلب التطبيقات الحديثة. والنوع `forking` للبرامج القديمة التي تنسخ نفسها إلى الخلفية ثم يخرج الأصل، والنوع `notify` للبرامج التي تخبر systemd بنفسها أنها جاهزة عبر واجهة [sd\_notify](https://www.freedesktop.org/software/systemd/man/latest/sd-daemon.html?ref=arabroot.io)، فلا يعتبرها جاهزة لحظة بدء العملية وإنما لحظة جهوزيتها فعلاً.
- `User` و`Group`: الخدمة تعمل بمستخدم نظام اسمه `orders` أنشأناه بالخيار `--system` ودون مجلد شخصي ودون Shell، فلا يمكن الدخول به، وأي خطأ في التطبيق يبقى محصوراً في صلاحيات هذا المستخدم. وقد شرحنا المستخدمين والصلاحيات في مقال [صلاحيات الملفات Permissions في Linux](https://arabroot.io/articles/%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%81%D9%8A-linux-%D9%88%D8%A7%D8%AE%D8%B7%D8%A7%D8%A4%D9%87%D8%A7/).
- `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`، لأنه لا يراقب المجلد ولا يقرأ الملفات من جديد وحده، ثم نفعل الخدمة ونشغلها بأمر واحد، ثم نقرأ حالتها:

```bash
systemctl daemon-reload
systemctl enable --now orders
sleep 2
systemctl status orders --no-pager
curl -s localhost:8080/
journalctl -u orders --no-pager
```

والمخرج:

```text
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 نفسه يقول إن الخدمة بدأت.

والصورة التالية تفصل بين الأمرين اللذين يخلط بينهما كثيرون:

![الأمر systemctl start orders يشغل عملية الآن وتظهر حالتها active مع رقم العملية، وتختفي بعد إعادة الإقلاع إذا لم تكن مفعلة، والأمر systemctl enable orders ينشئ رابطاً من مجلد multi-user.target.wants إلى ملف orders.service فيبقى بعد الإقلاع ولا يشغل شيئاً الآن، وفوقهما ترتيب الإقلاع من النواة إلى systemd إلى الأهداف](https://arabroot.io/content/images/2026/10/linux-systemd-06-enable-start.webp)

start للآن، وenable لما بعد الإقلاع، و--now للاثنين

## مشكلة واجهتنا: السجلات تتأخر بسبب التخزين المؤقت Output Buffering

ارجع إلى المخرج السابق ولاحظ ما ينقصه: البرنامج يطبع سطراً عند البدء وسطراً لكل طلب، وقد أرسلنا طلباً ونجح، ومع ذلك لم يظهر في سجل الخدمة إلا سطر systemd. ثم طلبنا المسار `/crash` وقرأنا السجل مرة أخرى:

```text
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`، لضاعت هذه السطور كلها ولم تعرف أبداً ماذا كان يفعل قبل أن يتوقف:

![خط زمني بدون PYTHONUNBUFFERED: بدء البرنامج والطلب GET والتوقف وقعت في أوقات مختلفة ولكن السطور الثلاثة سجلت بوقت واحد 17:54:39.354938، وتحته خط زمني مع PYTHONUNBUFFERED=1 تحمل فيه السطور أوقاتها الحقيقية 17:54:56.893 و17:54:57.831 و17:54:57.843](https://arabroot.io/content/images/2026/10/linux-systemd-05-buffering.webp)

بدون تفريغ فوري يسجل journald وقت الوصول، لا وقت الحدث

والحل أن نطلب من Python عدم التخزين المؤقت للمخرج، إما بالخيار `-u` في سطر التشغيل أو بالمتغير `PYTHONUNBUFFERED=1` كما يشرح [توثيق سطر أوامر Python](https://docs.python.org/3/using/cmdline.html?ref=arabroot.io). ونفس المشكلة موجودة بأشكال أخرى في لغات غير Python، فابحث عن إعداد التفريغ في لغة تطبيقك. ولكي لا نعدل الملف الأصلي استخدمنا ملفاً إضافياً Drop-in، وهو ملف صغير في مجلد اسمه اسم الخدمة مع `.d`، يضيف إعدادات أو يغير إعدادات فوق الملف الأصلي. وهذا ما يفعله الأمر `systemctl edit orders` في السيرفر الحقيقي، حيث يفتح لك المحرر ويحفظ الملف في المكان الصحيح، وفي المختبر كتبناه مباشرة:

```bash
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` يعرض الملف الأصلي وكل الملفات الإضافية فوقه، وهو أول ما تكتبه عندما تريد أن تعرف الإعداد الفعلي لخدمة لم تكتبها أنت:

```text
# /etc/systemd/system/orders.service
[Unit]
Description=Orders API
...
# /etc/systemd/system/orders.service.d/override.conf
[Service]
Environment=PYTHONUNBUFFERED=1
```

وبعد الإصلاح أعدنا نفس التجربة، فصار لكل سطر وقته الحقيقي:

```text
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` نفس الأرقام:

```text
MainPID=148
NRestarts=1
ExecMainStatus=0
```

والقيمة `NRestarts` تعد مرات إعادة التشغيل التلقائي، وهي رقم يستحق المراقبة، فخدمة تعيد التشغيل كل ساعة تعمل في نظر `status` ولكن فيها مشكلة. وجربنا بعد ذلك حالتين أخريين: قتل العملية بالإشارة 9 كما يفعل قاتل العمليات OOM Killer عند نفاد الذاكرة، ثم إيقافها بالطريقة الرسمية:

```bash
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 NRestarts
```

```text
Oct 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)](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html?ref=arabroot.io):

| القيمة      | متى يعيد التشغيل                                                     | متى تستخدمها                                         |
| ----------- | -------------------------------------------------------------------- | ---------------------------------------------------- |
| no          | أبداً، وهي القيمة الافتراضية                                         | مهام تعمل مرة واحدة                                  |
| on-failure  | رمز خروج غير صفر، أو إشارة، أو انتهاء مهلة، أو فشل المراقبة Watchdog | أغلب الخدمات، وهي الأنسب للبداية                     |
| on-abnormal | الإشارات والمهلات فقط، وليس رموز الخروج                              | برامج تخرج برمز خطأ عن قصد عندما يكون إعدادها خاطئاً |
| always      | مهما كان سبب التوقف، حتى الخروج الطبيعي برمز 0                       | برامج يجب أن تعمل دائماً ولو أنهت نفسها بشكل طبيعي   |

ولاحظ أن `RestartSec` الافتراضي 100 ملي ثانية فقط، وهذا يقودنا إلى المشكلة الثانية.

## حد البدء Start Limit: الخدمة التي لم تتوقف عن المحاولة

إعادة التشغيل تنقذك من خطأ نادر، ولكن ماذا لو كان الخطأ في الإعداد نفسه، فالبرنامج يتوقف فور بدئه في كل مرة؟ لتجربة ذلك عطلنا السطر `API_KEY` في ملف المتغيرات، فصار البرنامج يخرج برمز 2 فوراً، ثم أعدنا التشغيل وانتظرنا 16 ثانية:

```bash
sed -i "s/^API_KEY=/#API_KEY=/" /etc/orders/orders.env
systemctl restart orders
sleep 16
systemctl status orders --no-pager
```

```text
● 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 ثوان:

```text
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 ملي ثانية:

```bash
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-pager
```

```text
Oct 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` أو تشغلها بنفسك. والصورة التالية تجمع الحالتين:

![دورة حالات الخدمة: active ثم failed عند الخروج برمز خطأ ثم activating auto-restart ثم الرجوع إلى active بعد RestartSec، أو الانتقال إلى failed نهائياً start-limit-hit إذا تجاوزت المحاولات 5 في 10 ثوان. وتحتها مقارنة: مع RestartSec=2 حلقة لا تنتهي ووصل العداد إلى 23، ومع RestartSec=100ms توقفت الخدمة بعد 5 محاولات برسالة Start request repeated too quickly](https://arabroot.io/content/images/2026/10/linux-systemd-03-restart-loop.webp)

الحد يحسب المحاولات في نافذة زمنية، والمدة بين المحاولات تقرر هل تصل إليه

فأيهما الصحيح؟ لا توجد إجابة واحدة، ولكن يجب أن تختار عن قصد. فالحلقة التي لا تنتهي مناسبة لخدمة تعتمد على شيء خارجي قد يعود وحده، مثل قاعدة بيانات تعيد التشغيل، فتنتظر الخدمة حتى تعود. والتوقف عند الحد مناسب عندما يكون الخطأ غالباً في الإعداد، فلا فائدة من المحاولة كل ثانيتين لأسابيع، والأفضل أن تظهر الخدمة في قائمة الفشل فيراها نظام المراقبة. ويمكنك ضبط الحد نفسه بالخيارين `StartLimitIntervalSec` و`StartLimitBurst` في القسم `[Unit]`، وفي الإصدارات الحديثة يوجد أيضاً `RestartSteps` و`RestartMaxDelaySec` لزيادة المدة بين المحاولات تدريجياً، فتبدأ سريعة ثم تبطئ، وهو حل وسط جيد. وبعد التجربة أعدنا السطر `API_KEY` ورجعنا إلى الإعداد السابق:

```bash
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 لا يحفظ النص فقط، وإنما يضيف إلى كل سطر عشرات الحقول: الخدمة، ورقم العملية، والمستخدم، والوقت بدقة الجزء من المليون، ورقم الإقلاع، ومستوى الأهمية، ويحفظ كل ذلك في ملفات ثنائية مفهرسة، فتبحث فيها بسرعة بأي حقل:

![مسار السجل: الخدمة orders.service تطبع سطراً يبدأ بالرمز 3 على stdout، فيستقبله systemd-journald ويضيف إليه الحقول _SYSTEMD_UNIT و_PID و_UID وPRIORITY و_BOOT_ID والوقت، ويحفظه في المجلد /var/log/journal في ملفات ثنائية مفهرسة، ثم تسأله بالأمر journalctl بخيارات -u و-p و--since و--grep و-b و-o json-pretty و-f](https://arabroot.io/content/images/2026/10/linux-systemd-04-journal.webp)

البرنامج يطبع فقط، وjournald يضيف كل ما تحتاجه للبحث

ولكي ترى هذه الحقول طلبنا آخر سطر من سجل الخدمة بكل تفاصيله:

```bash
journalctl -u orders -n 1 -o json-pretty --no-pager
```

```text
{
	"_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)](https://www.freedesktop.org/software/systemd/man/latest/systemd.journal-fields.html?ref=arabroot.io). ولاحظ أن الحقول التي تبدأ بشرطة سفلية يكتبها journald نفسه ولا يستطيع البرنامج تزويرها، فالحقل `_UID` هو المستخدم الحقيقي للعملية مهما كتب البرنامج في رسالته. ولاحظ أيضاً الحقل `PRIORITY` بالقيمة 6، وهذا يعيدنا إلى الأرقام بين القوسين في بداية كل سطر.

### مستوى الأهمية Priority: لماذا كتبنا <3> في بداية السطر؟

السجلات في Linux لها ثمانية مستويات موروثة من syslog، من 0 وهو emerg إلى 7 وهو debug، والمستوى 3 هو err والمستوى 6 هو info. وsystemd يفهم أي سطر يبدأ برقم بين قوسين، فيحذف الرقم من الرسالة ويجعله مستوى السطر، وهذا سلوك الخيار `SyslogLevelPrefix` المفعل افتراضياً كما في صفحة [systemd.exec(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html?ref=arabroot.io). ولذلك ظهرت الرسائل في السجل دون الأرقام، بينما ظهرت كما هي في `nohup.out`. والفائدة أنك تستطيع أن تطلب الأخطاء فقط:

```bash
journalctl -u orders --no-pager -p err -o short-precise
```

```text
Oct 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 مباشرة بحقولها. وقد شرحنا كيف تكتب سجلات تفيدك فعلاً عند المشكلة في مقال [سجلات تساعدك فعلاً](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-%D8%AA%D8%B3%D8%A7%D8%B9%D8%AF%D9%83-%D9%81%D8%B9%D9%84%D8%A7-%D9%85%D9%86-%D8%AD%D8%AF%D8%AB-%D8%AE%D8%B7%D8%A7-%D8%A7%D9%84%D9%89-%D8%B3%D8%A8%D8%A8-%D8%A7%D9%84%D9%85%D8%B4%D9%83%D9%84%D8%A9/).

### البحث في السجل مع journalctl: الخدمة والوقت والنص والإقلاع

بعد أن أرسلنا ثلاثة طلبات إلى المسار `/item` بحثنا عنها بالنص، ثم طلبنا التحذيرات وما هو أخطر في آخر نصف ساعة:

```bash
journalctl -u orders --grep "GET /item" -o cat --no-pager
journalctl -u orders -p warning --since "-30min" --no-pager -o short-iso | tail -3
```

```text
GET /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)](https://www.freedesktop.org/software/systemd/man/latest/journald.conf.html?ref=arabroot.io):

```bash
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=100M
```

```text
Journal 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 للمطورين ومديري السيرفرات](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/).

## ماذا يحدث بعد إعادة الإقلاع Reboot؟

هذه هي المشكلة التي أوقفت الواجهة في القصة أسبوعاً. ولتجربتها أعدنا تشغيل الـ Container كاملاً بالأمر `docker restart`، فيوقف systemd كل الخدمات ثم يبدأ من جديد كما يحدث عند إعادة إقلاع السيرفر، ثم سألنا عن الخدمة:

```bash
systemctl is-enabled orders
systemctl is-active orders
curl -s localhost:8080/
```

```text
enabled
active
orders ok pid=32
```

رجعت الخدمة وحدها، ورقم العملية الآن 32 لأن الأرقام بدأت من جديد مع النظام. وفي سجل الخدمة ظهر الإيقاف المنظم ثم البدء:

```text
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)](https://www.freedesktop.org/software/systemd/man/latest/systemd.kill.html?ref=arabroot.io). وإذا كان برنامجك يحتاج وقتاً لينهي الطلبات الجارية قبل الإيقاف فعالج الإشارة `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 عن الشبكة الجاهزة](https://systemd.io/NETWORK%5FONLINE/?ref=arabroot.io). وخدمة تتصل بسيرفر آخر عند بدئها تحتاج الأول.

ولكي ترى الترتيب الفعلي على سيرفرك، يعرض الأمر `systemctl list-dependencies orders` شجرة ما تعتمد عليه الخدمة، والأمر `systemd-analyze critical-chain orders.service` سلسلة الانتظار التي أخرت بدءها عند الإقلاع، وتجد كل أدوات التحليل في صفحة [systemd-analyze(1)](https://www.freedesktop.org/software/systemd/man/latest/systemd-analyze.html?ref=arabroot.io). وأهم قاعدة هنا أن الترتيب في systemd لا يعني الجهوزية، فقاعدة البيانات «بدأت» عندما بدأت عمليتها، وقد تحتاج ثوان بعدها لتقبل الاتصالات، ولهذا يجب أن يعيد تطبيقك محاولة الاتصال عند البدء بدلاً من أن يفشل من أول محاولة.

## تقوية الخدمة Hardening مع systemd-analyze security

تشغيل الخدمة بمستخدم خاص خطوة أولى، وsystemd يقدم عشرات الخيارات الأخرى التي تحد ما تستطيع الخدمة فعله لو اخترقها أحد: أن تكون ملفات النظام للقراءة فقط بالنسبة لها، وأن ترى مجلد `/tmp` خاصاً بها، وألا تستطيع زيادة صلاحياتها. والأمر `systemd-analyze security` يقيس هذه الخيارات ويعطي كل خدمة رقماً من 0 إلى 10، والرقم الأكبر يعني تعرضاً أكبر. وهذا ما قاله عن خدمتنا قبل أي تقوية:

```text
✗ UMask=                                                      Files created by service are world-readable by default                       0.1

→ Overall exposure level for orders.service: 9.2 UNSAFE :-{
```

أضفنا أربعة خيارات في ملف إضافي ثان، ثم أعدنا التشغيل والقياس:

```bash
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
```

```text
→ 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](https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html?ref=arabroot.io):

```bash
systemd-run --wait -q -p ProtectSystem=strict touch /usr/local/probe
echo "exit $?"
ls /usr/local/probe
```

```text
exit 0
/usr/local/probe
```

نجحت الكتابة، أي أن الحماية لم تطبق إطلاقاً، ولم نجد في السجل أي تحذير. والسبب أن هذه الخيارات تعتمد على مساحات أسماء التركيب Mount Namespaces في النواة، وإنشاؤها يحتاج الصلاحية `CAP_SYS_ADMIN` التي لا يعطيها Docker للـ Container افتراضياً، فتجاوزها systemd بصمت. وهذا ليس خطأ في systemd، فصفحة [systemd.exec(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html?ref=arabroot.io) تقول تحت كل خيار من هذه الخيارات إن تطبيقه قد يكون مستحيلاً، مثلاً عندما لا تتوفر مساحات أسماء التركيب، وإن الخدمة يجب أن تكتب بحيث لا تعتمد عليه وحده في الحماية. ولكي نتأكد من التفسير شغلنا Container ثانياً من نفس الـ Image مع الخيار `--cap-add SYS_ADMIN`، وأعدنا نفس الأمر:

```text
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 وقياسها](https://arabroot.io/articles/%D8%AA%D9%82%D9%88%D9%8A%D8%A9-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-hardening-%D9%88%D9%82%D9%8A%D8%A7%D8%B3%D9%87%D8%A7-%D9%85%D8%B9-lynis-%D9%88-cis/).

## الأوامر اليومية مع 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 الثاني بعد تجربة الصلاحية مباشرة، وحذف المختبر كله بأمرين:

```bash
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. وهذه مقالات مرتبطة:

[![](https://arabroot.io/content/images/2026/10/os-basics-cover-cover.webp)دليلنظام التشغيل Operating System: ماذا يفعل بين برامجك والجهاز؟خريطة للمبتدئين تريك بأوامر حقيقية كيف يدير نظام التشغيل العمليات والذاكرة والملفات، ولماذا لا يسقط جهازك كله عندما يسقط برنامج واحد.](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-%D9%86%D8%B8%D8%A7%D9%85-%D8%A7%D9%84%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-operating-system/)[![](https://arabroot.io/content/images/2026/10/linux-cli-cover-cover.webp)دليلسطر الأوامر في Linux للمبتدئين: أول ما تحتاجه على السيرفركيف تتنقل بين الملفات على السيرفر، وتربط الأوامر ببعضها، وتتحكم في العمليات Processes دون أن تحذف شيئاً بالخطأ.](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/linux-permissions-cover-cover.webp)دليلصلاحيات الملفات Permissions في Linux وأخطاؤها الشائعةماذا تعني الحروف rwx، ولماذا تظهر رسالة Permission denied، وكيف تصلحها دون أن تلجأ إلى chmod 777.](https://arabroot.io/articles/%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%81%D9%8A-linux-%D9%88%D8%A7%D8%AE%D8%B7%D8%A7%D8%A4%D9%87%D8%A7/)[![](https://arabroot.io/content/images/2026/10/logging-basics-cover-cover.webp)دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/linux-performance-cover-cover.webp)دليلتحليل أداء سيرفر Linux في 60 ثانية: ماذا تكتب أولاً؟ماذا تكتب أول ما تدخل إلى سيرفر بطيء، وكيف تعرف خلال دقيقة هل المشكلة في المعالج أم الذاكرة أم القرص أم الشبكة.](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/)

## سجل التحديثات

- أكتوبر 2026: كتابة الدليل، وتنفيذ التجارب على systemd 257.13 داخل Container من Debian 13 (trixie) مع Python 3.13.5، على نواة Linux 5.15 (WSL2) وDocker Engine 29.8.0.