كلاود
بنية تحتية

خدمات systemd: شغّل تطبيقك كخدمة نظام تنهض بعد أي انهيار

كلاود|٢٤ سبتمبر ٢٠٢٦|6 دقائق قراءة|١٤٧ مشاهدة
خدمات systemd: شغّل تطبيقك كخدمة نظام تنهض بعد أي انهيار

توقف تطبيقك بعد أن أغلقت جلسة SSH؟ المشكلة أنك شغّلته يدوياً لا كخدمة نظام. نكتب unit file كامل مشروحاً سطراً سطراً، خمسة أوامر systemctl يومية، سجلات journalctl، ومثالين حقيقيين: queue worker وخدمة Node/SSR.

الجواب المباشر: اجعل تطبيقك خدمة نظام يديرها systemd

لو شغّلت تطبيقك بأمر يدوي في الطرفية، فهو مربوط بجلسة SSH الحالية، وسيتوقف لحظة إغلاقها أو انقطاع الاتصال، ولن ينهض بعد انهيار أو إعادة تشغيل الخادم. الحل الصحيح على أي خادم لينكس حديث هو تسجيل التطبيق كـخدمة systemd، وهو مدير الخدمات الأساسي في أوبونتو ودبيان وAlmaLinux وغيرها. بمجرد أن تكتب ملف unit صغيراً وتفعّله، يتكفّل systemd بتشغيل تطبيقك عند إقلاع الخادم تلقائياً، وإعادة تشغيله فوراً إن انهار، وتجميع سجلاته في مكان واحد، وإخبارك بحالته بأمر واحد.

الملف نفسه ثلاث كتل فقط: كتلة Unit للوصف والتبعيات، وكتلة Service لكيفية التشغيل، وكتلة Install لمتى يبدأ. السطر الأهم فيه هو Restart=always، فهو الفرق بين خدمة تنهض وحدها بعد أي انهيار وتطبيق يظل ميتاً حتى تكتشف أنت العطل. في هذا المقال نكتب ملفاً كاملاً ونشرحه سطراً سطراً، ثم نمرّ على الأوامر الخمسة التي تستخدمها يومياً، ونقرأ السجلات بـjournalctl، ونطبّق على مثالين حقيقيين: عامل طابور (queue worker) وخدمة Node تخدم SSR، ثم ننهي بسبب واضح لماذا nohup وscreen ليست حلاً للإنتاج.

تشريح ملف unit كامل

ملفات الخدمات المخصصة توضع في /etc/systemd/system/ بامتداد .service. لنكتب خدمة باسم myapp.service:

السطروظيفته
[Unit]بداية كتلة الوصف والتبعيات
Description=My App Workerوصف يظهر في status والسجلات
After=network.targetلا تبدأ إلا بعد جهوز الشبكة
[Service]بداية كتلة كيفية التشغيل
User=deployيعمل بحساب محدود لا بحساب root
WorkingDirectory=/var/www/appمجلد العمل قبل التشغيل
ExecStart=/usr/bin/php artisan queue:workالأمر الفعلي المشغّل، بمسار مطلق
Restart=alwaysأعد التشغيل بعد أي خروج أو انهيار
RestartSec=3انتظر 3 ثوانٍ قبل كل إعادة تشغيل
[Install]بداية كتلة الإقلاع
WantedBy=multi-user.targetيبدأ تلقائياً عند إقلاع الخادم

لماذا كل سطر مهم

After=network.target يمنع سباقاً شائعاً حيث يبدأ تطبيقك قبل جهوز الشبكة فيفشل في الاتصال بقاعدة البيانات. User=deploy يطبّق مبدأ أقل الصلاحيات، فلا تشغّل خدمة تواجه الإنترنت بحساب root. ExecStart يجب أن يكون بمسار مطلق كامل، لأن systemd لا يعرف متغيرات PATH الخاصة بجلستك. أما Restart=always مع RestartSec=3، فهما قلب الموضوع: لو انهار تطبيقك بسبب استثناء أو نفدت ذاكرته، ينتظر systemd ثلاث ثوانٍ ثم يعيده تلقائياً، دون تدخل بشري ودون توقف يذكر في الخدمة.

الأوامر الخمسة اليومية

