為了在后臺運行任務,我們可以使用線程(或者進程)。
使用線程(或者進程)的好處是保持處理邏輯簡潔。但是,在需要可擴展的生產環境中,我們也可以考慮使用Celery代替線程。
Celery是什么?
Celery是個異步分布式任務隊列。
通過Celery在后臺跑任務并不像用線程那么的簡單,但是用Celery的話,能夠使應用有較好的可擴展性,因為Celery是個分布式架構。下面介紹Celery的三個核心組件。
生產者(Celery client)。生產者(Celery client)發送消息。在Flask上工作時,生產者(Celery client)在Flask應用內運行。
消費者(Celery workers)。消費者用于處理后臺任務。消費者(Celery client)可以是本地的也可以是遠程的。我們可以在運行Flask的server上運行一個單一的消費者(Celery workers),當業務量上漲之后再去添加更多消費者(Celery workers)。
消息傳遞者(message broker)。生產者(Celery client)和消費者(Celery workers)的信息的交互使用的是消息隊列(message queue)。Celery支持若干方式的消息隊列,其中最常用的是RabbitMQ和Redis。
話不多說上代碼先。代碼中包含兩個例子:異步發送郵件;開始一或多個異步工作,然后在網頁上更新其進度。
from flask import Flask
from celery import Celery
app = Flask(__name__)
app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0'
app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:6379/0'
celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])
celery.conf.update(app.config)
其中的URL參數告訴了Celery,消息傳遞的服務的位置。如果消息傳遞者用的不是Redis,或者Redis部署在其他機器,那么需要做適當的改變。
而通過調用 celery.conf.update()
方法,我們能夠為Celery同步Flask上的配置。僅當需要Celery存儲狀態即存儲結果時,CELERY_RESULT_BACKEND
選項才會被用到。
下文第一個例子不需要存儲狀態以及存儲結果,但是第二個例子是需要的,所以一次配置好。
任何想要在后臺運行的任務,都需要使用裝飾者celery.task
進行包裝,如下。
@celery.task
def my_background_task(arg1, arg2):
# some long running task here
return result
現在Flask 應用就能夠發起“在后臺執行任務”的請求了,如下。
task = my_background_task.delay(10, 20)
其中delay()
方法是 apply_async()
的快捷調用。
此處用apply_async()
同樣奏效,如下。
task = my_background_task.apply_async(args=[10, 20])
相比于 delay()
方法,當使用 apply_async()
方法時,我們能夠對后臺任務的執行方式有更多的控制。例如任務在何時執行等。
舉例來說,下面的代碼可以讓任務在一分鐘之后開始運行。
task = my_background_task.apply_async(args=[10, 20], countdown=60)
delay()
和 apply_async()
的返回值是一個 AsyncResult
的對象。通過該對象,能夠獲得任務的狀態。
例一:異步發郵件
第一個例子的需求比較廣泛:發電子郵件的時候無需阻塞主應用線程。本例使用了擴展Flask-Mail。
網頁包含了一個Text類型的域的表單。用戶需要在其中輸入郵箱地址,點擊提交,然后服務器向該地址發送一封測試郵件。該表單包含兩個提交按鈕,其中一個會立即發送郵件,而另一個會在點擊后延遲一分鐘后再發送。html代碼如下。
<html>
<head>
<title>Flask + Celery Examples</title>
</head>
<body>
<h1>Flask + Celery Examples</h1>
<h2>Example 1: Send Asynchronous Email</h2>
{% for message in get_flashed_messages() %}
<p style="color: red;">{{ message }}</p>
{% endfor %}
<form method="POST">
<p>Send test email to: <input type="text" name="email" value="{{ email }}"></p>
<input type="submit" name="submit" value="Send">
<input type="submit" name="submit" value="Send in 1 minute">
</form>
</body></html>
用于發送郵件的Flask-Mail需要一些配置,主要與發送郵件的郵件服務器、發送郵件時間相關。考慮到用戶名密碼安全性,作者將其放到了環境變量中。
# Flask-Mail configuration
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
app.config['MAIL_DEFAULT_SENDER'] = 'flask@example.com'
異步發送代碼如下。
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
return render_template('index.html', email=session.get('email', ''))
email = request.form['email']
session['email'] = email
# send the email
msg = Message('Hello from Flask',
recipients=[request.form['email']])
msg.body = 'This is a test email sent from a background Celery task.'
if request.form['submit'] == 'Send':
# send right away
send_async_email.delay(msg)
flash('Sending email to {0}'.format(email))
else:
# send in one minute
send_async_email.apply_async(args=[msg], countdown=60)
flash('An email will be sent to {0} in one minute'.format(email))
return redirect(url_for('index'))
用 session 將用戶鍵入的信息保存,以便頁面刷新時能夠使用該信息。
朋友們發現了,重點在發送郵件的代碼,使用的是Celery 的任務send_async_email
,通過調用它的 delay()
方法或apply_async
() 進行異步發送。
最后來看異步任務代碼。
@celery.task
def send_async_email(msg):
"""Background task to send an email with Flask-Mail."""
with app.app_context():
mail.send(msg)
使用裝飾者 celery.task 包裝 send_async_email , 使其成為后臺運行的任務。因為Flask-Mail需要應用的context,所以需要在調用send方法前先創建應用的context環境。
另一點很重要,從異步調用的返回值是不會保存的,所以應用本身無法知道是否異步調用是否成功。在這個例子之中需要看Celery的消費者的輸出才能確定發送郵件過程是否有問題。
第一個例子比較簡單,我們起了后臺任務然后就不必再去管它了。很多應用的需求與例子一相仿。
然而也會有一些應用,需要監控后臺任務的運行,獲得任務的結果。下面來看第二個例子。
例二:顯示狀態更新進度
用戶可以點擊按鈕以啟動一個或者多個長時間任務,此時在網頁使用ajax技術不斷輪詢服務器以更新所有的這些長時間任務們的狀態。
而對于每一個長時間任務,網頁上會有一個窗臺條、一個進度百分比、一個狀態消息與之對應,當完成時會顯示相應結果。
狀態更新時后臺任務代碼。
@celery.task(bind=True)
def long_task(self):
"""Background task that runs a long function with progress reports."""
verb = ['Starting up', 'Booting', 'Repairing', 'Loading', 'Checking']
adjective = ['master', 'radiant', 'silent', 'harmonic', 'fast']
noun = ['solar array', 'particle reshaper', 'cosmic ray', 'orbiter', 'bit']
message = ''
total = random.randint(10, 50)
for i in range(total):
if not message or random.random() < 0.25:
message = '{0} {1} {2}...'.format(random.choice(verb),
random.choice(adjective),
random.choice(noun))
self.update_state(state='PROGRESS',
meta={'current': i, 'total': total,
'status': message})
time.sleep(1)
return {'current': 100, 'total': 100, 'status': 'Task completed!',
'result': 42}
代碼中作者在Celery 裝飾者中加入了 bind=True
參數,這使得Celery向函數中傳入了self
參數,因此在函數中能夠記錄狀態更新。
本例中隨機挑選了一些單詞作為狀態的更新,同時,選取隨機數作為每個后臺任務運行時間。
self.update_state()
方法用于指明 Celery如何接收任務更新。
Celery有很多內建狀態比如STARTED
, SUCCESS
等等,當然Celery也允許程序員自定義狀態。本例子中使用的是自定義狀態,PROGRESS
。與PROGRESS
一起的還有metadata
。 metadata
是一個字典,包含當
前進度,任務大小,以及消息。
當循環跳出時,返回字典,字典中包含任務的執行結果。
long_task()
函數在 Celery消費者進程中運行。下面看一下Flask應用如何啟動該后臺任務。
@app.route('/longtask', methods=['POST'])
def longtask():
task = long_task.apply_async()
return jsonify({}), 202, {'Location': url_for('taskstatus', task_id=task.id)}
用戶需要向/longtask
發送 POST
請求以觸發后臺任務執行。服務器啟動任務并存儲返回值。作者使用了狀態碼202
,在REST API
中有“請求正在處理中”的意思,而加入了Location
頭則是為了生產者能夠獲取任務執行時的狀態信息。url_for
用于生成路由到taskstatus
函數的url,并且該url包含task id,task id的值是task.id
.
taskstatus 函數用于獲取后臺任務的更新狀態。
@app.route('/status/<task_id>')
def taskstatus(task_id):
task = long_task.AsyncResult(task_id)
if task.state == 'PENDING':
// job did not start yet
response = {
'state': task.state,
'current': 0,
'total': 1,
'status': 'Pending...'
}
elif task.state != 'FAILURE':
response = {
'state': task.state,
'current': task.info.get('current', 0),
'total': task.info.get('total', 1),
'status': task.info.get('status', '')
}
if 'result' in task.info:
response['result'] = task.info['result']
else:
# something went wrong in the background job
response = {
'state': task.state,
'current': 1,
'total': 1,
'status': str(task.info), # this is the exception raised
}
return jsonify(response)
為了得到后臺任務產生的數據,使用了task id作為參數創建了一個task 對象。
本函數產生了JSON響應,JSON響應中的內容與update_state()
更新的一致。
我們使用task.state
區分后臺任務的狀態:本例有未運行、未發生錯誤、發生錯誤三種狀態。
我們使用 task.info 訪問任務相關信息。而發生錯誤時, task.state 的狀態是 FAILURE 時,異常會包含在 task.info 之中。
前端JS代碼
作者用的是nanobar.js實現進度條,用了jQuery的ajax。
<script src="http://cdnjs.cloudflare.com/ajax/libs/nanobar/0.2.1/nanobar.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
啟動后臺任務的按鈕的JS代碼如下。
function start_long_task() {
// add task status elements
div = $('<div class="progress"><div></div><div>0%</div><div>...</div><div> </div></div><hr>');
$('#progress').append(div);
// create a progress bar
var nanobar = new Nanobar({
bg: '#44f',
target: div[0].childNodes[0]
});
// send ajax POST request to start background job
$.ajax({
type: 'POST',
url: '/longtask',
success: function(data, status, request) {
status_url = request.getResponseHeader('Location');
update_progress(status_url, nanobar, div[0]);
},
error: function() {
alert('Unexpected error');
}
});
}
其中被加入的HTML元素與任務的信息的對應關系如下。
<div class="progress">
<div></div> <-- Progress bar
<div>0%</div> <-- Percentage
<div>...</div> <-- Status message
<div> </div> <-- Result
</div><hr>
start_long_task()
函數通過ajax向 /longtask
發送POST
請求,使得后臺任務開始運行。
當ajax的POST
請求返回時,回調函數獲得響應,響應中包含形如 /status/<task_id>
的url, 其他函數(如update_progress )用此url從 taskstatus 函數獲取數據。
調用函數 update_progress()
,向函數傳入start_url
以及 nanoba
r變量,用于生成進度條。
function update_progress(status_url, nanobar, status_div) {
// send GET request to status URL
$.getJSON(status_url, function(data) {
// update UI
percent = parseInt(data['current'] * 100 / data['total']);
nanobar.go(percent);
$(status_div.childNodes[1]).text(percent + '%');
$(status_div.childNodes[2]).text(data['status']);
if (data['state'] != 'PENDING' && data['state'] != 'PROGRESS') {
if ('result' in data) {
// show result
$(status_div.childNodes[3]).text('Result: ' + data['result']);
}
else {
// something unexpected happened
$(status_div.childNodes[3]).text('Result: ' + data['state']);
}
}
else {
// rerun in 2 seconds
setTimeout(function() {
update_progress(status_url, nanobar, status_div);
}, 2000);
}
});
}
update_progress
函數向/status/<task_id>
發送GET請求,獲得json數據然后更新相應的頁面元素。
當后臺任務完成時,result會加載到頁面之中。如果沒有result的話,這就意味著任務的執行以失敗告終,此時任務的狀態是 FAILURE 。
任當后臺任務運行時,為了能夠持續獲得任務狀態并更新頁面,作者使用了定時器,定時器每個兩秒一更新直到后臺任務完成。
運行例子
讀者先安裝好virtualenv(強烈推薦!但是virtualenv非必需安裝)。
下載代碼,安裝相應庫,如下。
1 $ git clone https://github.com/miguelgrinberg/flask-celery-example.git
2 $ cd flask-celery-example
3 $ virtualenv venv
4 $ source venv/bin/activate
5 (venv) $ pip install -r requirements.txt
未安裝virtualenv的話直接跳過第三行第四行命令。
redis server端讀者自行安裝。安裝后運行啟動。
Celery 消費者也需要讀者運行,使用 celery命令。
郵件用戶名密碼自行設置。
$ export MAIL_USERNAME=<your-mail-username>
$ export MAIL_PASSWORD=<your-mail-password>
$ source venv/bin/activate(venv)
$ celery worker -A app.celery --loglevel=info
Celery的 -A選項是應用中的celer對象,與文章最開頭的代碼對應。
--loglevel=info 則是讓日志內容更為詳細。
最后啟動應用。
$ source venv/bin/activate(venv) $ python app.py
訪問http://localhost:5000/ 即可。
原文鏈接 : http://blog.miguelgrinberg.com/post/using-celery-with-flask
譯文鏈接 : http://www.cnblogs.com/ifkite/p/4257721.html
姊妹篇鏈接:http://blog.miguelgrinberg.com/post/celery-and-the-flask-application-factory-pattern