آموزش Cron Job در لینوکس با crontab و systemd timer

Cron Job دستوری است که سرویس cron در زمان مشخص و به‌صورت خودکار اجرا می‌کند. در این آموزش Cron Job در لینوکس، زمان‌بندی را با crontab -e تعریف می‌کنید، خروجی را در فایل لاگ ذخیره می‌کنید و با flock جلوی اجرای هم‌زمان را می‌گیرید. بیشتر خطاهای کرون از تفاوت محیط اجرای آن با ترمینال شما می‌آید: PATH کوتاه‌تر، shell متفاوت و نبودن متغیرهای محیطی.

برای دنبال کردن مثال‌ها باید با ترمینال، مسیر فایل‌ها و دسترسی‌ها آشنا باشید؛ اگر نیستید، ابتدا دستورات پایه لینوکس و مدیریت دسترسی در لینوکس را ببینید. دستورها روی Ubuntu و Debian (بسته cron) و روی RHEL، Rocky Linux و AlmaLinux (بسته cronie) قابل اجرا هستند و هر جا رفتار دو خانواده متفاوت است، جداگانه ذکر شده است. بخش آخر systemd timer را به‌عنوان جایگزین معرفی می‌کند.

آموزش Cron Job در لینوکس: ساختار crontab

هر خط crontab پنج فیلد زمان دارد و بعد از آن دستوری که باید اجرا شود. خط زیر اسکریپت بکاپ را هر شب ساعت ۲:۳۰ اجرا می‌کند:

# minute  hour  day-of-month  month  day-of-week  command
  30      2     *             *      *            /usr/local/bin/backup-db.sh
فیلدمقادیر مجاز
دقیقه0 تا 59
ساعت0 تا 23
روز ماه1 تا 31
ماه1 تا 12 یا نام کوتاه انگلیسی (jan، feb و …)
روز هفته0 تا 7 (0 و 7 هر دو یکشنبه، 6 شنبه) یا نام کوتاه (sun، mon و …)

در هر فیلد، ستاره یعنی «همه مقادیر»، ویرگول فهرست می‌سازد (1,15)، خط تیره بازه تعریف می‌کند (9-17) و اسلش گام را مشخص می‌کند (‎*/5). چند نمونه پرکاربرد:

عبارتزمان اجرا
*/5 * * * *هر ۵ دقیقه
0 * * * *ابتدای هر ساعت
30 2 * * *هر روز ساعت ۲:۳۰ بامداد
0 9 * * 6,0-3شنبه تا چهارشنبه ساعت ۹ صبح
0 3 * * 5جمعه‌ها ساعت ۳ بامداد
0 0 1 * *اولین روز هر ماه میلادی ساعت ۰۰:۰۰

دو رفتار که غافلگیر می‌کند

  • اگر هم روز ماه و هم روز هفته محدود شده باشند (هیچ‌کدام ستاره نباشد)، دستور وقتی اجرا می‌شود که یکی از آن‌ها برقرار باشد. خط 0 4 1 * 5 هم روز اول ماه و هم هر جمعه اجرا می‌شود و به جمعه‌هایی که روز اول ماه هستند محدود نمی‌ماند.
  • گام در هر بازه از ابتدای همان بازه شروع می‌شود. ‎*/23 در فیلد ساعت یعنی ساعت 0 و 23 هر روز، نه «هر ۲۳ ساعت یک بار».

به جای پنج فیلد می‌توانید رشته‌های ویژه را هم به کار ببرید: @reboot پس از هر راه‌اندازی سیستم، @hourly معادل 0 * * * *، @daily معادل 0 0 * * *، @weekly معادل 0 0 * * 0، و @monthly و @yearly.

دستورات crontab و محل فایل‌های کرون

crontab -e                          # edit the current user's crontab
crontab -l                          # list jobs
sudo crontab -u www-data -l         # list another user's jobs
crontab -l > ~/crontab-backup-$(date +%F).txt
crontab -r                          # removes ALL jobs of the user

