celery4+django2定時任務

apocelipes發表於2018-12-21

網上有很多celery + django實現定時任務的教程,不過它們大多數是基於djcelery + celery3的;
或者是使用django_celery_beat配置較為繁瑣的。

顯然簡潔而高效才是我們最終的追求,而celery4已經不需要額外外掛即可與django結合實現定時任務了,原生的celery beat就可以很好的實現定時任務功能。

當然使用原生方案的同時有幾點外掛所帶來的好處被我們放棄了:

  • 外掛提供的定時任務管理將不在可用,當我們只需要任務定期執行而不需要人為排程的時候這點忽略不計。
  • 無法高效的管理或追蹤定時任務,定時任務的跟蹤其實交給日誌更合理,但是對任務的修改就沒有那麼方便了,不過如果不需要經常變更/增減任務的話這點也在可接受範圍內。

Celery定時任務配置

在進行配置前先來看看專案結構:

.
├── linux_news
│   ├── celery.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── news
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   ├── models
│   ├── tasks.py
│   ├── tests.py
│   └── views
└── start-celery.sh

其中news是我們的app,用於從一些rss訂閱源獲取新聞資訊,linux_news則是我們的project。我們需要關心的主要是celery.pysettings.pytasks.pystart-celery.sh

首先是celery.py,想讓celery執行任務就必須例項化一個celery app,並把settings.py裡的配置傳入app:

import os
from celery import Celery

# set the default Django settings module for the `celery` program.
os.environ.setdefault(`DJANGO_SETTINGS_MODULE`, `linux_news.settings`)

app = Celery(`linux_news`)

# `django.conf:settings`表示django,conf.settings也就是django專案的配置,celery會根據前面設定的環境變數自動查詢並匯入
# - namespace表示在settings.py中celery配置項的名字的統一字首,這裡是`CELERY_`,配置項的名字也需要大寫
app.config_from_object(`django.conf:settings`, namespace=`CELERY`)

# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

配置就是這麼簡單,為了能在django裡使用這個app,我們需要在__init__.py中匯入它:

from .celery import app as celery_app

然後我們來看tasks.py,它應該位於你的app目錄中,前面我們配置了自動發現,所以celery會自動找到這些tasks,我們的tasks將寫在這一模組中,程式碼涉及了一些orm的使用,為了契合主題我做了些精簡:

from linux_news.celery import celery_app as app
from .models import *
import time
import feedparser
import pytz
import html


@app.task(ignore_result=True)
def fetch_news(origin_name):
    """
    fetch all news from origin_name
    """
    origin = get_feeds_origin(origin_name)
    feeds = feedparser.parse(origin.feed_link)
    for item in feeds[`entries`]:
        entry = NewsEntry()
        entry.title = item.title
        entry.origin = origin
        entry.author = item.author
        entry.link = item.link
        # add timezone
        entry.publish_time = item.time.replace(tzinfo=pytz.utc)
        entry.summary = html.escape(item.summary)

        entry.save()


@app.task(ignore_result=True)
def fetch_all_news():
    """
    這是我們的定時任務
    fetch all origins` news to db
    """
    origins = NewsOrigin.objects.all()
    for origin in origins:
        fetch_news.delay(origin.origin_name)

tasks裡是一些耗時操作,比如網路IO或者資料庫讀寫,因為我們不關心任務的返回值,所以使用@app.task(ignore_result=True)將其遮蔽了。

任務配置完成後我們就要配置celery了,我們選擇redis作為任務佇列,我強烈建議在生產環境中使用rabbitmq或者redis作為任務佇列或結果快取後端,而不應該使用關係型資料庫:

# redis
REDIS_PORT = 6379
REDIS_DB = 0
# 從環境變數中取得redis伺服器地址
REDIS_HOST = os.environ.get(`REDIS_ADDR`, `redis`)

# celery settings
# 這兩項必須設定,否則不能正常啟動celery beat
CELERY_ENABLE_UTC = True
CELERY_TIMEZONE = TIME_ZONE
# 任務佇列配置
CELERY_BROKER_URL = f`redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}`
CELERY_ACCEPT_CONTENT = [`application/json`, ]
CELERY_RESULT_BACKEND = f`redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}`
CELERY_TASK_SERIALIZER = `json`

然後是我們的定時任務設定:

from celery.schedules import crontab
CELERY_BEAT_SCHEDULE={
        `fetch_news_every-1-hour`: {
            `task`: `news.tasks.fetch_all_news`,
            `schedule`: crontab(minute=0, hour=`*/1`),
        }
}

定時任務配置物件是一個dict,由任務名和配置項組成,主要配置想如下:

  • task:任務函式所在的模組,模組路徑得寫全,否則找不到將無法執行該任務
  • schedule:定時策略,一般使用celery.schedules.crontab,上面例子為每小時的0分執行一次任務,具體寫法與linux的crontab類似可以參考文件說明
  • args:是個元組,給出任務需要的引數,如果不需要引數也可以不寫進配置,就像例子中的一樣
  • 其餘配置項較少用,可以參考文件
    至此,配置celery beat的部分就結束了。

啟動celery beat

配置完成後只需要啟動celery了。

啟動之前配置一下環境。不要用root執行celery!不要用root執行celery!不要用root執行celery!重要的事情說三遍。

start-celery.sh:

export REDIS_ADDR=127.0.0.1

celery -A linux_news worker -l info -B -f /path/to/log

-A 表示app所在的目錄,-B表示啟動celery beat執行定時任務。
celery正常啟動後就可以通過日誌來檢視任務是否正常執行了:

[2018-12-21 13:00:00,022: INFO/MainProcess] Received task: news.tasks.fetch_all_news[e4566ede-2cfa-4c19-b2f3-0c7d6c38690d]  
[2018-12-21 13:00:00,046: INFO/MainProcess] Received task: news.tasks.fetch_news[583e96dc-f508-49fa-a24a-331e0c07a86b]  
[2018-12-21 13:00:00,051: INFO/ForkPoolWorker-2] Task news.tasks.fetch_all_news[e4566ede-2cfa-4c19-b2f3-0c7d6c38690d] succeeded in 0.02503809699555859s: None
[2018-12-21 13:00:00,052: INFO/MainProcess] Received task: news.tasks.fetch_news[c61a3e55-dd3c-4d49-8d6d-ca9b1757db25]  
[2018-12-21 13:00:00,449: INFO/ForkPoolWorker-5] Task news.tasks.fetch_news[c61a3e55-dd3c-4d49-8d6d-ca9b1757db25] succeeded in 0.39487219898728654s: None
[2018-12-21 13:00:00,606: INFO/ForkPoolWorker-3] Task news.tasks.fetch_news[583e96dc-f508-49fa-a24a-331e0c07a86b] succeeded in 0.5523456179944333s: None

以上就是celery4執行定時任務的內容,如有錯誤和疏漏,歡迎指正。