بعد كتابة الملف، أخبر systemd أن يعيد قراءة ملفاته ثم فعّل خدمتك:

  • sudo systemctl daemon-reload: أعد تحميل ملفات الوحدات بعد أي تعديل على .service. ينساه الكثير فيتعجبون لماذا لم يسرِ تغييرهم.
  • sudo systemctl enable --now myapp: فعّل الخدمة للإقلاع التلقائي وشغّلها الآن معاً.
  • sudo systemctl restart myapp: أعد التشغيل بعد نشر إصدار جديد من الكود.
  • sudo systemctl status myapp: اعرض الحالة، هل تعمل، منذ متى، وآخر أسطر سجلها.
  • sudo systemctl stop myapp: أوقفها مؤقتاً عند الصيانة.

هذه الخمسة تغطي دورة حياة أي خدمة. القاعدة الذهبية: أي تعديل على محتوى ملف .service يتبعه daemon-reload ثم restart، وإلا يبقى systemd على النسخة القديمة في ذاكرته.

السجلات: journalctl

ميزة كبيرة في systemd أنه يجمع مخرجات تطبيقك (stdout وstderr) تلقائياً في سجل مركزي، فلا تحتاج إعادة توجيه يدوي لملفات. لقراءة سجل خدمتك:

  • journalctl -u myapp: كل سجل الخدمة من البداية.
  • journalctl -u myapp -f: تابع السجل حياً لحظة بلحظة (مثل tail -f)، مفيد أثناء تصحيح خطأ.
  • journalctl -u myapp --since "1 hour ago": سجل آخر ساعة فقط.
  • journalctl -u myapp -p err: الأخطاء فقط بمستوى err فأعلى.

لتفادي امتلاء القرص بالسجلات مع الوقت، اضبط حداً في /etc/systemd/journald.conf مثل SystemMaxUse=500M، فيحتفظ النظام بآخر 500 ميغابايت ويحذف الأقدم تلقائياً.

مثالان حقيقيان

عامل طابور (queue worker)

تطبيقات لارافيل تعالج المهام الثقيلة (إرسال بريد، معالجة صور، استدعاء APIs) في طابور خلفي عبر php artisan queue:work. هذا الأمر يجب أن يعمل دائماً ويعود بعد أي انهيار. ملف الخدمة يستخدم ExecStart بالمسار الكامل للأمر مع الخيارات المناسبة، مثل تحديد الاتصال ومهلة كل مهمة. مع Restart=always، لو قتل النظام العامل لتجاوزه الذاكرة، يعود systemd بتشغيله خلال ثوانٍ فلا تتكدس المهام. نصيحة مهمة: بعد كل نشر لإصدار جديد، نفّذ restart للخدمة حتى يلتقط العامل الكود الجديد، لأن العامل يبقى محمّلاً بالكود القديم في الذاكرة.

خدمة Node أو SSR

لو تشغّل خادم Node، أو خدمة عرض من جهة الخادم (SSR) لتطبيق React أو Vue، فنفس المبدأ ينطبق. ExecStart يشير إلى ثنائي node بمسار مطلق يليه ملف الدخول، مثل server.js أو ملف SSR المبني. أضف متغيرات البيئة عبر Environment=NODE_ENV=production أو EnvironmentFile=/var/www/app/.env. مع تفعيل الخدمة، يبقى موقعك يعرض صفحاته المرسّمة من الخادم بلا انقطاع، وينهض تلقائياً بعد إعادة تشغيل الخادم السحابي، وهذا حاسم لتحسين الظهور في محركات البحث التي تفضّل المحتوى المرسّم من الخادم.

ترتيب الخدمات والتبعيات بينها

كثير من الأعطال سببها خدمة بدأت قبل ما تعتمد عليه. systemd يعطيك ثلاث كلمات دقيقة لضبط الترتيب والتبعية. After= يحدد الترتيب فقط: ابدأ خدمتي بعد جهوز خدمة أخرى، دون أن تشترط نجاحها. Requires= تبعية صارمة: لو فشلت الخدمة المطلوبة، توقف خدمتك معها. Wants= تبعية ليّنة: حاول تشغيل الأخرى، لكن استمر حتى لو فشلت. المثال العملي: خدمة تطبيق تعتمد على قاعدة بيانات محلية تكتب فيها Requires=postgresql.service مع After=postgresql.service معاً، فتضمن أن قاعدة البيانات تبدأ أولاً وأن تطبيقك لا يعمل بلا قاعدته. لو استخدمت After وحدها بلا Requires، قد يبدأ تطبيقك ويفشل لأن قاعدة البيانات لم ترتفع بعد، فتحصل على أخطاء اتصال متقطعة يصعب تشخيصها. الضبط الصحيح للتبعيات يوفّر عليك ساعات من مطاردة أعطال وهمية بعد كل إعادة تشغيل للخادم.

