中介軟體---分散式任務排程---Celery
最近研究了下非同步任務神器-Celery,發現非常好用,可以說是高可用,假如你發出一個任務執行命令給 Celery,只要 Celery 的執行單元 (worker) 在執行,那麼它一定會執行;如果執行單元 (worker) 出現故障,如斷電,斷網情況下,只要執行單元 (worker) 恢復執行,那麼它會繼續執行你已經發出的命令。這一點有很強的實用價值:假如有交易系統接到了大量交易請求,主機卻掛了,但前端使用者仍可以繼續發交易請求,傳送交易請求後,使用者無需等待。待主機恢復後,已發出的交易請求可以繼續執行,只不過使用者收到交易確認的時間延長而已,但並不影響使用者體驗。
Celery 簡介
它是一個非同步任務排程工具,使用者使用 Celery 產生任務,借用中間人來傳遞任務,任務執行單元從中間人那裡消費任務。任務執行單元可以單機部署,也可以分散式部署,因此 Celery 是一個高可用的生產者消費者模型的非同步任務佇列。你可以將你的任務交給 Celery 處理,也可以讓 Celery 自動按 crontab 那樣去自動排程任務,然後去做其他事情,你可以隨時檢視任務執行的狀態,也可以讓 Celery 執行完成後自動把執行結果告訴你。
應用場景:
-
高併發的請求任務。網際網路已經普及,人們的衣食住行中產生的交易都可以線上進行,這就避免不了某些時間極高的併發任務請求,如公司中常見的購買理財、學生繳費,在理財產品投放市場後、開學前的一段時間,交易量猛增,確認交易時間較長,此時可以把交易請求任務交給 Celery 去非同步執行,執行完再將結果返回給使用者。使用者提交後不需要等待,任務完成後會通知到使用者(購買成功或繳費成功),提高了網站的整體吞吐量和響應時間,幾乎不需要增加硬體成本即可滿足高併發。
-
定時任務。在雲端計算,大資料,叢集等技術越來越普及,生產環境的機器也越來越多,定時任務是避免不了的,如果每臺機器上執行著自己的 crontab 任務,管理起來相當麻煩,例如當進行災備切換時,某些 crontab 任務可能需要單獨手工調起,給運維人員造成極大的麻煩,有了 Celery ,你可以集中管理所有機器的定時任務,而且災備無論何時切換,crontab 任務總能正確的執行。
-
非同步任務。 一些耗時較長的操作,比如 I/O 操作,網路請求,可以交給 Celery 去非同步執行,使用者提交後可以做其他事情,當任務完成後將結果返回使用者即可,可提高使用者體驗。
Celery 的優點
-
純 Python 編寫,開源。這已經是站在巨人的肩膀上了,雖然 Celery 是由純 Python 編寫的,但協議可以用任何語言實現。迄今,已有 Ruby 實現的 RCelery 、node.js 實現的 node-celery 以及一個 PHP 客戶端 ,語言互通也可以通過 using webhooks 實現。
-
靈活的配置。預設的配置已經滿足絕大多數需求,因此你不需要編寫配置檔案基本就可以使用,當然如果有個性化地定製,你可以選擇使用配置檔案,也可以將配置寫在原始碼檔案裡。
-
方便監控。任務的所有狀態,均在你的掌握之下。
-
完善的錯誤處理。
-
靈活的任務佇列和任務路由。你可以非常方便地將一個任務執行在你指定的佇列上,這叫任務路由。
Celery 的架構
學習一個工具,最好先從它的架構理解,輔以快速入門的程式碼來實踐,最深入的就是閱讀他的原始碼了,下圖是 Celery 的架構圖。
celery架構.png
任務生產者 :呼叫Celery提供的API,函式,裝飾器而產生任務並交給任務佇列的都是任務生產者。
任務排程 Beat:Celery Beat程式會讀取配置檔案的內容,週期性的將配置中到期需要執行的任務傳送給任務佇列
中間人(Broker):Celery 用訊息通訊,通常使用中間人(Broker)在客戶端和 worker 之前傳遞,這個過程從客戶端向佇列新增訊息開始,之後中間人把訊息派送給 worker。官方給出的實現Broker的工具有:
名稱 | 狀態 | 監視 | 遠端控制 |
---|---|---|---|
RabbitMQ | 穩定 | 是 | 是 |
Redis | 穩定 | 是 | 是 |
Mongo DB | 實驗性 | 是 | 是 |
Beanstalk | 實驗性 | 否 | 否 |
Amazon SQS | 實驗性 | 否 | 否 |
Couch DB | 實驗性 | 否 | 否 |
Zookeeper | 實驗性 | 否 | 否 |
Django DB | 實驗性 | 否 | 否 |
SQLAlchemy | 實驗性 | 否 | 否 |
Iron MQ | 第三方 | 否 | 否 |
在實際使用中我們選擇 RabbitMQ 或 Redis 作為中間人即可。
執行單元 worker:worker 是任務執行單元,是屬於任務佇列的消費者,它持續地監控任務佇列,當佇列中有新地任務時,它便取出來執行。worker 可以執行在不同的機器上,只要它指向同一個中間人即可,worker還可以監控一個或多個任務佇列, Celery 是分散式任務佇列的重要原因就在於 worker 可以分佈在多臺主機中執行。修改配置檔案後不需要重啟 worker,它會自動生效。
任務結果儲存backend:用來持久儲存 Worker 執行任務的結果,Celery支援不同的方式儲存任務的結果,包括AMQP,Redis,memcached,MongoDb,SQLAlchemy等。
Celery 的使用示例:
以 Python3.6.5 版本為例。
1. 安裝 python 庫:celery,redis。
pip install celery #安裝celery
pip install celery[librabbitmq,redis,auth,msgpack] #安裝celery對應的依賴
celery其他的依賴包如下:
序列化:
celery[auth]:使用auth序列化。
celery[msgpack]:使用msgpack序列化。
celery[yaml]:使用yaml序列化。
併發:
celery[eventlet]:使用eventlet池。
celery[gevent]:使用gevent池。
celery[threads]:使用執行緒池。
傳輸和後端:
celery[librabbitmq]:使用librabbitmq的C庫.
celery[redis]:使用Redis作為訊息傳輸方式或結果後端。
celery[mongodb]:使用MongoDB作為訊息傳輸方式(實驗性),或是結果後端(已支援)。
celery[sqs]:使用AmazonSQS作為訊息傳輸方式(實驗性)。
celery[memcache]:使用memcache作為結果後端。
celery[cassandra]:使用ApacheCassandra作為結果後端。
celery[couchdb]:使用CouchDB作為訊息傳輸方式(實驗性)。
celery[couchbase]:使用CouchBase作為結果後端。
celery[beanstalk]:使用Beanstalk作為訊息傳輸方式(實驗性)。
celery[zookeeper]:使用Zookeeper作為訊息傳輸方式。
celery[zeromq]:使用ZeroMQ作為訊息傳輸方式(實驗性)。
celery[sqlalchemy]:使用SQLAlchemy作為訊息傳輸方式(實驗性),或作為結果後端(已支援)。
celery[pyro]:使用Pyro4訊息傳輸方式(實驗性)。
celery[slmq]:使用SoftLayerMessageQueue傳輸(實驗性)。
2. 安裝 Redis,以 ubuntu 作業系統為例(如果使用 RabbitMQ,自己裝一下就可以)。
通過原始碼安裝:
$ wget http://download.redis.io/releases/redis-4.0.11.tar.gz
$ tar xzf redis-4.0.11.tar.gz
$ cd redis-4.0.11
$ make
修改 redis 配置檔案 redis.conf,修改bind = 127.0.0.0.1為bind = 0.0.0.0,意思是允許遠端訪問redis資料庫。
啟動 redis-server
$ cd src
$ ./redis-server ../redis.conf
3. 第一個 celery 應用程式。
功能:模擬一個耗時操作,並列印 worker 所在機器的 IP 地址,中間人和結果儲存都使用 redis 資料庫。
#encoding=utf-8
#filename my_first_celery.py
from celery import Celery
import time
import socket
app = Celery(''tasks'', broker='redis://127.0.0.1:6379/0',backend ='redis://127.0.0.1:6379/0' )
def get_host_ip():
"""
查詢本機ip地址
:return: ip
"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
finally:
s.close()
return ip
@app.task
def add(x, y):
time.sleep(3) # 模擬耗時操作
s = x + y
print("主機IP {}: x + y = {}".format(get_host_ip(),s))
return s
啟動這個 worker:
celery -A my_first_celery worker -l info
這裡,-A 表示我們的程式的模組名稱,worker 表示啟動一個執行單元,-l 是批 -level,表示列印的日誌級別。可以使用 celery –help 命令來檢視celery命令的幫助文件。執行命令後,worker介面展示資訊如下:
aaron@ubuntu:~/project$ celery -A my_first_celery worker -l info
-------------- celery@ubuntu v4.2.1 (windowlicker)
---- **** -----
--- * *** * -- Linux-4.10.0-37-generic-x86_64-with-Ubuntu-16.04-xenial 2018-08-27 22:46:00
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: tasks:0x7f1ce0747080
- ** ---------- .> transport: redis://127.0.0.1:6379/0
- ** ---------- .> results: redis://127.0.0.1:6379/0
- *** --- * --- .> concurrency: 1 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. my_first_celery.add
[2018-08-27 22:46:00,726: INFO/MainProcess] Connected to redis://127.0.0.1:6379/0
[2018-08-27 22:46:00,780: INFO/MainProcess] mingle: searching for neighbors
[2018-08-27 22:46:02,075: INFO/MainProcess] mingle: all alone
[2018-08-27 22:46:02,125: INFO/MainProcess] celery@ubuntu ready.
已經相當清晰了。 如果你不想使用 celery 命令來啟動 worker,可直接使用檔案來驅動,修改 my_first_celery.py (增加入口函式main)
if __name__ == '__main__':
app.start()
再執行
python my_first_celery.py worker
即可。
4. 呼叫任務
在 my_first_celery.py 的同級目錄下編寫如下指令碼 start_task.py如下。
from my_first_celery import add #匯入我們的任務函式add
import time
result = add.delay(12,12) #非同步呼叫,這一步不會阻塞,程式會立即往下執行
while not result.ready():# 迴圈檢查任務是否執行完畢
print(time.strftime("%H:%M:%S"))
time.sleep(1)
print(result.get()) #獲取任務的返回結果
print(result.successful()) #判斷任務是否成功執行
執行
python start_task.py
結果如下所示:
22:50:59
22:51:00
22:51:01
24
True
發現等待了大約3秒鐘後,任務返回了結果24,並且是成功完成,此時worker介面增加的資訊如下:
[2018-08-27 22:50:58,840: INFO/MainProcess] Received task: my_first_celery.add[a0c4bb6b-17af-474c-9eab-407d593a7807]
[2018-08-27 22:51:01,898: WARNING/ForkPoolWorker-1] 主機IP 192.168.195.128: x + y = 24
[2018-08-27 22:51:01,915: INFO/ForkPoolWorker-1] Task my_first_celery.add[a0c4bb6b-17af-474c-9eab-407d593a7807] succeeded in 3.067237992000173s: 24
這裡的資訊非常詳細,其中a0c4bb6b-17af-474c-9eab-407d593a7807是taskid,只要指定了 backend,根據這個 taskid 可以隨時去 backend 去查詢執行結果,使用方法如下:
>>> from my_first_celery import add
>>> taskid= 'a0c4bb6b-17af-474c-9eab-407d593a7807'
>>> add.AsyncResult(taskid).get()
24
>>>#或者
>>> from celery.result import AsyncResult
>>> AsyncResult(taskid).get()
24
重要說明:如果想遠端執行 worker 機器上的作業,請將 my_first_celery.py 和 start_tasks.py 複製到遠端主機上(需要安裝 celery),修改 my_first_celery.py 指向同一個中間人和結果儲存,再執行 start_tasks.py 即可遠端執行 worker 機器上的作業。my_first_celery.add函式的程式碼不是必須的,你也要以這樣呼叫任務:
from my_first_celery import app
app.send_task("my_first_celery.add",args=(1,3))
5. 第一個 celery 專案
在生產環境中往往有大量的任務需要排程,單獨一個檔案是不方便的,celery 當然支援模組化的結構,我這裡寫了一個用於學習的 Celery 小型工程專案,含有佇列操作,任務排程等實用操作,目錄樹如下所示:
celeryproj.png
其中 init.py是空檔案,目的是告訴 Python myCeleryProj 是一個可匯入的包.
app.py
from celery import Celery
app = Celery("myCeleryProj", include=["myCeleryProj.tasks"])
app.config_from_object("myCeleryProj.settings")
if __name__ == "__main__":
app.start()
settings.py
from kombu import Queue
import re
from datetime import timedelta
from celery.schedules import crontab
CELERY_QUEUES = ( # 定義任務佇列
Queue("default", routing_key="task.#"), # 路由鍵以“task.”開頭的訊息都進default佇列
Queue("tasks_A", routing_key="A.#"), # 路由鍵以“A.”開頭的訊息都進tasks_A佇列
Queue("tasks_B", routing_key="B.#"), # 路由鍵以“B.”開頭的訊息都進tasks_B佇列
)
CELERY_TASK_DEFAULT_QUEUE = "default" # 設定預設佇列名為 default
CELERY_TASK_DEFAULT_EXCHANGE = "tasks"
CELERY_TASK_DEFAULT_EXCHANGE_TYPE = "topic"
CELERY_TASK_DEFAULT_ROUTING_KEY = "task.default"
CELERY_ROUTES = (
[
(
re.compile(r"myCeleryProj\.tasks\.(taskA|taskB)"),
{"queue": "tasks_A", "routing_key": "A.import"},
), # 將tasks模組中的taskA,taskB分配至佇列 tasks_A ,支援正規表示式
(
"myCeleryProj.tasks.add",
{"queue": "default", "routing_key": "task.default"},
), # 將tasks模組中的add任務分配至佇列 default
],
)
# CELERY_ROUTES = (
# [
# ("myCeleryProj.tasks.*", {"queue": "default"}), # 將tasks模組中的所有任務分配至佇列 default
# ],
# )
# CELERY_ROUTES = (
# [
# ("myCeleryProj.tasks.add", {"queue": "default"}), # 將add任務分配至佇列 default
# ("myCeleryProj.tasks.taskA", {"queue": "tasks_A"}),# 將taskA任務分配至佇列 tasks_A
# ("myCeleryProj.tasks.taskB", {"queue": "tasks_B"}),# 將taskB任務分配至佇列 tasks_B
# ],
# )
BROKER_URL = "redis://127.0.0.1:6379/0" # 使用redis 作為訊息代理
CELERY_RESULT_BACKEND = "redis://127.0.0.1:6379/0" # 任務結果存在Redis
CELERY_RESULT_SERIALIZER = "json" # 讀取任務結果一般效能要求不高,所以使用了可讀性更好的JSON
CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24 # 任務過期時間,不建議直接寫86400,應該讓這樣的magic數字表述更明顯
CELERYBEAT_SCHEDULE = {
"add": {
"task": "myCeleryProj.tasks.add",
"schedule": timedelta(seconds=10),
"args": (10, 16),
},
"taskA": {
"task": "myCeleryProj.tasks.taskA",
"schedule": crontab(hour=21, minute=10),
},
"taskB": {
"task": "myCeleryProj.tasks.taskB",
"schedule": crontab(hour=21, minute=12),
},
}
tasks.py
import os
from myCeleryProj.app import app
import time
import socket
def get_host_ip():
"""
查詢本機ip地址
:return: ip
"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
finally:
s.close()
return ip
@app.task
def add(x, y):
s = x + y
time.sleep(3) # 模擬耗時操作
print("主機IP {}: x + y = {}".format(get_host_ip(), s))
return s
@app.task
def taskA():
print("taskA begin...")
print(f"主機IP {get_host_ip()}")
time.sleep(3)
print("taskA done.")
@app.task
def taskB():
print("taskB begin...")
print(f"主機IP {get_host_ip()}")
time.sleep(3)
print("taskB done.")
readme.txt
#啟動 worker
#分別在三個終端視窗啟動三個佇列的worker,執行命令如下所示:
celery -A myCeleryProj.app worker -Q default -l info
celery -A myCeleryProj.app worker -Q tasks_A -l info
celery -A myCeleryProj.app worker -Q tasks_B -l info
#當然也可以一次啟動多個佇列,如下則表示一次啟動兩個佇列tasks_A,tasks_B。
celery -A myCeleryProj.app worker -Q tasks_A,tasks_B -l info
#則表示一次啟動兩個佇列tasks_A,tasks_B。
#最後我們再開啟一個視窗來呼叫task: 注意觀察worker介面的輸出
>>> from myCeleryProj.tasks import *
>>> add.delay(4,5);taskA.delay();taskB.delay() #同時發起三個任務
<AsyncResult: 21408d7b-750d-4c88-9929-fee36b2f4474>
<AsyncResult: 737b9502-77b7-47a6-8182-8e91defb46e6>
<AsyncResult: 69b07d94-be8b-453d-9200-12b37a1ca5ab>
#也可以使用下面的方法呼叫task
>>> from myCeleryProj.app import app
>>> app.send_task(myCeleryProj.tasks.add,args=(4,5)
>>> app.send_task(myCeleryProj.tasks.taskA)
>>> app.send_task(myCeleryProj.tasks.taskB)
(完)
相關文章
- 分散式任務排程分散式
- 分散式排程任務-ElasticJob分散式AST
- LTS分散式任務排程部署分散式
- 淺談分散式任務排程系統Celery的設計與實現分散式
- Aloha:一個分散式任務排程框架分散式框架
- celery 與 flask 實現非同步任務排程Flask非同步
- micro-job分散式任務排程框架更新分散式框架
- 分散式任務排程平臺XXL-JOB分散式
- 新一代分散式任務排程框架分散式框架
- 叢集及分散式定時任務中介軟體MEE_TIMED分散式
- 分散式任務排程系統設計小結分散式
- Spring Boot Quartz 分散式叢集任務排程實現Spring Bootquartz分散式
- 開源分散式任務排程系統就選:DolphinScheduler分散式
- LAF-DTX分散式事務中介軟體分散式
- 分散式任務排程平臺XXL-JOB快速搭建教程分散式
- SpringBoot自定義starter開發分散式任務排程實踐Spring Boot分散式
- SpringBoot分散式任務中介軟體開發 附視訊講解 (手把手教你開發和使用中介軟體)Spring Boot分散式
- 任務排程
- 分散式訊息中介軟體分散式
- 嵌入式軟體開發之程式架構設計-任務排程架構
- 帶有分散式鎖的Go計劃任務排程器- DEV分散式Godev
- 整合了這個分散式任務排程平臺,真的很爽~~分散式
- 分散式事務中介軟體Seata的設計原理分散式
- 中介軟體---分散式跟蹤---Pinpoint分散式
- 分散式任務排程內的 MySQL 分頁查詢最佳化分散式MySql
- Java 分散式任務排程平臺:PowerJob 快速開始+配置詳解Java分散式
- 分散式系統架構之構建你的任務排程中心分散式架構
- Easy Scheduler 1.0.2 釋出,分散式工作流任務排程系統分散式
- 基於任務排程的企業級分散式批處理方案分散式
- Airflow 任務排程AI
- Laravel 任務排程Laravel
- LiteOS-任務篇-原始碼分析-任務排程函式原始碼函式
- [原始碼解析] 分散式任務佇列 Celery 之啟動 Consumer原始碼分散式佇列
- 一個輕量級的分散式定時任務排程平臺-Cloudtask分散式Cloud
- SchedulerX 如何幫助使用者解決分散式任務排程難題?分散式
- [原始碼分析] 分散式任務佇列 Celery 之 傳送Task & AMQP原始碼分散式佇列MQ
- [原始碼分析] 並行分散式任務佇列 Celery 之 Timer & Heartbeat原始碼並行分散式佇列
- Spring 指南(排程任務)Spring