crontab -r بدون پرسش همه کارهای کاربر را پاک می‌کند و روی صفحه‌کلید کنار e قرار دارد؛ پیش از ویرایش‌های مهم با crontab -l نسخه پشتیبان بگیرید. پس از ذخیره فایل در crontab -e نیازی به ری‌استارت سرویس نیست؛ cron تغییر فایل‌ها را خودش تشخیص می‌دهد و آن‌ها را دوباره می‌خواند. اگر نحو خطی اشتباه باشد، crontab هنگام ذخیره خطا می‌دهد و فایل را نصب نمی‌کند.

محلفرمت خطکاربرد
crontab کاربر (crontab -e)پنج فیلد و دستورکارهای یک کاربر مشخص
‎/etc/crontabپنج فیلد، نام کاربر، دستورکرون سیستمی
‎/etc/cron.d/مانند ‎/etc/crontabکارهای سیستمی هر برنامه در فایل جداگانه
‎/etc/cron.daily، weekly و monthlyاسکریپت اجرایی بدون خط زماناجرا با run-parts یا anacron

در فایل‌های ‎/etc/crontab و ‎/etc/cron.d ستون ششم نام کاربری است که دستور با آن اجرا می‌شود و فراموش کردن آن رایج‌ترین خطاست. در Debian و Ubuntu نام فایل‌های ‎/etc/cron.d فقط می‌تواند حروف، عدد، خط تیره و زیرخط داشته باشد؛ فایلی به نام backup.cron یا db.backup به دلیل داشتن نقطه نادیده گرفته می‌شود. نمونه یک فایل درست:

# /etc/cron.d/db-backup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
30 2 * * * root /usr/local/bin/backup-db.sh >> /var/log/backup-db.log 2>&1

محیط اجرای Cron: چرا اسکریپت در ترمینال کار می‌کند و در کرون نه؟

cron دستور را در یک محیط حداقلی اجرا می‌کند: SHELL برابر ‎/bin/sh است، HOME و LOGNAME از ‎/etc/passwd خوانده می‌شوند و فایل‌هایی مثل ‎.bashrc و ‎.profile بارگذاری نمی‌شوند. PATH هم کوتاه است؛ در cron دبیان و اوبونتو ‎/usr/bin:/bin و در cronie خانواده RHEL ‎/usr/bin:/bin:/usr/sbin:/sbin. به همین دلیل دستوری که در ‎/usr/local/bin یا ‎/usr/sbin است در ترمینال اجرا می‌شود و در کرون خطای command not found می‌دهد.

  • مسیر کامل برنامه‌ها را بنویسید. مسیر هر دستور را با command -v php پیدا کنید.
  • یا PATH را در ابتدای crontab تعریف کنید، مانند نمونه فایل ‎/etc/cron.d بالا.
  • اگر اسکریپت از امکانات bash مثل آرایه یا [[ ]] استفاده می‌کند، آن را با bash /usr/local/bin/script.sh صدا بزنید یا SHELL=/bin/bash را در crontab بگذارید.
  • پوشه کاری کرون، پوشه خانه کاربر است. در اسکریپت از مسیر مطلق استفاده کنید یا دستور را با cd /var/www/app && شروع کنید.
  • متغیرهایی که برنامه لازم دارد، مثل رشته اتصال دیتابیس، را داخل خود اسکریپت از فایل مشخصی بخوانید.

کاراکتر درصد در خط crontab معنای خاص دارد: cron آن را به خط جدید تبدیل می‌کند و هر چه بعد از اولین % بیاید به ورودی دستور فرستاده می‌شود. نتیجه، دستوری ناقص است که بی‌صدا شکست می‌خورد. آن را با بک‌اسلش escape کنید:

# Wrong: % becomes a newline
0 1 * * * tar czf /backup/www-$(date +%F).tar.gz /var/www

# Right
0 1 * * * tar czf /backup/www-$(date +\%F).tar.gz /var/www

# Reproduce cron's minimal environment when testing a script
env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME="$HOME" /bin/sh -c '/usr/local/bin/backup-db.sh'

لاگ و خروجی Cron Job