حدود الموارد: احمِ خادمك من خدمة جامحة

ميزة قوية أخرى في systemd أنه يستطيع تقييد موارد كل خدمة، فلا تستهلك خدمة واحدة كل ذاكرة الخادم وتسقط البقية. أضف في كتلة Service حدوداً واقعية:

الإعدادوظيفته
MemoryMax=512Mيوقف الخدمة إن تجاوزت 512 ميغابايت، بدل أن تفترس ذاكرة الخادم
CPUQuota=50%يحدّ استهلاك المعالج بنصف نواة واحدة
TasksMax=100يحدّ عدد العمليات الفرعية لمنع تفجّر العمليات

على خادم أساس بذاكرة 2GB، ضبط MemoryMax لكل خدمة يمنع أن تُسقط تسريبة ذاكرة في خدمة واحدة كل التطبيق. لو بلغت الخدمة الحد، يقتلها systemd ثم يعيدها فوراً بحكم Restart=always، فتتعافى تلقائياً بدل أن تعلّق الخادم كله.

مؤقتات systemd بدل cron

للمهام المجدولة، يقدّم systemd بديلاً عن cron يُسمّى timer units، وله مزايا عملية. تكتب ملفين: ملف .service للمهمة نفسها، وملف .timer يحدد متى تعمل عبر OnCalendar بصيغة واضحة مثل OnCalendar=*-*-* 03:00:00 للتشغيل يومياً الثالثة فجراً. لماذا تفضّله على cron في كثير من الحالات: مخرجات المهمة تذهب تلقائياً إلى journalctl فتقرأ سجلها كأي خدمة، وخيار Persistent=true يشغّل المهمة الفائتة فور عودة خادم كان مطفأً وقت موعدها بدل تخطّيها بصمت كما يفعل cron. مناسب للنسخ الاحتياطي المجدول وتنظيف الملفات المؤقتة والمهام الدورية الحرجة التي لا تريد أن تفوت.

لماذا nohup وscreen ليست حلاً للإنتاج

كثيرون يشغّلون تطبيقاتهم بـnohup command & أو داخل جلسة screen أو tmux ويظنونها كافية. المشكلة أن أياً منها لا يعيد التشغيل بعد الانهيار: لو مات تطبيقك بسبب استثناء يبقى ميتاً حتى تكتشفه، ولا ينهض أي منها بعد إعادة تشغيل الخادم، فأول تحديث نواة يسقط كل خدماتك بصمت، كما لا يجمع أي منها السجلات بشكل منظم قابل للبحث. باختصار، nohup وscreen أدوات جلسة مؤقتة لتجربة سريعة، لا أدوات تشغيل خدمة إنتاج. systemd صُمم لهذا الدور بالضبط.

القدرةnohup / screensystemd
إعادة التشغيل بعد الانهيارلانعم (Restart=always)
النهوض بعد إقلاع الخادملانعم (enable)
سجلات مركزية قابلة للبحثلانعم (journalctl)
حالة الخدمة بأمر واحدلانعم (status)

خاتمة عملية

لتحويل أي تطبيق إلى خدمة صامدة:

  • اكتب ملف .service في /etc/systemd/system/ بثلاث كتل، والأهم Restart=always وRestartSec=3.
  • شغّل الخدمة بحساب محدود عبر User=، لا بحساب root، ومع مسارات مطلقة في ExecStart.
  • فعّلها بـenable --now، وراقب حالتها بـstatus وسجلها بـjournalctl -f.
  • بعد كل نشر: daemon-reload ثم restart ليلتقط التطبيق الكود الجديد.

لتشغيل خدمات systemd تملك عليها صلاحية root الكاملة تحتاج خادماً سحابياً خاصاً بك، لا استضافة مشتركة. راجع باقات الخادم السحابي والأسعار والباقات، والتفعيل تلقائي خلال ثوانٍ بعد الدفع على سيرفرات الرياض بزمن استجابة نحو 8ms.

مشاركة