این یکی از آن باگهایی است که آدم را دیوانه میکند، چون دستور را کپی میکنید، در ترمینال میزنید، بیعیب کار میکند — و همان دستور در کرونجاب هیچ کاری نمیکند.
روی سرور ما این ماجرا دو بار تکرار شد. اولی سر یک کرون آمار و دومی سر تولید نقشهی سایت. هر دو بار همان علت.
علامت#
در ترمینال سیپنل این کار میکند:
php84 artisan schedule:runهمین را در کرون میگذارید:
* * * * * cd /home/USER/app && php84 artisan schedule:runو در ایمیل کرون یا لاگ این را میگیرید:
/bin/sh: php84: command not foundعلت: php84 یک باینری نیست#
نکتهی اصلی اینجاست و برای خیلیها غافلگیرکننده است: php84 روی سیپنل معمولاً یک برنامه در مسیر جستجو نیست، یک نام مستعار است.
ببینید:
type php84اگر جوابی شبیه این گرفتید، ماجرا همین است:
php84 is aliased to `/opt/cpanel/ea-php84/root/usr/bin/php'نامهای مستعار در فایل ~/.bashrc تعریف میشوند. و این فایل فقط در شل تعاملی خوانده میشود — یعنی وقتی شما پشت یک ترمینال نشستهاید.
کرون شل تعاملی نیست. کرون معمولاً دستور را با /bin/sh اجرا میکند، محیط تقریباً خالی است و ~/.bashrc هرگز خوانده نمیشود. پس آن نام مستعار در دنیای کرون اصلاً وجود ندارد.
همین منطق برای اینها هم صادق است:
- توابع تعریفشده در
~/.bashrc - متغیرهای محیطی که در
~/.bash_profileست میکنید - تغییرات
PATHکه در فایلهای پروفایل نوشتهاید - محیط
nvmو مدیر نسخهی مشابه برای Node
راهحل: مسیر مطلق#
هیچ ترفندی لازم نیست. فقط مسیر کامل باینری را بنویسید.
اول پیدایش کنید:
type php84
# یا
ls -d /opt/cpanel/ea-php*/root/usr/bin/phpبعد در کرون از همان استفاده کنید:
* * * * * cd /home/USER/app && /opt/cpanel/ea-php84/root/usr/bin/php artisan schedule:run >> /dev/null 2>&1تست کنید که واقعاً کار میکند، ولی نه در ترمینال معمولی — در محیطی شبیه کرون:
env -i /bin/sh -c '/opt/cpanel/ea-php84/root/usr/bin/php -v'سوییچ -i محیط را خالی میکند، پس اگر اینجا جواب داد در کرون هم جواب میدهد. این یک تست واقعی است، برخلاف زدن همان دستور در ترمینال که چیزی را ثابت نمیکند.
دام دوم: هیچ خروجیای نمیبینید#
الگوی رایج کرون این است:
>> /dev/null 2>&1که یعنی «هم خروجی معمولی و هم خطا را دور بریز». برای کرونی که درست کار میکند مناسب است، ولی موقع عیبیابی فاجعه است: اگر کرون شکست بخورد هیچ ردی نمیماند.
موقع راهاندازی، خروجی را به یک فایل بفرستید:
* * * * * cd /home/USER/app && /opt/cpanel/ea-php84/root/usr/bin/php artisan schedule:run >> /home/USER/cron.log 2>&1و بعد ببینیدش:
tail -50 /home/USER/cron.logنکتهی ظریف: 2>&1 باید بعد از هدایت خروجی اصلی بیاید. اگر جایشان را عوض کنید، خطاها به مقصد قبلی میروند و در فایل نمینشینند.
وقتی مطمئن شدید همه چیز درست است، فایل لاگ را حذف کنید یا چرخش برایش بگذارید؛ وگرنه یک کرون دقیقهای در چند ماه فایل بزرگی میسازد.
دام سوم: پوشهی جاری#
کرون از پوشهی خانه شروع میکند، نه از ریشهی پروژه. اگر cd را جا بیندازید، لاراول فایل .env را پیدا نمیکند و خطای پیکربندی میگیرید که ربطش به کرون اصلاً معلوم نیست.
دو راه:
# راه اول: تغییر پوشه
* * * * * cd /home/USER/app && /opt/.../php artisan schedule:run
# راه دوم: مسیر کامل فایل artisan
* * * * * /opt/.../php /home/USER/app/artisan schedule:runراه دوم تمیزتر است چون به شل وابسته نیست.
دام چهارم: نسخهی PHP کرون با وب یکی نیست#
سیپنل بخش «Cron Jobs» دارد که ممکن است دستور را با مفسر پیشفرض سیستم اجرا کند — همان نسخهی قدیمی. اگر پروژهتان روی نسخهی جدید نوشته شده، کرون با خطای نحوی میشکند در حالی که سایت سالم کار میکند.
این یکی از نشانههای همان مشکل بزرگتری است که در چکلیست هاست اشتراکی توضیح دادیم: روی سیپنل، نسخهی وب و نسخهی خط فرمان دو چیز جدا هستند.
زمانبند لاراول و فاصلهی مجاز کرون#
لاراول طوری طراحی شده که یک کرون هر دقیقهای داشته باشید و بقیهی زمانبندی داخل کد انجام شود:
Schedule::command('stats:rollup')->dailyAt('03:10');
Schedule::command('sitemap:build')->dailyAt('04:00');اگر هاست اجازهی کرون دقیقهای نمیدهد و حداقل را پنج دقیقه گذاشته، باید بدانید چه اتفاقی میافتد: کارهایی که دقیقاً روی دقیقهای تنظیم شدهاند که کرون در آن اجرا نمیشود، هیچوقت اجرا نمیشوند. کار روزانهی ساعت ۳:۱۰ فقط وقتی اجرا میشود که کرون دقیقاً در ۳:۱۰ بیدار شود.
دو راهحل عملی:
- زمان کارها را با فاصلهی کرون هماهنگ کنید (مثلاً همه روی مضارب پنج).
- یا از
->everyFiveMinutes()و بازههای بزرگتر استفاده کنید که با هر بیدارشدن همخوانی دارند.
دام پنجم: علامت درصد در کرون#
این یکی از آن چیزهایی است که فقط یک بار به آن میخورید و برای همیشه یادتان میماند.
در فایل کرون، علامت % معنای خاص دارد: کرون آن را به خط جدید تبدیل میکند و بقیهی خط را بهعنوان ورودی استاندارد به دستور میدهد. یعنی این خط:
0 3 * * * mysqldump -u USER -pPASS DB | gzip > /home/USER/backup-$(date +%F).sql.gzدر عمل به این تبدیل میشود که دستور در + تمام میشود و بقیهاش اصلاً اجرا نمیشود. نتیجه یک فایل بکآپ با نام عجیب یا یک شکست بیصداست.
راهحل: هر % را خنثی کنید.
0 3 * * * mysqldump -u USER -pPASS DB | gzip > /home/USER/backup-$(date +\%F).sql.gzیا بهتر از آن، دستور را در یک اسکریپت بگذارید و کرون فقط اسکریپت را صدا بزند. آنوقت هیچ کاراکتری تفسیر نمیشود:
0 3 * * * /bin/bash /home/USER/scripts/backup.shاین الگو را برای هر کرونی که بیش از یک دستور دارد توصیه میکنیم. عیبیابی اسکریپت بینهایت سادهتر از عیبیابی یک خط طولانی در فرم کرون سیپنل است.
دام ششم: اجرای همزمان#
اگر کاری بیشتر از فاصلهی کرون طول بکشد، نسخهی بعدی شروع میشود در حالی که قبلی هنوز تمام نشده. برای کاری که در دیتابیس مینویسد، این میتواند دادهی تکراری بسازد.
زمانبند لاراول برای همین withoutOverlapping دارد:
Schedule::command('stats:rollup')->hourly()->withoutOverlapping();و اگر اسکریپت شل خودتان را دارید، از قفل فایل استفاده کنید:
#!/bin/bash
exec 9>/home/USER/locks/backup.lock
flock -n 9 || exit 0
# ادامهی کارسوییچ -n یعنی «اگر قفل گرفته است، صبر نکن و خارج شو». دقیقاً همان چیزی که برای کرون میخواهید.
متغیرهای محیطی در کرون#
اگر اسکریپتتان به متغیر محیطی نیاز دارد، کرون آن را ندارد. سه راه:
۱. در همان خط کرون تعریفش کنید:
0 3 * * * APP_ENV=production /opt/.../php /home/USER/app/artisan backup:run۲. در بالای فایل کرون بگذارید (روی همهی خطوط اثر میگذارد):
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=you@example.comمورد دوم مفید است: با تعریف MAILTO، خروجی هر کرون به ایمیل شما میرود. برای دورهی راهاندازی عالی است، ولی بعدش حذفش کنید وگرنه یک کرون دقیقهای صندوق شما را پر میکند.
۳. اسکریپت خودش فایل محیط را بخواند — که تمیزترین راه است، ولی حواستان به مشکل پایانخط باشد.
چکلیست عیبیابی کرون#
وقتی کرونی اجرا نمیشود، به همین ترتیب پیش بروید:
- آیا کرون اصلاً اجرا میشود؟ یک خط ساده بگذارید که فقط تاریخ را در فایلی بنویسد و پنج دقیقه صبر کنید.
- آیا مفسر پیدا میشود؟ با
env -iتست کنید. - آیا پوشهی جاری درست است؟ مسیر مطلق
artisanرا بدهید. - آیا خروجی را دور نریختهاید؟ موقتاً به فایل هدایتش کنید.
- آیا خطا در لاگ خود لاراول هست؟ فایلهای
storage/logsرا ببینید.
مورد اول را جدی بگیرید. چند بار دیدهایم که تمام وقت صرف عیبیابی دستور شده در حالی که کرون اصلاً ثبت نشده بود.
الگویی که در نهایت انتخاب کردیم#
بعد از دو بار خوردن به همین مشکل، کرونها را اینطور سازمان دادیم:
یک اسکریپت واحد در ~/scripts/cron.sh که همه چیز را داخل خودش دارد:
#!/bin/bash
set -euo pipefail
PHP=/opt/cpanel/ea-php84/root/usr/bin/php
APP=/home/USER/app
LOG=/home/USER/logs/cron.log
cd "$APP"
echo "[$(date -Is)] شروع" >> "$LOG"
"$PHP" artisan schedule:run >> "$LOG" 2>&1
echo "[$(date -Is)] پایان" >> "$LOG"و در فرم کرون سیپنل فقط یک خط:
* * * * * /bin/bash /home/USER/scripts/cron.shمزیتهایش:
- مسیر مفسر یک جا تعریف شده. ارتقای نسخهی PHP یعنی تغییر یک خط، نه گشتن در فرم کرون.
- خط
set -euo pipefailباعث میشود اسکریپت روی اولین خطا متوقف شود بهجای اینکه بیصدا ادامه دهد. - زمان شروع و پایان ثبت میشود، پس میفهمید کرون اصلاً اجرا شده یا نه — که سؤال اول هر عیبیابی است.
- هیچ کاراکتری در فرم کرون تفسیر نمیشود.
فقط یادتان باشد اسکریپت باید مجوز اجرا داشته باشد و پایانخط یونیکسی. اگر روی ویندوز نوشتید و آپلود کردید، حتماً این نکته را ببینید.
chmod +x ~/scripts/cron.shچرخش لاگ، چون کسی یادش نمیماند#
یک کرون دقیقهای که دو خط در لاگ مینویسد، در ماه حدود نود هزار خط تولید میکند. اگر رهایش کنید، بالاخره فضای حساب را میخورد.
سادهترین راه بدون نیاز به ابزار خاص، یک کرون هفتگی است که فایل را کوتاه میکند:
0 4 * * 0 tail -n 5000 /home/USER/logs/cron.log > /home/USER/logs/cron.tmp && mv /home/USER/logs/cron.tmp /home/USER/logs/cron.logپنج هزار خط آخر برای عیبیابی همیشه کافی است و فایل هیچوقت از چند مگابایت بزرگتر نمیشود.
قاعدهای که برای همیشه نگه میداریم: در کرونجاب و هر اسکریپت خودکار، همیشه مسیر مطلق باینری — نه نام مستعار، نه اتکا به مسیر جستجو.