cron خروجی هر دستور را برای صاحب crontab ایمیل می‌کند؛ اگر MAILTO خالی باشد ایمیلی ارسال نمی‌شود و روی سروری که سرویس ایمیل محلی ندارد، این خروجی معمولاً از دست می‌رود. پس خروجی و خطا را خودتان به فایل یا ژورنال سیستم بفرستید:

# Append stdout and stderr to a log file
*/10 * * * * /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1

# Or send output to the system journal with a tag
*/10 * * * * /usr/local/bin/sync.sh 2>&1 | /usr/bin/logger -t sync-job

# Did cron start the job?
journalctl -u cron --since today       # Debian / Ubuntu
journalctl -u crond --since today      # RHEL / Rocky / AlmaLinux
journalctl -t sync-job --since today   # output sent with logger

ترتیب هدایت خروجی مهم است: >> file 2>&1 هر دو خروجی را در فایل می‌نویسد، اما 2>&1 >> file خطاها را همچنان به خروجی اصلی می‌فرستد. لاگ سرویس cron فقط نشان می‌دهد دستور شروع شده است، نه اینکه موفق بوده؛ موفقیت را باید از خروجی اسکریپت یا کد خروج آن فهمید. برای فایل‌های لاگی که خودتان می‌سازید، یک فایل در ‎/etc/logrotate.d تعریف کنید تا حجمشان کنترل شود.

جلوگیری از اجرای هم‌زمان با flock و timeout

اگر کاری که هر ۵ دقیقه اجرا می‌شود گاهی بیشتر از ۵ دقیقه طول بکشد، اجرای بعدی روی اجرای قبلی می‌افتد: بار سرور دو برابر می‌شود و خروجی مشترک، مثلاً فایل بکاپ، ممکن است خراب شود. flock از بسته util-linux این مشکل را حل می‌کند:

*/5 * * * * /usr/bin/flock -n /run/lock/sync.lock /usr/bin/timeout 50m /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1

با ‎-n، اگر قفل در دست اجرای قبلی باشد، flock بلافاصله خارج می‌شود و اجرای جدید انجام نمی‌شود. timeout اجرایی را که گیر کرده پس از مدت مشخص متوقف می‌کند تا قفل برای همیشه نگه داشته نشود. برای کرون کاربر غیر root، فایل قفل را در پوشه‌ای بگذارید که آن کاربر اجازه نوشتن در آن را دارد. اگر چند سرور کار مشابهی دارند، همه را روی دقیقه صفر تنظیم نکنید تا بار روی مقصد مشترک، مثل سرور بکاپ، یکجا وارد نشود.

خطاهای رایج Cron Job و راه‌حل آن‌ها

