اگر crontab -e را باز کردهاید و نمیدانید چرا تسکتان ساعت ۳ بامداد اجرا نشده، یا چرا یک اسکریپت هر دقیقه بهجای هر ساعت ران میشود، مشکل تقریباً همیشه در ترتیب فیلدهاست. نحو کرون پنج فیلد دارد و ترتیب آنها ثابت است؛ جابهجایی دو فیلد باعث میشود زمانبندی بدون هیچ خطایی معنی دیگری پیدا کند. کرون چیزی را به شما نمیگوید. فقط اجرا نمیکند.
پنج فیلد نحو کرون و بازهٔ مجاز هرکدام
هر خط در crontab از چپ به راست این پنج فیلد را میخواند، بعد دستور:
دقیقه ساعت روزِ ماه ماه روزِ هفته دستور
0 3 * * * /usr/bin/php /var/www/backup.php
| فیلد | بازه | معنی |
|---|---|---|
| دقیقه | 0–59 | دقیقهٔ ساعت |
| ساعت | 0–23 | ساعت شبانهروز، نیمهشب صفر است |
| روز ماه | 1–31 | روز تقویمی |
| ماه | 1–12 | ژانویه یک است، نه صفر |
| روز هفته | 0–7 | صفر و هفت هر دو یکشنبهاند |
دو نکته که بیشترین خطا را میسازند: ماه از یک شروع میشود ولی روز هفته از صفر، و عدد ۷ هم یکشنبه است. اگر کسی از زبان دیگری عادت داشته باشد که ماه را از صفر بشمارد، زمانبندیاش یک ماه جابهجا میشود و تا پایان سال متوجه نمیشود.
عملگرها و عبارتهای خاص در نحو کرون
هر فیلد میتواند یکی از این چهار شکل را بگیرد:
- ستاره (
*): هر مقدار مجاز. یعنی «هر دقیقه» یا «هر ساعت». - لیست (
1,15,30): چند مقدار مشخص، با کاما. - بازه (
9-17): از مقدار اول تا آخر، شامل هر دو سر. - گام (
*/15یا10-50/5): هر n واحد.*/15در فیلد دقیقه یعنی ۰، ۱۵، ۳۰ و ۴۵.
ترکیب گام با بازه جایی است که خیلیها اشتباه میکنند. عبارت 10-50/5 در فیلد دقیقه یعنی از دقیقهٔ ۱۰ شروع کن و هر ۵ دقیقه جلو برو تا ۵۰؛ نتیجه ۱۰، ۱۵، ۲۰ و... میشود. اما 50/5 بدون بازه یعنی از ۵۰ تا ۵۹، نه «هر پنج دقیقه در کل ساعت». اگر میخواهید هر پنج دقیقه در تمام ساعت اجرا شود، */5 بنویسید.
نام ماه و روز هفته
بهجای عدد میتوانید سه حرف اول نام انگلیسی را بنویسید: mon، tue، jan، dec. حروف بزرگ و کوچک فرقی ندارند. بازه هم کار میکند: mon-fri یعنی روزهای کاری هفته.
عبارتهای اختصاری
کرون هشت ماکرو دارد که کل پنج فیلد را جایگزین میکنند:
| عبارت | معادل |
|---|---|
@reboot | یکبار، هنگام بالا آمدن سیستم |
@yearly / @annually | 0 0 1 1 * |
@monthly | 0 0 1 * * |
@weekly | 0 0 * * 0 |
@daily / @midnight | 0 0 * * * |
@hourly | 0 * * * * |
این عبارتها در نسخههای قدیمی cron پشتیبانی نمیشوند. اگر روی یک سیستم قدیمی کار میکنید و @daily بیصدا نادیده گرفته میشود، معادل عددی را بنویسید.
نمونههای زمانبندی پرکاربرد
| نحو کرون | زمان اجرا |
|---|---|
*/5 * * * * | هر ۵ دقیقه |
0 * * * * | ابتدای هر ساعت |
30 2 * * * | هر روز ساعت ۲:۳۰ بامداد |
0 3 * * 0 | یکشنبهها ساعت ۳ بامداد |
0 0 1 * * | اول هر ماه، نیمهشب |
0 9-17 * * 1-5 | هر ساعت، ۹ تا ۱۷، دوشنبه تا جمعه |
15 4 1,15 * * | روزهای ۱ و ۱۵ هر ماه، ساعت ۴:۱۵ |
0 */6 * * * | هر ۶ ساعت |
برای کارهای سنگین مثل بکاپ دیتابیس، ساعت ۲ تا ۴ بامداد انتخاب درستی است چون ترافیک سایت کم است. اما اگر روی هاست اشتراکی هستید، این ساعت شلوغترین زمان سرور است؛ همه بکاپهایشان را همانجا گذاشتهاند. ساعت ۱:۲۰ یا ۴:۴۰ انتخاب بهتری است. برای دیدن اینکه هر تسک چقدر از منابع را میخورد، مرجع کامل محدودیتهای منابع هاست توضیح میدهد هر عدد چه چیزی را میشمارد.
اینجا اشتباه میکنند
رایجترین اشتباهی که در تیکتها میبینم این است: کاربر یک اسکریپت PHP را با 0 3 * * * php /home/user/backup.php زمانبندی میکند، تسک اجرا میشود، ولی هیچ اتفاقی نمیافتد. علامت مشخصهاش این است که در /var/log/syslog خط CRON[12345]: (user) CMD (php /home/user/backup.php) را میبینید، یعنی کرون دستور را ران کرده، اما خروجی خالی است. علت تقریباً همیشه PATH است. کرون با محیط حداقلی اجرا میشود؛ php در PATH آن نیست یا نسخهٔ دیگری است. راهحل: مسیر مطلق بدهید، /usr/bin/php8.2، و در ابتدای crontab یک PATH=/usr/local/bin:/usr/bin:/bin بگذارید.
اشتباه دوم که سختتر پیدا میشود: خروجی اسکریپت به هیچجا نمیرود. کرون هر خروجی را بهعنوان ایمیل به کاربر محلی میفرستد. اگر MTA نصب نباشد، پیام در صف میماند و بعد از مدتی /var/mail پر میشود. همیشه انتهای دستور >> /var/log/backup.log 2>&1 بگذارید. اگر عمداً میخواهید هیچ خروجیای نباشد، >/dev/null 2>&1.
نکتهای که در مستندات پیدا نمیکنید
وقتی هم روز ماه و هم روز هفته مقدار مشخص دارند، کرون آنها را با OR ترکیب میکند، نه AND. یعنی 0 0 1 * mon یعنی «اول هر ماه، یا هر دوشنبه» — نه «اول ماه فقط اگر دوشنبه باشد». این رفتار در POSIX استاندارد است و در بیشتر پیادهسازیها حفظ شده. اگر واقعاً «اول ماه که دوشنبه باشد» میخواهید، باید در خود اسکریپت چک کنید یا از systemd timer با OnCalendar=Mon *-*-01 استفاده کنید.
نکتهٔ دوم: کرون از DST بیخبر است. اگر سرور روی زمان محلی تنظیم باشد و ساعت به عقب برود، تسکهای بین ساعت تکراری اجرا میشوند. روی سرورهای تولیدی همیشه timedatectl set-timezone UTC بزنید و ساعت تسکها را بر حسب UTC بنویسید. این کار همچنین مقایسهٔ لاگها بین سرورها را ممکن میکند.
برای کارهای طولانیمدت، کرون انتخاب اشتباهی است. اگر تسکی بیشتر از فاصلهٔ اجرایش طول بکشد، دو نسخه همزمان ران میشوند و روی دیتابیس قفل میخورند. راهحل ساده: flock. دستور را اینطور بنویسید:
*/10 * * * * /usr/bin/flock -n /tmp/sync.lock /usr/bin/php /var/www/sync.php
سوئیچ -n یعنی اگر قفل گرفته شده، اجرا نکن و خارج شو. بدون آن، flock منتظر میماند و صف تسکها روی هم انبار میشود.
کرون یا systemd timer؟
اگر روی سرور اختصاصی یا VPS با systemd کار میکنید و به لاگ ساختیافته، وابستگی به سرویسهای دیگر، یا کنترل منابع نیاز دارید، systemd timer انتخاب بهتری است. هر اجرا در journald ثبت میشود، میتوانید RandomizedDelaySec بگذارید تا همهٔ تسکها همزمان شروع نشوند، و با Persistent=true تسکهای ازدسترفته بعد از ریاستارت جبران میشوند. کرون هیچکدام را ندارد.
اما برای ۹۰ درصد کارهای روزمره روی هاست لینوکس — پاک کردن فایلهای temp، ران کردن cron وردپرس، چرخش لاگ — کرون سادهتر و کافی است. syntax را یکبار یاد میگیرید و همهجا کار میکند. من شخصاً تا وقتی به لاگ ساختیافته یا وابستگی سرویس نیاز پیدا نکنم، سراغ timer نمیروم.
یک محدودیت عملی که روی هاست اشتراکی به آن میخورید: حداقل فاصلهٔ اجرا معمولاً ۵ یا ۱۵ دقیقه است و تسکهای دقیقهای مسدود میشوند. اگر واقعاً به اجرای هر دقیقه نیاز دارید، معماری را عوض کنید: یک daemon بنویسید یا از queue worker استفاده کنید. برای دیدن ابزارهای تشخیصی و تست، ابزارهای رایگان وبمستر نقطهٔ شروع خوبی است.
پرسشهای پرتکرار
چطور بفهمم کرون تسک من را اجرا کرده یا نه؟
در سیستمهای با systemd، دستور journalctl -u cron --since "1 hour ago" هر اجرا را نشان میدهد. در سیستمهای قدیمیتر grep CRON /var/log/syslog جواب میدهد. اگر خط CMD را میبینید ولی اثرش نیست، مشکل در محیط اجرا یا خود اسکریپت است، نه در زمانبندی.
چرا تسک کرون من با PATH درست کار نمیکند؟
کرون با محیط حداقلی اجرا میشود و متغیرهای shell شما را نمیخواند. در ابتدای crontab مقدار PATH را صریح تعریف کنید و در دستور از مسیر مطلق باینری استفاده کنید. همین موضوع برای متغیرهای محیطی مثل DATABASE_URL هم صادق است؛ آنها را در crontab تعریف کنید یا از یک اسکریپت wrapper استفاده کنید.
تفاوت */5 با 0-59/5 در نحو کرون چیست؟
هیچ تفاوتی ندارند. هر دو یعنی هر پنج دقیقه در کل ساعت. تفاوت وقتی ظاهر میشود که بازه را محدود کنید: 10-50/5 فقط دقیقههای ۱۰ تا ۵۰ را پوشش میدهد و دقیقههای ۵۵، ۰ و ۵ را رد میکند.
آیا میتوانم کرون را روی چند سرور همزمان اجرا کنم بدون اینکه تسک دوبار ران شود؟
نه با کرون خالی. کرون هیچ مکانیزم قفل توزیعشدهای ندارد. اگر چند سرور پشت یک دیتابیس دارید، باید قفل را در لایهٔ دیتابیس یا با ابزاری مثل Redis پیاده کنید. راه سادهتر: تسک را فقط روی یک سرور زمانبندی کنید و بقیه از نتیجهٔ آن استفاده کنند.
اگر همین حالا یک crontab خراب دارید، اول با crontab -l محتوای فعلی را ببینید، بعد با crontab -e اصلاح کنید. قبل از ذخیره، پنج فیلد را دوباره از چپ به راست بشمارید. همین یک عادت، بیشتر تیکتهای «کرون کار نمیکند» را حذف میکند.