- 原文地址:Asynchronous Tasks with Flask and Redis Queue
- 原文作者:Michael Herman
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:劉嘉一
- 校對者:kasheemlew
如果你的應用中存在長執行任務,你應當把它們從普通流程中剝離並置於後臺執行。
可能你的 web 應用會要求使用者在註冊時上傳頭像(圖片可能需要被裁剪)和進行郵箱驗證。如果你直接在請求處理函式中去加工圖片和傳送驗證郵件,那麼終端使用者不得不等待這些執行的完成。相反,你更希望把這些任務放到任務佇列中,並由一個 worker 執行緒來處理,這種情況下應用就能立刻響應客戶端的請求了。由此一來,終端使用者可以在客戶端繼續其他的操作,你的應用也能被釋放去響應其他使用者的請求。
這篇文章講了如何在 Flask 應用中配置 Redis Queue(RQ)來處理長執行任務。
當然 Celery 也是一個不錯的解決方案。不過相比於 Redis Queue,它會稍顯複雜並引入更多的依賴項。
目錄
本文目標
閱讀完本文後,你應當學會:
- 在 Flask 應用中整合 Redis Queue 並建立相應任務。
- 使用 Docker 映象化包含 Flask 和 Redis 的應用。
- 使用獨立的 worker 執行緒在後臺處理長執行任務。
- 配置 RQ Dashboard 用於監控任務佇列、作業和 worker 執行緒。
- 使用 Docker 擴充套件 worker 執行緒的數量。
工作流程
在本文中,我們的目標是藉助 Redis Queue 的能力開發一個能處理長執行任務的 Flask 應用,其中長執行任務的執行獨立於普通請求、響應的執行。
- 終端使用者通過 POST 請求服務端建立一個新任務
- 如圖所示,任務佇列會增加一個新任務,之後服務端再把任務 id 返回給客戶端
- 建立好的任務會在服務端後臺執行,客戶端只需使用 AJAX 不斷輪詢任務狀態即可
最終我們將實現一個如下所示的應用:
專案配置
想要繼續看下去嗎?clone 下面的倉庫來看看裡面的程式碼和結構吧:
$ git clone https://github.com/mjhea0/flask-redis-queue --branch base --single-branch
$ cd flask-redis-queue
複製程式碼
因為我們一共需要管理三個程式(Flask、Redis 和 worker),為了簡化這一系列工作流,這裡我們選擇了使用 Docker 來部署,最終我們僅需在一個終端裡就可以執行整個應用了。
像這樣就能將應用跑起來:
$ docker-compose up -d --build
複製程式碼
使用你的瀏覽器訪問 http://localhost:5004,你應該能看到如下頁面:
任務觸發
當 project/client/static/main.js 裡的監聽器監聽到按鍵的點選後,它會獲取按鍵對應的任務型別 — 1
、2
或 3
,並把得到的任務型別當作引數通過 AJAX POST 請求發到服務端。
$('.btn').on('click', function() {
$.ajax({
url: '/tasks',
data: { type: $(this).data('type') },
method: 'POST'
})
.done((res) => {
getStatus(res.data.task_id)
})
.fail((err) => {
console.log(err)
});
});
複製程式碼
在服務端,project/server/main/views.py 會負責處理客戶端發來的請求:
@main_blueprint.route('/tasks', methods=['POST'])
def run_task():
task_type = request.form['type']
return jsonify(task_type), 202
複製程式碼
下面我們來裝配 Redis Queue。
Redis Queue
首先我們需要在 docker-compose.yml 中新增配置以啟動兩個新的程式 — Redis 和 worker:
version: '3.7'
services:
web:
build: .
image: web
container_name: web
ports:
- '5004:5000'
command: python manage.py run -h 0.0.0.0
volumes:
- .:/usr/src/app
environment:
- FLASK_DEBUG=1
- APP_SETTINGS=project.server.config.DevelopmentConfig
depends_on:
- redis
worker:
image: web
command: python manage.py run_worker
volumes:
- .:/usr/src/app
environment:
- APP_SETTINGS=project.server.config.DevelopmentConfig
depends_on:
- redis
redis:
image: redis:4.0.11-alpine
複製程式碼
在 "project/server/main" 目錄中新增一個新的任務 tasks.py:
# project/server/main/tasks.py
import time
def create_task(task_type):
time.sleep(int(task_type) * 10)
return True
複製程式碼
更新我們的檢視程式碼,讓它能連線 Redis 並把任務放入佇列,最後再把任務的 id 返回給客戶端:
@main_blueprint.route('/tasks', methods=['POST'])
def run_task():
task_type = request.form['type']
with Connection(redis.from_url(current_app.config['REDIS_URL'])):
q = Queue()
task = q.enqueue(create_task, task_type)
response_object = {
'status': 'success',
'data': {
'task_id': task.get_id()
}
}
return jsonify(response_object), 202
複製程式碼
別忘了正確地引入上面用到的庫:
import redis
from rq import Queue, Connection
from flask import render_template, Blueprint, jsonify, \
request, current_app
from project.server.main.tasks import create_task
複製程式碼
更新 BaseConfig
檔案:
class BaseConfig(object):
"""基礎配置"""
WTF_CSRF_ENABLED = True
REDIS_URL = 'redis://redis:6379/0'
QUEUES = ['default']
複製程式碼
細心的讀者可能發現了,我們在引用 redis
服務(在 docker-compose.yml 中引入的)的地址時,使用了 REDIS_URL
而非 localhost
或是某個特定 IP。在 Docker 中如何通過 hostname 連線其他服務,可以在 Docker Compose 官方文件 中找到答案。
最終,我們便可以使用 Redis Queue 的 worker 來處理放在隊首的任務了。
@cli.command('run_worker')
def run_worker():
redis_url = app.config['REDIS_URL']
redis_connection = redis.from_url(redis_url)
with Connection(redis_connection):
worker = Worker(app.config['QUEUES'])
worker.work()
複製程式碼
在這裡,我們通過自定義的 CLI 命令來啟動 worker。
需要注意的是,通過裝飾器 @cli.command()
啟動的程式碼可以訪問到應用的上下文,以及訪問到在 project/server/config.py 中定義的配置變數。
同樣需要引入正確的庫:
import redis
from rq import Connection, Worker
複製程式碼
在 requirements 檔案中新增應用的依賴資訊:
redis==2.10.6
rq==0.12.0
複製程式碼
構建並啟動新的 Docker 容器:
$ docker-compose up -d --build
複製程式碼
讓我們試試觸發一個任務:
$ curl -F type=0 http://localhost:5004/tasks
複製程式碼
你應該會得到類似的返回:
{
"data": {
"task_id": "bdad64d0-3865-430e-9cc3-ec1410ddb0fd"
},
"status": "success"
}
複製程式碼
任務狀態
讓我們回頭看看客戶端的按鍵監聽器:
$('.btn').on('click', function() {
$.ajax({
url: '/tasks',
data: { type: $(this).data('type') },
method: 'POST'
})
.done((res) => {
getStatus(res.data.task_id)
})
.fail((err) => {
console.log(err)
});
});
複製程式碼
每當建立任務的 AJAX 請求返回後,我們便會取出其中的任務 id 繼續呼叫 getStatus()
。若 getStatus()
也成功返回,那麼我們便在表格 DOM 中新增一行記錄。
function getStatus(taskID) {
$.ajax({
url: `/tasks/${taskID}`,
method: 'GET'
})
.done((res) => {
const html = `
<tr>
<td>${res.data.task_id}</td>
<td>${res.data.task_status}</td>
<td>${res.data.task_result}</td>
</tr>`
$('#tasks').prepend(html);
const taskStatus = res.data.task_status;
if (taskStatus === 'finished' || taskStatus === 'failed') return false;
setTimeout(function() {
getStatus(res.data.task_id);
}, 1000);
})
.fail((err) => {
console.log(err);
});
}
複製程式碼
更新檢視層程式碼:
@main_blueprint.route('/tasks/<task_id>', methods=['GET'])
def get_status(task_id):
with Connection(redis.from_url(current_app.config['REDIS_URL'])):
q = Queue()
task = q.fetch_job(task_id)
if task:
response_object = {
'status': 'success',
'data': {
'task_id': task.get_id(),
'task_status': task.get_status(),
'task_result': task.result,
}
}
else:
response_object = {'status': 'error'}
return jsonify(response_object)
複製程式碼
呼叫下面命令在佇列中新增一個任務:
$ curl -F type=1 http://localhost:5004/tasks
複製程式碼
然後再用上面返回體中的 task_id
來請求新增的任務詳情介面:
$ curl http://localhost:5004/tasks/5819789f-ebd7-4e67-afc3-5621c28acf02
{
"data": {
"task_id": "5819789f-ebd7-4e67-afc3-5621c28acf02",
"task_result": true,
"task_status": "finished"
},
"status": "success"
}
複製程式碼
同樣讓我們在瀏覽器中試試效果:
任務控制檯
RQ Dashboard 是一個 Redis Queue 的輕量級 web 端監控系統。
為了整合 RQ Dashboard,首先你需要在 "project" 下新建一個 "dashboard" 資料夾,然後再在其中新建一個 Dockerfile:
FROM python:3.7.0-alpine
RUN pip install rq-dashboard
EXPOSE 9181
CMD ["rq-dashboard"]
複製程式碼
接著把上面的模組作為 service 新增到 docker-compose.yml 中:
version: '3.7'
services:
web:
build: .
image: web
container_name: web
ports:
- '5004:5000'
command: python manage.py run -h 0.0.0.0
volumes:
- .:/usr/src/app
environment:
- FLASK_DEBUG=1
- APP_SETTINGS=project.server.config.DevelopmentConfig
depends_on:
- redis
worker:
image: web
command: python manage.py run_worker
volumes:
- .:/usr/src/app
environment:
- APP_SETTINGS=project.server.config.DevelopmentConfig
depends_on:
- redis
redis:
image: redis:4.0.11-alpine
dashboard:
build: ./project/dashboard
image: dashboard
container_name: dashboard
ports:
- '9181:9181'
command: rq-dashboard -H redis
複製程式碼
構建並啟動新的容器:
$ docker-compose up -d --build
複製程式碼
開啟 http://localhost:9181 來看看整個控制檯:
可以嘗試啟動一些任務來試試控制檯功能:
你也可以通過增加 worker 的數量來觀察應用的變化:
$ docker-compose up -d --build --scale worker=3
複製程式碼
結語
這是一篇在 Flask 中配置 Redis Queue 用於處理長執行任務的基礎指南。你可以利用該佇列來執行任何可能阻塞或拖慢使用者體驗的程式。
還想繼續挑戰自己?
- 註冊 Digital Ocean 並利用 Docker Swarm 把這個應用部署到多個節點。
- 為介面增加單元測試。(可以使用 fakeredis 來模擬 Redis 例項)
- 利用 Flask-SocketIO 把客戶端的輪詢改為 websocket 連線。
可以在 此倉庫 找到本文程式碼。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。