نشانهعلت محتملراه‌حل
command not found در خروجیPATH کرون کوتاه‌تر از ترمینال استمسیر کامل دستور یا تعریف PATH در ابتدای crontab
فایل ‎/etc/cron.d اجرا نمی‌شودنام فایل نقطه دارد یا ستون نام کاربر نوشته نشدهتغییر نام فایل و افزودن نام کاربر پیش از دستور
دستور حاوی date ناقص اجرا می‌شودکاراکتر % escape نشدهنوشتن ‎\%‎ به جای %
خطای ‎[[: not foundاسکریپت bash با sh اجرا شدهSHELL=/bin/bash یا اجرای اسکریپت با bash
اجرا در ساعت اشتباهمنطقه زمانی سیستم متفاوت است یا پس از تغییر آن cron ری‌استارت نشدهبررسی با timedatectl و ری‌استارت سرویس cron یا crond
Permission deniedاسکریپت اجرایی نیست یا کاربر کرون به فایل دسترسی نداردchmod +x و اجرای کرون با کاربر درست
پیام not allowed to use this programکاربر در ‎/etc/cron.deny است یا در ‎/etc/cron.allow نیستاصلاح فایل‌های cron.allow و cron.deny
بار سرور هر چند دقیقه یک بار بالا می‌روداجرای قبلی تمام نشده و اجرای جدید شروع شدهflock -n و timeout

systemd timer: جایگزین Cron در توزیع‌های جدید

روی توزیع‌هایی که systemd دارند، هر کار زمان‌بندی‌شده را می‌توان با یک فایل service و یک فایل timer تعریف کرد. خروجی به‌طور خودکار در ژورنال همان unit ثبت می‌شود، اجرای جاافتاده در زمان خاموشی سرور قابل جبران است و محدودیت منابع و وابستگی به سرویس‌های دیگر را در همان فایل service تنظیم می‌کنید. همان بکاپ شبانه با systemd timer:

# /etc/systemd/system/db-backup.service
[Unit]
Description=Nightly database backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-db.sh

# /etc/systemd/system/db-backup.timer
[Unit]
Description=Run db-backup daily at 02:30

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=5m

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer
systemctl list-timers --all
systemd-analyze calendar "*-*-* 02:30:00"      # check the schedule and next run
sudo systemctl start db-backup.service          # run once now for testing
journalctl -u db-backup.service --since today

اگر در بخش Unit فایل timer نام سرویس را ننویسید، systemd سرویسی با همان نام و پسوند service را اجرا می‌کند. Persistent=true زمان آخرین اجرا را روی دیسک نگه می‌دارد و اگر سرور در زمان مقرر خاموش بوده، پس از روشن شدن کار را اجرا می‌کند؛ این تنظیم تنها روی تایمرهای OnCalendar اثر دارد. RandomizedDelaySec زمان اجرا را به‌صورت تصادفی تا مقدار تعیین‌شده عقب می‌اندازد.

ویژگیCronsystemd timer
تعریفیک خطدو فایل unit
ثبت خروجیباید دستی به فایل یا logger هدایت شودخودکار در ژورنال
اجرای جاافتاده پس از خاموشیندارد (برای کارهای روزانه anacron)Persistent=true
تست زمان‌بندیابزار داخلی نداردsystemd-analyze calendar
محدودیت منابع و وابستگینداردبا تنظیمات فایل service
قابلیت انتقالروی هر سیستم یونیکس‌مانندفقط سیستم‌های دارای systemd

برای یک دستور ساده که روی سرورهای مختلف تکرار می‌شود، cron هنوز سریع‌ترین راه است. برای کارهای مهمی مثل بکاپ که باید ردشان در لاگ بماند و اجرای جاافتاده‌شان جبران شود، systemd timer انتخاب منطقی‌تری است. هر روشی که انتخاب می‌کنید، شکست بکاپ را فقط به لاگ نسپارید و نتیجه را با مانیتورینگ سرور یا دست‌کم یک هشدار ساده پیگیری کنید.

سوالات متداول

چطور مطمئن شوم Cron Job اجرا شده است؟

ابتدا لاگ سرویس را با journalctl -u cron (یا crond) ببینید تا معلوم شود دستور شروع شده است. سپس فایل لاگی را که خروجی را در آن نوشته‌اید بخوانید. اسکریپت‌های مهم باید در پایان کار یک خط موفقیت با تاریخ بنویسند تا نبودن آن خط نشانه مشکل باشد.

آیا بعد از تغییر crontab باید cron را ری‌استارت کنم؟

نه. cron تغییر فایل‌های crontab را تشخیص می‌دهد و آن‌ها را دوباره می‌خواند. ری‌استارت فقط پس از تغییر منطقه زمانی سیستم لازم است.

می‌توان کاری را هر چند ثانیه یک بار با کرون اجرا کرد؟

کوچک‌ترین واحد cron یک دقیقه است. برای بازه‌های کوتاه‌تر از systemd timer با OnUnitActiveSec استفاده کنید و AccuracySec را کم کنید، چون دقت پیش‌فرض تایمرها یک دقیقه است. اگر کاری واقعاً هر چند ثانیه لازم است، شاید باید سرویسی دائمی باشد، نه کار زمان‌بندی‌شده.

crontab -e بهتر است یا فایل در ‎/etc/cron.d؟

برای کارهای شخصی یک کاربر crontab -e مناسب است و هنگام ذخیره نحو را بررسی می‌کند. برای کارهای سیستمی یا کارهایی که با ابزار اتوماسیون مثل Ansible روی چند سرور پخش می‌شوند، فایل جداگانه در ‎/etc/cron.d مدیریت و بازبینی را ساده‌تر می‌کند.

پاسخی بگذارید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *