在Django建立定時執行的Cron jobs(工作排程)

2018/06/08 posted in  Django comments

有時候我們需要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插件做到這功能:

最終我選擇了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注意事項

要小心輸出到stdoutstderr的分別。

如果你的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

延伸閱讀:在Django使用Logging製作紀錄檔

更多關於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?