جدولة المهام على لينكس: من crontab الخام إلى Laravel Scheduler
سطر cron خمسة حقول: دقيقة، ساعة، يوم، شهر، يوم أسبوع. بدل حشو crontab بعشرات الأسطر الهشّة، ضع سطراً واحداً يشغّل schedule:run كل دقيقة وأدر كل مهامك من الكود مع withoutOverlapping وonOneServer.
جدولة المهام على لينكس تقوم على cron، وهو خدمة تشغّل أوامر في أوقات محددة. كل مهمة سطر من خمسة حقول زمنية يتبعها الأمر: الدقيقة (0-59)، الساعة (0-23)، يوم الشهر (1-31)، الشهر (1-12)، يوم الأسبوع (0-6 حيث الأحد صفر). النجمة تعني "كل قيمة"، فالسطر 0 3 * * * يشغّل الأمر الساعة الثالثة فجراً كل يوم، والسطر */5 * * * * يشغّله كل خمس دقائق. تحرّر جدولك بأمر crontab -e وتستعرضه بـcrontab -l. المشكلة أن حشو crontab بعشرات الأسطر يتحوّل بسرعة إلى فوضى هشّة: مسارات مطلقة منسية، متغيرات بيئة مفقودة، ومهام تتداخل فوق بعضها. الحل الأنظف في تطبيق Laravel هو سطر cron واحد فقط يشغّل php artisan schedule:run كل دقيقة، ثم تعرّف كل مهامك في الكود داخل ملف الجدولة حيث تملك قراءة أوضح، وقفلاً ضد التداخل بـwithoutOverlapping، وضماناً بألا تتكرر المهمة على عدة سيرفرات بـonOneServer. هذا المقال يشرح بنية سطر cron الخام، فخّ متغيرات البيئة والمسارات، لماذا المهمة الواحدة أنظف، وكيف تتحقق فعلياً أن مهمتك تعمل عبر السجلّات والقفل.
بنية سطر cron وقراءته الصحيحة
الحقول الخمسة تُقرأ من اليسار: الدقيقة، ثم الساعة، ثم يوم الشهر، ثم الشهر، ثم يوم الأسبوع. أمثلة تثبّت الفهم: 30 2 * * * تعني الساعة 2:30 فجراً يومياً، و0 0 1 * * تعني منتصف ليل أول كل شهر، و0 9 * * 1 تعني التاسعة صباحاً كل يوم اثنين. الرمز */n يعني "كل n"، والفاصلة تعدّد قيماً (1,15 = الأول والخامس عشر)، والشرطة القصيرة تعطي مدى (1-5 = من الاثنين إلى الجمعة). الخطأ الشائع خلط يوم الشهر ويوم الأسبوع، فإذا حددت الاثنين معاً يوماً محدداً في الشهر، ينفّذ cron المهمة إذا تطابق أيٌّ منهما لا كلاهما، وهذا سلوك يفاجئ كثيرين.
| سطر cron | متى يعمل |
|---|---|
| * * * * * | كل دقيقة |
| */15 * * * * | كل 15 دقيقة |
| 0 * * * * | بداية كل ساعة |
| 0 3 * * * | الثالثة فجراً يومياً |
| 0 0 * * 0 | منتصف ليل كل أحد |
فخّ متغيرات البيئة والمسارات المطلقة
أكبر سبب لفشل مهام cron صامتاً أن cron يعمل ببيئة شبه فارغة، لا ترث لا PATH الكامل ولا متغيرات جلستك، فأمر يعمل مثالياً حين تكتبه بيدك في الطرفية يفشل حين يشغّله cron لأنه لا يجد php أو composer في المسار. القاعدة: استخدم دائماً مسارات مطلقة لكل شيء. بدل php artisan ... اكتب المسار الكامل للـPHP وللمشروع، مثل /usr/bin/php /home/site/artisan schedule:run. إذا كانت المهمة تعتمد على متغيرات بيئة، عرّفها صراحة في أعلى ملف crontab أو حمّل ملف البيئة داخل السكربت نفسه، مع توجيه المخرجات دائماً إلى سجلّ لتراها لاحقاً، بإضافة >> /home/site/storage/logs/cron.log 2>&1 في نهاية السطر، فتلتقط المخرجات والأخطاء معاً بدل أن تختفي في الفراغ.
لماذا سطر واحد أنظف من عشرات الأسطر
Laravel Scheduler يقلب المعادلة: بدل عشرات أسطر crontab، تضع سطراً واحداً فقط على السيرفر:
* * * * * /usr/bin/php /home/site/artisan schedule:run >> /dev/null 2>&1
هذا السطر يوقظ Laravel كل دقيقة، فيقرأ جدولك المعرّف في الكود ويقرر أي مهمة حان وقتها. الفائدة أن كل جدولك صار في ملف واحد تحت إدارة الإصدارات (Git)، تراه وتراجعه وتنشره كباقي الكود، بلا لمس crontab على السيرفر مجدداً. تكتب مثلاً daily() لمهمة يومية، وeveryFiveMinutes() لمهمة كل خمس دقائق، وhourly() و weeklyOn() وcron('0 3 * * *') إن أردت صيغة cron الخام لحالة خاصة. لو أردت إضافة مهمة جديدة، تكتبها في الكود وتنشر، بلا SSH ولا تحرير crontab يدوي معرّض للخطأ. هذا الفصل بين "محرّك التوقيت" (سطر cron الوحيد) و"تعريف المهام" (الكود) هو جوهر النظافة.
withoutOverlapping وonOneServer
مشكلتان تظهران في الإنتاج. الأولى التداخل: مهمة تأخذ ثلاث دقائق بينما جدولتها كل دقيقة، فتبدأ نسخة ثانية قبل انتهاء الأولى وتتراكم النسخ حتى تشبع السيرفر. الحل استدعاء withoutOverlapping() على المهمة، فيضع Laravel قفلاً يمنع بدء نسخة جديدة قبل انتهاء الجارية. الثانية تعدد السيرفرات: إذا كان تطبيقك يعمل على عدة سيرفرات خلف موازن حمل، فكل واحد سيشغّل schedule:run وقد تُنفّذ المهمة الواحدة عدة مرات، فترسل نفس البريد أو تصدر نفس الفاتورة مكرراً. الحل onOneServer()، فيتفق السيرفرات عبر قفل مشترك (Redis أو قاعدة البيانات) على أن واحداً فقط ينفّذها. هاتان الأداتان تحوّلان الجدولة من هشّة إلى موثوقة.
كيف تتحقق أن المهمة تعمل فعلاً
الجدولة الصامتة خطر: تظن مهمتك تعمل بينما هي متوقفة منذ أسبوع. تحقّق بثلاث طرق. الأولى السجلّات: وجّه مخرجات كل مهمة إلى ملف سجلّ خاص بها، وسجّل سطراً في بدايتها ونهايتها بختم زمني، فتفتح السجلّ وترى آخر تشغيل ومتى كان. الثانية أمر php artisan schedule:list الذي يعرض كل المهام المجدولة ومواعيدها القادمة، فتتأكد أن ما في الكود هو ما تتوقعه. الثالثة القفل نفسه: راقب أن قفل withoutOverlapping يُحرَّر بعد كل تشغيل، فلو بقي عالقاً لأن المهمة ماتت فجأة، تتوقف كل التشغيلات التالية بصمت، ولهذا اضبط مهلة القفل عبر withoutOverlapping(10) بالدقائق فيُحرَّر تلقائياً إن تعطّلت. للمهام الحرجة أضف مراقبة نبض (heartbeat) ترسل إشارة لخدمة خارجية عند كل تشغيل ناجح، فإذا انقطعت الإشارة يصلك تنبيه أن المهمة توقفت.
أمثلة عملية لمهام تجدولها فعلاً
الجدولة ليست مفهوماً مجرّداً، بل تخدم مهام يومية ملموسة في أي تطبيق حقيقي. تنظيف السجلّات القديمة مهمة ليلية تحذف ملفات تجاوزت مدة معينة فلا يمتلئ قرصك بصمت حتى يتوقف كل شيء. إرسال ملخّص يومي بالبريد للإدارة يعمل صباحاً في وقت ثابت. مزامنة بيانات مع نظام خارجي عبر API قد تعمل كل خمس دقائق. توليد فواتير الاشتراكات المستحقة يعمل مرة يومياً بعد منتصف الليل. تنبيهات انتهاء الاشتراك ترسل قبل الموعد بأيام محددة. أخذ نسخة احتياطية إضافية على مستوى التطبيق يعمل ليلاً. القاعدة العملية في توزيع الأوقات: باعد بين المهام الثقيلة فلا تتكدّس كلها الساعة الثالثة فجراً وتشبع المعالج دفعة واحدة، بل وزّعها على ساعات الهدوء المختلفة. اجعل المهام الحسّاسة للوقت (مثل التنبيهات) في أوقات دقيقة، والمهام الثقيلة غير المستعجلة (مثل التقارير والتنظيف) في أوقات انخفاض الحمل، فتحمي تجربة زوّارك من تباطؤ ناتج عن مهمة خلفية ضخمة تعمل وقت الذروة.
أين يعمل كل هذا: بيئة السيرفر
كل ما سبق يفترض وصولاً كاملاً للسيرفر عبر SSH، وهو ما تحصل عليه في خادم سحابي VPS حيث تملك صلاحية الجذر وتحرّر crontab بنفسك. الاستضافة المشتركة عادةً تحدّ من cron أو تمنع بعض الأوامر، فإذا كان مشروعك يعتمد على جدولة دقيقة ومهام خلفية، فالـVPS هو مكانه الطبيعي. سيرفرات كلاود في الرياض بزمن استجابة نحو 8ms، والتفعيل تلقائي خلال ثوانٍ بعد الدفع، فتبدأ بضبط جدولك فوراً. راجع الأسعار والباقات لتختار المواصفات المناسبة لحملك.
الخلاصة العملية
افهم الحقول الخمسة أولاً، واستخدم دائماً مسارات مطلقة ووجّه المخرجات لسجلّ. في تطبيق Laravel ضع سطر cron واحداً فقط يشغّل schedule:run كل دقيقة، وأدر كل مهامك من الكود تحت Git بدل حشو crontab. احمِ مهامك من التداخل بـwithoutOverlapping ومن التكرار على عدة سيرفرات بـonOneServer. تحقّق دائماً عبر السجلّات وschedule:list ومراقبة النبض للمهام الحرجة، فالجدولة التي لا تراقبها ليست جدولة، بل مفاجأة مؤجّلة. ابدأ بسيطاً بمهمة واحدة تكتب سطراً في سجلّها كل دقيقة، وتأكد أنها تعمل فعلاً على سيرفرك قبل أن تبني فوقها مهامك الحقيقية، فأساس متين في الجدولة يوفّر عليك ساعات تشخيص لمهمة توقّفت بصمت منذ أسبوع. فمتى استقرّ لديك سطر cron الواحد يشغّل schedule:run، صار كل ما تبقى مجرد كتابة نظيفة في الكود، تراجعها وتنشرها كأي ميزة أخرى بلا خوف من عبث يدوي على السيرفر.


