إدارة تطبيقاتك باستخدام systemd
يُعدّ systemd مدير الخدمات القياسي في Linux. يتيح لك إنشاء خدمة مخصصة إدارة دورة حياة تطبيقاتك: التشغيل التلقائي عند الإقلاع، وإعادة التشغيل بعد الانهيار، وترتيب التشغيل بالنسبة لقاعدة البيانات، وتجميع السجلات مركزياً، وتقييد الموارد. كل ذلك دون تثبيت مشرف إضافي: فـ systemd موجود أصلاً في Debian وUbuntu وRHEL وفي كل التوزيعات الحالية تقريباً.
بالنسبة لتطبيق PHP، حالات الاستخدام النموذجية هي workers طوابير الرسائل، والمستهلكون طويلو التشغيل، وخوادم HTTP المدمجة الصغيرة، والمهام المجدولة. لنرَ كيف نكتب وحدات (units) موثوقة، ثم كيف نشغّلها يومياً.
تشريح ملف unit
تُوصف الخدمة بملف نصي بصيغة INI، يوضع في /etc/systemd/system/ للوحدات الخاصة بالجهاز. ويتضمن ثلاثة أقسام:
[Unit]: الوصف والاعتماديات على الوحدات الأخرى.[Service]: كيفية تشغيل العملية وإيقافها وإعادة تشغيلها، وتحت أي هوية.[Install]: الهدف (target) الذي تُربط به الخدمة عند تفعيلها باستخدامsystemctl enable.
إنشاء خدمة
# /etc/systemd/system/myapp.service
[Unit]
Description=My PHP Application
After=network.target mysql.service
Requires=mysql.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/php bin/console messenger:consume async --limit=100
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
التوجيهات المهمة:
AfterوRequires: مفهومان مختلفان. يحددAfterترتيب التشغيل؛ بينما يحددRequiresاعتمادية: إذا أُوقف MySQL تتوقف الخدمة أيضاً. أماWantsفهو صيغة أكثر مرونة تشغّل الاعتمادية دون أن تفشل إذا كانت غائبة. في بعض التوزيعات تُسمى الخدمةmariadb.service: تحقق من الاسم الدقيق.Type=simple: العملية التي يطلقهاExecStartهي العملية الرئيسية وتبقى في المقدمة. وهذه حالmessenger:consume. استخدمType=oneshotلأمر يُنفَّذ ثم ينتهي.UserوGroup: لا تشغّل أي تطبيق أبداً بصلاحيات root. هنا يملك الـ worker الصلاحيات نفسها التي يملكها PHP-FPM.WorkingDirectory: يجب أن يكون مسار الملف التنفيذي مطلقاً، لكن الوسائط مثلbin/consoleتُحلّ انطلاقاً من هذا المجلد.Restart=alwaysوRestartSec=5: مع--limit=100، يتوقف الـ worker عمداً بعد 100 رسالة؛ ثم يعيد systemd تشغيله بعد خمس ثوانٍ بذاكرة نظيفة. أماRestart=on-failureفلا يعيد التشغيل إلا عند التوقف بخطأ.StandardOutput=journal: يلتقط journald مخرجات العملية، ويختمها بالوقت، ويربطها بالخدمة.
الأوامر الأساسية
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp
journalctl -u myapp -f
الأمر daemon-reload إلزامي بعد كل إنشاء أو تعديل لملف unit: من دونه يستمر systemd في استخدام النسخة القديمة. ينشئ enable الرابط الذي يشغّل الخدمة عند الإقلاع، ويشغّلها start فوراً؛ أما enable --now فيقوم بالأمرين معاً. قبل تحميل ملف، يبلّغ systemd-analyze verify /etc/systemd/system/myapp.service عن التوجيهات غير المعروفة والأخطاء الإملائية.
بالنسبة للسجلات، يوفر journalctl مرشحات قيّمة أثناء الحوادث:
# Logs from the last hour, errors only
journalctl -u myapp --since "1 hour ago" -p err
# Logs from the current boot, without paging
journalctl -u myapp -b --no-pager
تجنّب حلقات إعادة التشغيل
الخدمة التي تنهار فور تشغيلها (ملف إعدادات مفقود، قاعدة بيانات غير قابلة للوصول) ستُعاد تشغيلها في حلقة. افتراضياً، يتخلى systemd بعد 5 عمليات تشغيل خلال 10 ثوانٍ ويضع الخدمة في حالة فشل. ومع RestartSec=5 لا تُبلغ هذه العتبة أبداً، لذا عدّل النافذة في القسم [Unit]:
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=10
هنا، أكثر من 10 عمليات تشغيل خلال 5 دقائق تنقل الخدمة إلى الحالة failed، التي تظهر في systemctl --failed ويسهل مراقبتها. بعد إصلاح السبب، يعيد systemctl reset-failed myapp العدّاد إلى الصفر.
إدارة workers الخاصة بـ Symfony Messenger
بالنسبة لتطبيقات Symfony التي تستخدم Messenger، أنشئ خدمة بعدة نسخ (instances):
# /etc/systemd/system/messenger-worker@.service
[Unit]
Description=Symfony Messenger worker %i
[Service]
User=www-data
Group=www-data
ExecStart=/usr/bin/php /var/www/app/bin/console messenger:consume async --time-limit=3600
Restart=always
[Install]
WantedBy=multi-user.target
# Start 3 workers
sudo systemctl enable messenger-worker@{1..3}
sudo systemctl start messenger-worker@{1..3}
الرمز @ في اسم الملف يجعله قالباً (template): فـ messenger-worker@1 وmessenger-worker@2 وغيرهما نسخ منفصلة من الملف نفسه، ويُستبدل %i بمعرّف النسخة. أما الصيغة {1..3} فهي توسيع خاص بـ shell Bash، وليست ميزة من ميزات systemd.
يجعل الخيار --time-limit=3600 كل worker يعيد التشغيل كل ساعة، مما يحدّ من أثر تسرّبات الذاكرة. يمكنك أيضاً استخدام --memory-limit=128M. أثناء النشر، تحتفظ الـ workers بالكود القديم في الذاكرة: نفّذ php bin/console messenger:stop-workers في نهاية النشر. ينهي كل worker رسالته الحالية ثم يتوقف، فيعيد systemd تشغيله على الكود الجديد.
متغيرات البيئة والتجاوزات
لا تضع الأسرار في ملف الـ unit، فهو مقروء للجميع. استخدم ملف بيئة محمياً:
[Service]
EnvironmentFile=/etc/myapp/env
Environment=APP_ENV=prod
يحتوي الملف /etc/myapp/env على أسطر KEY=value، ويجب أن يملكه root بصلاحيات 600: إذ يقرؤه systemd قبل تبديل المستخدم. لتعديل خدمة توفرها حزمة دون المساس بالملف الأصلي، استخدم sudo systemctl edit nginx: ينشئ هذا الأمر ملف تجاوز في /etc/systemd/system/nginx.service.d/ يصمد أمام التحديثات.
تقييد الموارد وعزل الخدمة
يضع systemd كل خدمة في cgroup خاص بها، مما يتيح تحديد سقف لمواردها وتقييد ما يمكنها رؤيته من النظام:
[Service]
MemoryMax=512M
CPUQuota=50%
TasksMax=100
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
يقتل MemoryMax العملية إذا تجاوزت الحد، بدلاً من ترك النواة تختار ضحية عشوائياً. ويحدّ CPUQuota=50% الخدمة بنصف نواة واحدة. يمنحها PrivateTmp مجلد /tmp معزولاً، ويجعل ProtectSystem=full المجلدات /usr و/boot و/etc للقراءة فقط، ويخفي ProtectHome المجلدات الشخصية. يمنح الأمر systemd-analyze security myapp الخدمة درجة تعرّض ويسرد خيارات التحصين المتاحة.
استبدال cron بمؤقّت (timer)
مؤقّتات systemd بديل حديث لـ cron: تظهر عمليات التنفيذ في السجل، ويكون الفشل مرئياً في systemctl --failed، ويمكن تدارك تنفيذ فائت أثناء توقف الجهاز. يفعّل المؤقّت الخدمة التي تحمل الاسم نفسه:
# /etc/systemd/system/app-cleanup.service
[Unit]
Description=Daily application cleanup
[Service]
Type=oneshot
User=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/php bin/console app:cleanup
# /etc/systemd/system/app-cleanup.timer
[Unit]
Description=Run app-cleanup every day at 3am
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now app-cleanup.timer
systemctl list-timers
systemd-analyze calendar "*-*-* 03:00:00"
يُطلق Persistent=true الخدمة عند الإقلاع إذا فات التنفيذ المجدول. ويتحقق systemd-analyze calendar من صحة التعبير ويعرض موعد التنفيذ التالي.
أفضل الممارسات
- حدّد الاعتماديات باستخدام
AfterوRequires - اضبط
Restart=alwaysلضمان المرونة - استخدم سجلات systemd بدلاً من ملفات السجل
- قيّد الموارد باستخدام cgroups
- لا تشغّل أي تطبيق بصلاحيات root: اضبط دائماً
UserوGroup - خزّن الأسرار في
EnvironmentFileمحمي - استخدم
systemctl editلتجاوز خدمة توفرها حزمة - أعد تشغيل الـ workers عند كل نشر باستخدام
messenger:stop-workers
ليس systemd الحل المناسب في كل مكان: ففي بيئة تعتمد على الحاويات، يتولى المنسّق (Docker، Kubernetes) إدارة دورة حياة العمليات. لكن على خادم تقليدي أو VPS، تحلّ بضعة ملفات unit مكتوبة جيداً محلّ Supervisor وسكربتات init المصنوعة يدوياً وجزء كبير من crontab، وبشكل أفضل.