[譯] 在 Flask 中使用 Redis Queue 實現非同步任務

劉嘉一發表於2019-01-17

[譯] 在 Flask 中使用 Redis Queue 實現非同步任務

如果你的應用中存在長執行任務,你應當把它們從普通流程中剝離並置於後臺執行。

可能你的 web 應用會要求使用者在註冊時上傳頭像(圖片可能需要被裁剪)和進行郵箱驗證。如果你直接在請求處理函式中去加工圖片和傳送驗證郵件,那麼終端使用者不得不等待這些執行的完成。相反,你更希望把這些任務放到任務佇列中,並由一個 worker 執行緒來處理,這種情況下應用就能立刻響應客戶端的請求了。由此一來,終端使用者可以在客戶端繼續其他的操作,你的應用也能被釋放去響應其他使用者的請求。

這篇文章講了如何在 Flask 應用中配置 Redis Queue(RQ)來處理長執行任務。

當然 Celery 也是一個不錯的解決方案。不過相比於 Redis Queue,它會稍顯複雜並引入更多的依賴項。

目錄

本文目標

閱讀完本文後,你應當學會:

  1. 在 Flask 應用中整合 Redis Queue 並建立相應任務。
  2. 使用 Docker 映象化包含 Flask 和 Redis 的應用。
  3. 使用獨立的 worker 執行緒在後臺處理長執行任務。
  4. 配置 RQ Dashboard 用於監控任務佇列、作業和 worker 執行緒。
  5. 使用 Docker 擴充套件 worker 執行緒的數量。

工作流程

在本文中,我們的目標是藉助 Redis Queue 的能力開發一個能處理長執行任務的 Flask 應用,其中長執行任務的執行獨立於普通請求、響應的執行。

  1. 終端使用者通過 POST 請求服務端建立一個新任務
  2. 如圖所示,任務佇列會增加一個新任務,之後服務端再把任務 id 返回給客戶端
  3. 建立好的任務會在服務端後臺執行,客戶端只需使用 AJAX 不斷輪詢任務狀態即可

Flask 整合 Redis Queue 的呼叫時序圖

最終我們將實現一個如下所示的應用:

開發完成

專案配置

想要繼續看下去嗎?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,你應該能看到如下頁面:

flask、redis queue 和 docker

任務觸發

project/client/static/main.js 裡的監聽器監聽到按鍵的點選後,它會獲取按鍵對應的任務型別 — 123,並把得到的任務型別當作引數通過 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"
}
複製程式碼

同樣讓我們在瀏覽器中試試效果:

flask, redis queue, docker

任務控制檯

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 來看看整個控制檯:

rq dashboard

可以嘗試啟動一些任務來試試控制檯功能:

rq dashboard

你也可以通過增加 worker 的數量來觀察應用的變化:

$ docker-compose up -d --build --scale worker=3
複製程式碼

結語

這是一篇在 Flask 中配置 Redis Queue 用於處理長執行任務的基礎指南。你可以利用該佇列來執行任何可能阻塞或拖慢使用者體驗的程式。

還想繼續挑戰自己?

  1. 註冊 Digital Ocean 並利用 Docker Swarm 把這個應用部署到多個節點。
  2. 為介面增加單元測試。(可以使用 fakeredis 來模擬 Redis 例項)
  3. 利用 Flask-SocketIO 把客戶端的輪詢改為 websocket 連線。

可以在 此倉庫 找到本文程式碼。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章