有時候我們需要server定時做某些東西,例如每天凌晨計算一天的營業額。
一般Web Server只會在收到請求時作出相應行動,所以最簡單做法,就是開一個Admin endpoint,人手「激活」server去做某些事。
這當然很蠢啊。
這時候我們可以用Linux上的crontab
去讓他「自動」、「定時」執行某些工作。
Cron
Cron常見於Unix和Unix-like系統(下略稱Linux),名字來源於希臘語chronos(χρόνος),原意是時間。一般透過crontab
指令操作。
Linux會在背景長期執行cron daemon
,不斷檢查當前有沒有需要執行的Cron job。
所以我們要建立工作排程,需要告訴cron daemon
甚麼時候/頻率,需要做哪些事情。
使用crontab
修改crontab檔案
crontab -e # e代表edit
基本格式
min hr day month weekday command
透過例子學習
*/25 20,21 1-10 12 1-6 /bin/sh backup.sh
以上cron job代表:
每逢12月的1-10號(同時當天不是星期日)的20:00, 20:25, 20:50, 21:00, 21:25, 21:50執行/bin/sh backup.sh
。
Crontab格式教學
http://crontab.guru/
https://tecadmin.net/crontab-in-linux-with-20-examples-of-cron-schedule/
Special case
列出當前Cron jobs
crontab -l # l代表list
# 會列出已注冊的cron jobs
時區
時區是crontab的天敵。很多時候我們用的linux機都是雲端機,例如在AWS、GCP、Azure等等地方租用linux,全都是海外地方,時區不同,所以crontab裡的17:00,可能是亞洲的05:00,要多加留意,尤其cron job很難test。
更改時區
例如改成香港時區:
sudo ln -sf /usr/share/zoneinfo/Asia/Hong_Kong /etc/localtime # ln = link, -s = symbolic, -f = --force
sudo service cron restart
其他時區都可以在/usr/share/zoneinfo/
找到。
Reference: how to use TimeZone with Cron tab
建立Django cron job
Naive做法
以上例子只能執行shell script,要開啟django執行工作,可以:
# run_django.sh
cd ~/my_project/
source env/bin/activate/
./manage.py shell -c "from some_app.models import Food; blahblahblah" # -c代表command
# crontab
*/10 * * * * /bin/sh /path/to/run_django.sh
這樣也是太蠢了。
Open source tool
經過一些research,有以下三個Django插件做到這功能:
- Celery: Distributed Task Queue:殺雞焉用牛刀?
- Tivix/django-cron:Setup有點麻煩,多了一層多餘的Abstraction
- kraiz/django-crontab:Dead simple
最終我選擇了django-crontab,快樂使用中。
原理解釋
詳細用法就參照Doc吧,這裡只介紹一下原理。
在settings.py
設置CRONJOBS:
CRONJOBS = [
('*/15 18-23 * * *', 'some_app.cron.some_cron_job', '>> /var/log/cron_job.log 2>&1'),
]
加到crontab:
./manage.py crontab add
然後打開crontab -e
,可以看到最底有一行:
*/15 18-23 * * * /home/ubuntu/my_project/env/bin/python /home/ubuntu/my_project/manage.py crontab run xxxxxxxxxxxxx # django-cronjobs for my_project
- 這代表django-crontab其實是幫你注冊了一個cron job到linux的crontab
/home/ubuntu/my_project/env/bin/python
代表使用你的virtualenv(虛擬環境)中的python去執行manage.py
manage.py crontab run xxxxxxxxxxx
執行django-crontab裡的run
- 我看了看源碼,django-crontab會把cron job的設置hash成一條字串(就是那個
xxxxx
),run
的時候會以那個hash,在settings.py
找回要執行的cron job,最後執行它
延伸閱讀:建置python虛擬環境(Virtual Environment)
Cron job logging注意事項
要小心輸出到stdout和stderr的分別。
如果你的Django設置了StreamHandler
處理logging,要確保cron job的suffix(後綴)是例如以下:
('*/15 18-23 * * *', 'some_app.cron.some_cron_job', '>> /var/log/cron_job.log 2>&1')
(>
代表覆蓋原有檔案,>>
代表append到原有檔案)
>>
或1>>
都代表Redirect stdout,但StreamHandler
預設輸出到stderr,所以緊記加上2>&1
,把stderr也輸出到同一地方,這樣才會看到cron job的logging。
詳細可參照我在django-crontab開的issue。
更多關於redirect script
Related: Default file decriptor 1, 2
If stream is specified, the instance will use it for logging output; otherwise, sys.stderr will be used.
How can I redirect and append both stdout and stderr to a file with Bash?