ทำไม Cron Job ไม่ทำงาน? 5 สาเหตุที่พบบ่อยและวิธีแก้ไข
พบกับ 5 สาเหตุหลักที่ทำให้ cron job ไม่ทำงานตามกำหนดการ ทั้งปัญหา PATH, การใช้เครื่องหมายเปอร์เซ็นต์, การระบุ crontab ผิดพลาด, การสูญหายของ log และการตั้งค่า shell ที่ไม่ถูกต้อง
เหตุใด cron job ของคุณจึงไม่ทำงาน
cron job ที่ "ไม่เคยทำงาน" แท้จริงแล้วเกือบทั้งหมดได้ทำงานไปแล้ว มันทำงานในสภาพแวดล้อมที่ไม่ใช่ shell ของคุณ มันล้มเหลวในวินาทีแรก และข้อความแจ้งเตือนถูกส่งไปยังที่ที่คุณไม่ได้ตรวจสอบ สาเหตุ 5 ประการอธิบายปัญหาที่พบได้เกือบทั้งหมด ได้แก่: search path, เครื่องหมายเปอร์เซ็นต์, การใช้ไฟล์ crontab ผิดไฟล์, ผลลัพธ์ที่ถูกส่งไปยัง mail และสคริปต์ที่คาดหวัง login session
cron เป็น daemon (บริการเบื้องหลัง) ที่อ่านไฟล์ crontab และเริ่มคำสั่งตามกำหนดการ มันไม่อ่าน .bashrc ของคุณ มันไม่เปิด terminal มันไม่เริ่ม login shell และมันไม่แจ้งให้คุณทราบเมื่อคำสั่งล้มเหลว ทุกสาเหตุที่ระบุด้านล่างล้วนเกิดจากข้อเท็จจริง 4 ประการนี้
ให้ตรวจสอบตามลำดับ และเริ่มจากคำถามพื้นฐานที่สุด: cron ได้เริ่มทำงานจริงหรือไม่? "cron ไม่เคยเริ่มงาน" และ "งานเริ่มแล้วตายไป" เป็นปัญหาที่แตกต่างกันโดยสิ้นเชิงและไม่มีส่วนใดเกี่ยวข้องกัน ดังนั้นให้ตอบคำถามนี้ก่อนเป็นอันดับแรก
cron ทำงานหรือไม่?
daemon มีชื่อ unit ต่างกันไปตามตระกูลของ distribution ให้ตรวจสอบทั้งสองชื่อ แล้วจึงอ่าน log
systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"Debian และ Ubuntu ใช้ชื่อ unit ว่า cron ส่วน Fedora, Rocky และ Alma ใช้ชื่อว่า crond บนเครื่องหนึ่งเครื่องจะมีเพียงชื่อเดียวเท่านั้น ดังนั้นการที่คำสั่งหนึ่งรายงานว่าไม่พบ unit จึงเป็นเรื่องปกติและไม่ใช่ความผิดพลาด
ให้อ่านรายการที่ระบบของคุณบันทึกไว้เอง อย่าพยายามหาบรรทัดที่คัดลอกมาจากคู่มืออื่น เพราะถ้อยคำในแต่ละ implementation ของ cron และการตั้งค่า logging นั้นแตกต่างกัน คุณกำลังตรวจสอบเพียงสองสิ่งเท่านั้น คือ มีรายการบันทึกในนาทีที่คุณกำหนดไว้หรือไม่ และรายการนั้นระบุคำสั่งของคุณหรือไม่ หากมีรายการที่ระบุคำสั่งของคุณ แสดงว่า cron ทำงานตามหน้าที่แล้ว และความล้มเหลวเกิดขึ้นภายในตัวคำสั่งนั้นเอง หากไม่มีรายการใดเลย แสดงว่า cron ไม่ได้รับตารางเวลาของคุณ ซึ่งเป็นสาเหตุที่ 3 ด้านล่าง
image บางตัวส่งข้อความของ cron ผ่าน rsyslog ไปยังไฟล์แทนที่จะเป็น journal ให้ตรวจสอบใน /var/log เพื่อหาไฟล์ที่ตั้งชื่อตาม cron หรือ syslog จากนั้นให้อ่านส่วนท้ายของไฟล์นั้น
ls -l /var/log
sudo tail -n 50 /var/log/syslogหากไม่มีทั้ง unit และ log เป็นไปได้ว่า cron อาจไม่ได้ถูกติดตั้งไว้ image แบบ minimal บน cloud และ container มักจะตัดส่วนนี้ออกไป
dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cronสาเหตุที่ 1: cron ไม่มีค่า PATH ของคุณ
Shell แบบโต้ตอบ (interactive shell) ของคุณสร้าง PATH จาก /etc/profile, ~/.profile, ~/.bashrc และทุกไฟล์ที่ไฟล์เหล่านั้นเรียกใช้ แต่ไม่มีสิ่งเหล่านี้ทำงานใน cron job โดย cron จะเริ่มคำสั่งด้วยสภาพแวดล้อมจำกัดของตัวเอง ดังนั้นโปรแกรมที่อยู่ในไดเรกทอรีนอกเหนือจากไดเรกทอรีมาตรฐานของระบบจึงไม่ถูกพบ สิ่งที่อยู่ภายใต้ /usr/local/bin, /opt, ตัวจัดการเวอร์ชันภาษา (language version manager), Python virtual environment หรือ Go workspace มักจะประสบปัญหานี้ งานจะล้มเหลวตั้งแต่บรรทัดแรก และ shell จะเขียนข้อผิดพลาดในลักษณะ "not found" ซึ่งข้อความที่แน่นอนจะขึ้นอยู่กับว่า shell ใดเป็นผู้รันคำสั่งนั้น
ให้ค้นหาพาธจริง (absolute path) ของทุกคำสั่งที่งานของคุณใช้งาน
command -v docker
command -v node
readlink -f "$(command -v node)"จากนั้นให้เขียนพาธแบบเต็มเหล่านั้นลงในงาน หรือกำหนดค่า PATH ไว้ที่ด้านบนสุดของ crontab
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool runคัดลอกรายการดังกล่าวจากเครื่องของคุณด้วย echo "$PATH" และตัดสิ่งที่มีอยู่เฉพาะภายในเซสชันแบบโต้ตอบออก มีกฎข้อหนึ่งที่สำคัญคือ cron จะไม่ขยายตัวแปร (expand variables) ในบรรทัดการกำหนดค่าเหล่านี้ PATH=$PATH:/usr/local/bin จะเก็บข้อความตัวอักษร $PATH:/usr/local/bin ไว้ตรงๆ ส่งผลให้งานของคุณมีพาธการค้นหาที่ไม่มีไดเรกทอรีที่ใช้งานได้จริงเลย ดังนั้นให้เขียนรายการทั้งหมดออกมาให้ครบถ้วน
ตัวจัดการเวอร์ชัน (version manager) ต้องการมากกว่าแค่พาธ โดย nvm, pyenv, rbenv และ asdf จะติดตั้งฟังก์ชัน shell หรือไดเรกทอรี shims จาก .bashrc ของคุณ ซึ่ง cron job จะไม่อ่านไฟล์นั้น ให้เรียกใช้ binary ที่ระบุเวอร์ชันด้วยพาธแบบเต็ม หรือเรียกใช้ (source) สคริปต์เริ่มต้นของตัวจัดการนั้นเป็นบรรทัดแรกของสคริปต์ของคุณเอง
สาเหตุที่ 2: เครื่องหมายเปอร์เซ็นต์สิ้นสุดคำสั่งของคุณ
ในช่องคำสั่งของ crontab นั้น % ไม่ใช่ตัวอักษรทั่วไป เครื่องหมาย % ตัวแรกที่ไม่ได้ถูก escape จะถือเป็นการสิ้นสุดคำสั่ง ข้อมูลทั้งหมดหลังจากนั้นจะถูกส่งไปยังคำสั่งในรูปแบบ standard input และ % แต่ละตัวที่ตามมาจะกลายเป็นบรรทัดใหม่ นี่เป็นฟีเจอร์จริงของ cron สำหรับการป้อนข้อมูลสั้นๆ ให้กับโปรแกรม และเป็นเหตุผลว่าทำไมการตั้งชื่อไฟล์ด้วยวันที่จึงเป็นรายการ crontab ที่พบบ่อยว่าทำงานผิดพลาด
หากเขียน 0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site โปรแกรม tar จะไม่ได้รับวันที่ที่จัดรูปแบบไว้ เพราะ cron ตัดบรรทัดที่ % ตัวแรก ทำให้ shell ได้รับคำสั่ง substitution ที่ไม่สมบูรณ์ และส่วนที่เหลือของบรรทัดจะถูกส่งเข้าไปเป็น standard input คุณต้อง escape เครื่องหมายเปอร์เซ็นต์ทุกตัวด้วย backslash
0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/siteมีสองเลเยอร์ที่อ่านบรรทัดนั้นตามลำดับ \% เป็นกฎของ cron ซึ่ง cron จะนำไปใช้ก่อนเริ่มทำงานใดๆ ส่วน $(date +\%F) คือ command substitution ซึ่ง shell ที่ cron เรียกขึ้นมาจะนำไปใช้ในภายหลัง การเข้าใจว่าเลเยอร์ใดเป็นเจ้าของตัวอักษรตัวไหนคือหัวใจสำคัญของเรื่องนี้
นิสัยที่ปลอดภัยกว่าคือการเก็บตรรกะการทำงานไว้นอก crontab ทั้งหมด ให้ใส่ไว้ในสคริปต์แทน ซึ่งเครื่องหมายเปอร์เซ็นต์จะไม่มีความหมายพิเศษใดๆ
#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/siteบรรทัดใน crontab จะเหลือเพียงพาธและการ redirect เท่านั้น crontab ที่คุณสามารถอ่านเข้าใจได้ทันทีที่มอง คือ crontab ที่คุณสามารถแก้ไขปัญหาได้ง่าย
สาเหตุที่ 3: คุณแก้ไข crontab ผิดไฟล์หรือไม่?
ไม่มี crontab เพียงไฟล์เดียวในระบบ แต่มีไฟล์หลายชุดซึ่งมีเจ้าของและจำนวนฟิลด์ที่แตกต่างกัน หากเขียนคำสั่งลงในไฟล์ที่ไม่ถูกต้อง งานนั้นจะไม่ทำงาน
crontab -eเป็นการแก้ไข crontab ของผู้ใช้ที่เรียกใช้คำสั่ง ส่วนsudo crontab -eเป็นการแก้ไขของ root บ่อยครั้งที่ผู้ดูแลระบบสองคนตรวจสอบเครื่องเดียวกันแต่กลับอ่านไฟล์คนละไฟล์sudo crontab -l -u deployใช้สำหรับแสดงรายการ crontab ของผู้ใช้อื่น ซึ่งเป็นวิธีตรวจสอบว่ามีการติดตั้งคำสั่งใดไว้จริงสำหรับบัญชีที่ควรจะเป็นผู้รันงานนั้น/etc/crontabและทุกไฟล์ใน/etc/cron.dจะมีฟิลด์เพิ่มขึ้นมาหนึ่งฟิลด์ระหว่างกำหนดการและคำสั่ง นั่นคือชื่อผู้ใช้ที่จะให้รันงาน หากคุณนำบรรทัด crontab แบบ 5 ฟิลด์ไปวางใน/etc/cron.dคำแรกของคำสั่งจะถูกอ่านเป็นชื่อผู้ใช้แทน- ไฟล์ใน
/etc/cron.dต้องตั้งชื่อด้วยตัวอักษร ตัวเลข ขีดล่าง และขีดกลางเท่านั้น ไฟล์ที่ชื่อbackup.shหรือsite.confจะถูกข้ามไปเนื่องจากชื่อไฟล์ไม่เป็นไปตามเงื่อนไข ให้เปลี่ยนชื่อเป็นbackupแล้วตรวจสอบ log อีกครั้ง - ไฟล์ใน
/etc/cron.dควรมี root เป็นเจ้าของ และต้องไม่เปิดสิทธิ์ให้ group หรือ others เขียนไฟล์ได้ls -l /etc/cron.dจะแสดงให้คุณเห็นทั้งสองสถานะนี้พร้อมกัน - สคริปต์ที่วางใน
/etc/cron.dailyและโฟลเดอร์ย่อยต้องปฏิบัติตามกฎการตั้งชื่อเดียวกันและต้องมีสิทธิ์ execute หากไม่มีสิทธิ์ execute ระบบจะข้ามสคริปต์นั้นไปโดยไม่แจ้งเตือน /etc/cron.allowและ/etc/cron.denyเป็นตัวกำหนดว่าใครสามารถติดตั้ง crontab ได้บ้าง หากไฟล์เหล่านี้มีอยู่ในเครื่องของคุณ ให้อ่านเนื้อหาภายในก่อนที่จะสรุปว่าผู้ใช้ของคุณได้รับอนุญาตให้ใช้งาน
ให้ติดตั้ง user crontab ด้วยคำสั่ง crontab แทนการแก้ไขไฟล์ใน spool โดยตรง เพราะ crontab จะตรวจสอบไวยากรณ์ของไฟล์ก่อนทำการติดตั้ง เมื่อบันทึกไฟล์แล้วให้อ่านสิ่งที่คำสั่งแสดงผลกลับมา หากคำสั่งปฏิเสธไฟล์ เวอร์ชันเดิมจะยังคงทำงานอยู่และการเปลี่ยนแปลงของคุณจะไม่มีผล ซึ่งดูเหมือนกับว่า cron เพิกเฉยต่อคำสั่งของคุณ
เจ้าของไฟล์ยังเป็นตัวกำหนดสิทธิ์การเข้าถึงด้วย งานที่อยู่ใน crontab ของ root จะสร้างไฟล์ที่มี root เป็นเจ้าของ ซึ่งแอปพลิเคชันที่ต้องอ่านไฟล์เหล่านั้นอาจไม่สามารถเขียนทับได้ งานที่อยู่ใน crontab ของผู้ใช้ทั่วไปจะไม่สามารถอ่านไดเรกทอรีที่จำกัดสิทธิ์เฉพาะ root ได้ ดังนั้นควรเลือกเจ้าของงานให้เหมาะสมกับหน้าที่: การดูแลรักษาแอปพลิเคชันควรทำโดยบัญชีของแอปพลิเคชันนั้นเอง ซึ่งเป็นเหตุผลเบื้องหลังของ การแทนที่ wp-cron ของ WordPress ด้วย system cron job โหมดของไฟล์ที่งานของคุณสร้างขึ้นจะมาจาก umask ที่ได้รับสืบทอดมา ซึ่งเป็นค่าที่อาจไม่ตรงกับค่าใน shell ของคุณ ดังนั้น วิธีการที่ umask กำหนดสิทธิ์ไฟล์ จึงเป็นหัวข้อที่ควรอ่านหากผลลัพธ์ของงานที่ได้ไม่สามารถเปิดอ่านได้
สาเหตุที่ 4: ผลลัพธ์ถูกส่งไปยังอีเมลที่ไม่มีใครอ่าน
cron จะรวบรวมทุกอย่างที่งานเขียนลงใน standard output และ standard error หากงานมีการเขียนข้อความใดๆ ออกมา cron จะส่งข้อความนั้นไปยังระบบอีเมลภายในเครื่อง โดยระบุชื่อผู้รับเป็นเจ้าของ crontab หรือตามที่ MAILTO กำหนดไว้ ใน VPS รุ่นที่ตัดฟีเจอร์ออกมักจะไม่มี MTA (mail transfer agent) ติดตั้งอยู่ ทำให้ไม่มีการส่งอีเมลเกิดขึ้น ข้อผิดพลาดของคุณจึงปรากฏขึ้นเพียงชั่วครู่แล้วหายไป นี่คือเหตุผลทั้งหมดที่ทำให้งานที่ล้มเหลวดูเหมือนเงียบสนิท
ให้ส่งผลลัพธ์ไปยังไฟล์ที่คุณควบคุมแทน
0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1>> จะทำการต่อท้าย (append) standard output ลงในไฟล์ 2>&1 จะชี้ standard error ไปยังปลายทางเดียวกับที่ standard output ชี้อยู่ ณ ขณะนั้น ดังนั้นคำสั่งนี้จึงต้องวางไว้หลังการ redirect หากเขียนสลับกันเป็น 2>&1 >> file ตัว standard error จะยังคงส่งไปยังปลายทางเดิม และข้อผิดพลาดที่คุณกำลังตามหาจะเป็นส่วนที่ไม่เคยถูกบันทึกลงในไฟล์เลย
journal เป็นอีกหนึ่งปลายทางที่ดี logger จะเขียนข้อมูลลงใน syslog ภายใต้แท็กที่คุณเลือก
0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-siteอ่านข้อมูลกลับมาด้วย journalctl -t backup-site วิธีนี้ช่วยให้ผลลัพธ์ของงานอยู่คู่กับรายการ cron ทำให้ติดตามลำดับเวลาได้ง่าย หากคุณต้องการบันทึกด้วยว่าใครเป็นผู้รันคำสั่งใดบนเซิร์ฟเวอร์ นั่นเป็นระบบแยกต่างหาก ซึ่งครอบคลุมอยู่ใน การตรวจสอบคำสั่งของผู้ใช้บนเซิร์ฟเวอร์ของคุณ
MAILTO="" ที่ส่วนบนของ crontab จะเป็นการปิดการส่งอีเมลสำหรับงานที่อยู่ด้านล่างทั้งหมด การตั้งค่า MAILTO ให้เป็นที่อยู่อีเมลจริงจะช่วยได้ก็ต่อเมื่อมี MTA ที่ใช้งานได้จริงเท่านั้น ดังนั้นควรตรวจสอบให้แน่ใจว่าอีเมลสามารถส่งออกจากเครื่องได้ก่อนที่จะพึ่งพาวิธีนี้
กฎข้อหนึ่งระหว่างการดีบั๊ก: ห้ามต่อท้ายด้วย > /dev/null 2>&1 เด็ดขาด นี่เป็นบรรทัดที่นิยมใช้มากที่สุดในทุก crontab แต่การทำเช่นนั้นจะเป็นการทิ้งหลักฐานเดียวที่คุณมีไป หากต้องการใช้ ให้ใส่กลับเข้าไปภายหลังเมื่อตรวจสอบแล้วว่างานทำงานได้ตามปกติ
สาเหตุที่ 5: สคริปต์คาดหวังสภาพแวดล้อมที่ cron ไม่ได้จัดเตรียมให้
เมื่อพบคำสั่งและบันทึกผลลัพธ์ได้แล้ว สิ่งที่เหลืออยู่คือทุกอย่างที่เซสชันปกติของคุณมีให้โดยอัตโนมัติ
- Shell อาจไม่ใช่ bash ให้ตรวจสอบด้วย
ls -l /bin/shบน Debian และ Ubuntu ค่านี้จะชี้ไปที่ dash ดังนั้นการใช้ double bracket test, อาร์เรย์ และsourceจะล้มเหลวด้วยข้อผิดพลาดทางไวยากรณ์ ให้ระบุบรรทัด#!/bin/bashไว้ที่ต้นสคริปต์แล้วเรียกใช้งาน หรือตั้งค่าSHELLไว้ที่ด้านบนของ crontab - ไดเรกทอรีทำงาน (working directory) ไม่ใช่ไดเรกทอรีที่คุณใช้งานอยู่ ให้ใช้ absolute path เสมอ หรือใช้
cdไปยังไดเรกทอรีที่ต้องการในบรรทัดแรกของสคริปต์ การใช้ relative path เป็นสาเหตุที่พบบ่อยที่สุดที่ทำให้งาน "ทำงานได้เมื่อรันด้วยมือ" - Locale ไม่ใช่ค่าเดียวกับเซสชันของคุณ คำสั่งใดก็ตามที่จัดรูปแบบวันที่ ตัวเลข หรือเรียงลำดับข้อความ อาจให้ผลลัพธ์ที่ต่างออกไปภายใต้
LANGที่ต่างกัน หากขั้นตอนถัดไปต้องประมวลผลผลลัพธ์นั้น ให้ตั้งค่า locale ไว้ในสคริปต์โดยตรงแทนการคาดหวังค่าเริ่มต้น - ไม่มี TTY (terminal) คำสั่งที่ต้องการการยืนยัน เปิดโปรแกรมแก้ไขข้อความ หรือแสดงแถบความคืบหน้าอาจค้างหรือหยุดทำงาน ให้เพิ่ม flag สำหรับการทำงานแบบ non-interactive ที่เครื่องมือนั้นรองรับ
- ไม่มี SSH agent เนื่องจาก
SSH_AUTH_SOCKไม่อยู่ในสภาพแวดล้อมของ cron คำสั่งsshหรือrsyncที่เคยทำงานได้เพราะโหลด agent ไว้แล้วจะล้มเหลวในการยืนยันตัวตน ให้สร้างกุญแจ (key) แยกต่างหากสำหรับงานนั้นๆ โดยกำหนดสิทธิ์ให้เป็นเจ้าของโดยผู้ใช้ที่รันงานดังกล่าว - ไม่มี user session bus ดังนั้นการใช้
systemctl --userจาก cron job จะล้มเหลวหากไม่ได้ตั้งค่าXDG_RUNTIME_DIRการใช้ system unit เป็นทางเลือกที่ดีกว่า
บน Fedora, Rocky และ Alma ยังมีอีกหนึ่งสาเหตุที่น่าสงสัย คือ SELinux ซึ่งจำกัดการทำงานของ cron job ดังนั้นงานที่เข้าถึง path ที่มี label ไม่ถูกต้องจะถูกปฏิเสธ แม้ว่าสิทธิ์ของไฟล์จะดูถูกต้องก็ตาม ให้ตรวจสอบการปฏิเสธการเข้าถึงด้วย sudo ausearch -m avc -ts recent และอ่าน พื้นฐาน SELinux สำหรับเซิร์ฟเวอร์ ก่อนที่จะปิดการทำงานใดๆ
การตรวจสอบสภาพแวดล้อมของ cron ในหนึ่งนาที
เลิกคาดเดาสภาพแวดล้อมของ cron แล้วตรวจสอบด้วยตัวเอง เขียนสคริปต์เพื่อดัมพ์ข้อมูลทั้งหมด ตั้งเวลาให้ทำงานทุกนาที รอสักครู่ แล้วจึงอ่านไฟล์ที่ได้
cat > /home/deploy/cron-probe.sh <<'EOF'
#!/bin/bash
echo "=== probe ==="
date -Is
pwd
id
echo "SHELL=$SHELL"
echo "LANG=$LANG"
command -v node || echo "node is not on this PATH"
env | sort
EOF
chmod +x /home/deploy/cron-probe.shเพิ่มบรรทัดลงใน crontab ของผู้ใช้ที่รันงานจริง โดยระบุพาธแบบ absolute ทั้งสองฝั่ง
* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&1รอหนึ่งนาที จากนั้นอ่านไฟล์ /home/deploy/cron-probe.log และเปรียบเทียบกับผลลัพธ์ของคำสั่งเดียวกันที่รันในเชลล์ของคุณ บรรทัด PATH, ไดเรกทอรีที่ทำงานอยู่ (working directory) และ locale มักจะอธิบายสาเหตุของความล้มเหลวได้ด้วยตัวมันเอง โปรดสังเกตรายละเอียดสองประการของการตั้งค่านี้: เครื่องหมายเปอร์เซ็นต์ที่อยู่ภายในสคริปต์จะไม่ถูกควบคุมโดยกฎของ cron และพาธของไฟล์ log ต้องเป็นตำแหน่งที่ผู้ใช้ของงานนั้นมีสิทธิ์เขียนได้
ลบบรรทัดใน crontab นั้นทันทีที่คุณได้คำตอบ งานที่รันทุกนาทีและเขียนข้อมูลต่อท้ายไฟล์จะทำให้ดิสก์ขนาดเล็กเต็มได้อย่างรวดเร็วและเงียบเชียบ
กำหนดการที่คุณตั้งไว้ถูกต้องหรือไม่?
บรรทัดใน crontab ของผู้ใช้ประกอบด้วย 5 ฟิลด์ ได้แก่ นาที, ชั่วโมง, วันที่ในเดือน, เดือน และวันในสัปดาห์ มีสองฟิลด์ที่ทำงานสัมพันธ์กันจนอาจทำให้ผู้ใช้ประหลาดใจ
เมื่อมีการจำกัดทั้งวันที่ในเดือนและวันในสัปดาห์ (หมายความว่าไม่มีฟิลด์ใดเป็น *) cron จะรันงานเมื่อฟิลด์ใดฟิลด์หนึ่งตรงกับเงื่อนไข 0 0 13 * 5 ไม่ได้หมายถึง "วันศุกร์ที่ 13" แต่มันจะรันงานตอนเที่ยงคืนของวันที่ 13 ในทุกเดือน และรันตอนเที่ยงคืนของทุกวันศุกร์ หากต้องการระบุวันใดวันหนึ่งโดยเฉพาะ ให้ปล่อยฟิลด์ใดฟิลด์หนึ่งเป็น * แล้วใช้เงื่อนไขตรวจสอบอีกฟิลด์หนึ่งภายในสคริปต์แทน
cron ใช้เขตเวลาของระบบ VPS หลายแห่งตั้งค่าเริ่มต้นเป็น UTC (Coordinated Universal Time) ดังนั้นงานที่คุณตั้งไว้ตอน 03:00 จะรันที่เวลา 03:00 UTC ซึ่งอาจตรงกับช่วงบ่ายในเวลาท้องถิ่นของคุณ timedatectl จะแสดงเขตเวลาที่เซิร์ฟเวอร์ของคุณใช้งานจริง ให้ตรวจสอบค่านี้แทนการคาดเดาว่าระบบจะใช้เขตเวลาเดียวกับคอมพิวเตอร์ของคุณ
ยังมีกับดักเรื่องกำหนดการอีกสองประการที่ควรทราบ @reboot จะทำงานเมื่อ cron เริ่มต้นขึ้น ซึ่งไม่ใช่จังหวะเดียวกับที่เครือข่ายพร้อมใช้งาน ดังนั้นงานที่ต้องใช้ DNS หรือโฮสต์ระยะไกลอาจล้มเหลวในช่วงบูตเครื่อง แต่กลับทำงานได้สำเร็จเมื่อรันด้วยตนเองในภายหลัง นอกจากนี้ ไม่มีกลไกใดป้องกันไม่ให้งานที่ทำงานช้าเริ่มรันซ้ำในขณะที่สำเนาเดิมยังทำงานอยู่ คุณควรครอบงานนั้นด้วย lock
*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1flock -n จะยุติการทำงานทันทีหากพบว่ามีการถือ lock อยู่แล้ว เพื่อป้องกันไม่ให้งานที่ซ้อนทับกันทำงานพร้อมกันจนเกิดปัญหา
เมื่อ systemd timer เป็นเครื่องมือที่เหมาะสมกว่า
cron ทำงานได้ดีในเรื่องเดียวคือการสั่งรันคำสั่งตามเวลาที่กำหนด แต่ในด้านอื่นนั้นถือว่ามีข้อจำกัดมาก การใช้ timer ช่วยให้คุณสามารถดู log ผ่าน journal ได้โดยไม่ต้องทำ redirect, สามารถตรวจสอบ exit status ย้อนหลังได้, กำหนดลำดับการทำงานเทียบกับ network-online.target ได้ และยังสามารถตั้งค่าการหน่วงเวลาแบบสุ่มเพื่อป้องกันไม่ให้เซิร์ฟเวอร์จำนวนมากเริ่มทำงานพร้อมกันในวินาทีเดียวกัน เมื่อใดที่งานของคุณต้องการคุณสมบัติเหล่านี้ การใช้ systemd service และ timer บน VPS จะช่วยลดภาระงานได้มากกว่าการจัดการบรรทัดคำสั่งใน crontab นอกจากนี้ พฤติกรรมการลองใหม่ (retry) ก็ควรจัดการที่นี่เช่นกัน เพราะ นโยบายการรีสตาร์ทของ systemd จะเป็นตัวกำหนดสิ่งที่เกิดขึ้นหลังจากเกิดความล้มเหลว ในขณะที่ cron ไม่มีกลไกจัดการในส่วนนี้เลย
ให้ใช้ cron สำหรับงานขนาดเล็กทั่วไป ส่วนงานใดที่มีการพึ่งพาบริการอื่นหรือต้องการนโยบายการลองใหม่ ให้ย้ายไปใช้ timer ทั้งสองระบบสามารถทำงานบนเซิร์ฟเวอร์เดียวกันได้ ดังนั้นคุณไม่จำเป็นต้องย้ายงานทั้งหมดให้เสร็จสิ้นในคราวเดียว
FAQ
ทำไม cron job ของฉันทำงานได้เมื่อสั่งด้วยมือ แต่กลับล้มเหลวเมื่อรันผ่าน cron?
เพราะ shell ของคุณและสภาพแวดล้อมของ cron นั้นแตกต่างกัน shell ที่คุณล็อกอินจะอ่าน /etc/profile และ ~/.bashrc ซึ่งเป็นตัวกำหนด PATH, locale และตัวแปร agent ของคุณ แต่ cron จะเริ่มคำสั่งโดยไม่มีสิ่งเหล่านี้ และรันจากไดเรกทอรีทำงานที่ต่างออกไป บางครั้งยังใช้ shell ที่ต่างกันด้วย ให้ใช้ absolute path สำหรับทุกคำสั่ง กำหนดสิ่งที่จำเป็นไว้ที่ส่วนบนของ crontab หรือภายในสคริปต์ และตั้งเวลาให้ job ตรวจสอบทำงานทุกหนึ่งนาทีเพื่อรัน env | sort, pwd และ id ลงในไฟล์ log เพื่อให้คุณสามารถอ่านสภาพแวดล้อมจริงของ cron แทนการคาดเดา
ฉันจะตรวจสอบได้อย่างไรว่า cron ได้รัน job ของฉันจริงหรือไม่?
ให้อ่าน log ของ daemon ให้ใช้ journalctl -u cron บน Debian และ Ubuntu หรือ journalctl -u crond บน Fedora, Rocky และ Alma; บาง image จะส่งข้อความผ่าน rsyslog ไปยังไฟล์ภายใต้ /var/log แทน ให้มองหารายการที่ตรงกับนาทีที่คุณตั้งเวลาไว้และตรวจสอบว่ามีชื่อคำสั่งของคุณอยู่ หากไม่มีรายการแสดงว่า cron ไม่ได้รับตารางเวลานั้น ให้ตรวจสอบว่าคุณแก้ไข crontab ถูกไฟล์หรือไม่ หากมีรายการแต่ไม่มีผลลัพธ์ แสดงว่าคำสั่งเริ่มทำงานแล้วหยุดไป ให้ดักจับ output ด้วยการ redirect
ทำไม date +%Y ถึงทำงานผิดพลาดภายใน crontab?
cron ถือว่า % เป็นอักขระพิเศษในช่องคำสั่ง โดย % ตัวแรกที่ไม่ได้ถูก escape จะเป็นการสิ้นสุดคำสั่ง ทุกสิ่งที่ตามหลังจะถูกส่งไปยังคำสั่งนั้นในฐานะ standard input และ % แต่ละตัวที่ตามมาจะกลายเป็นบรรทัดใหม่ ดังนั้นชื่อไฟล์ที่จัดรูปแบบด้วย date จึงไม่ถูกส่งไปยังโปรแกรมที่คุณเขียนไว้ ให้ escape เครื่องหมายเปอร์เซ็นต์แต่ละตัวด้วย \% หรือย้ายคำสั่งไปไว้ในสคริปต์แล้วเรียกสคริปต์จาก cron แทน เพราะภายในสคริปต์เครื่องหมายเปอร์เซ็นต์ไม่มีความหมายพิเศษ
output ของ cron job ของฉันหายไปไหน?
มันจะถูกส่งไปยังระบบเมลภายใน โดยจ่าหน้าถึงเจ้าของ crontab หรือสิ่งที่ MAILTO กำหนดไว้ VPS ส่วนใหญ่ไม่ได้ติดตั้ง mail transfer agent ไว้ ข้อความจึงถูกทิ้งและ job ดูเหมือนเงียบไป ให้ redirect output ไปยังไฟล์ด้วย >> /path/to/log 2>&1 โดยเรียงลำดับเช่นนั้นเพื่อให้ standard error ตามหลัง standard output หรือส่งผ่าน logger -t myjob แล้วอ่านกลับด้วย journalctl -t myjob อย่าใช้ > /dev/null 2>&1 ในขณะที่คุณยังอยู่ในขั้นตอนการดีบั๊ก
ฉันควรใช้ cron หรือ systemd timer?
ให้ใช้ cron สำหรับคำสั่งง่ายๆ ที่รันตามเวลาที่กำหนด โดยเฉพาะงานที่คุณอาจต้องย้ายไปยังเครื่องที่ไม่ได้รัน systemd ให้ใช้ timer เมื่อคุณต้องการให้ output ไปอยู่ใน journal โดยไม่ต้อง redirect, ต้องการสถานะ exit status ที่สอบถามได้, ต้องการให้รันหลังจาก network พร้อมใช้งาน, ต้องการหน่วงเวลาเริ่มแบบสุ่ม หรือต้องการนโยบายการลองใหม่หลังจากล้มเหลว ทั้งสองอย่างสามารถรันบนเซิร์ฟเวอร์เดียวกันได้ คุณจึงสามารถทยอยย้ายงานทีละรายการได้ตามความเหมาะสม