[英]Celery Beat: Limit to single task instance at a time
我有 celery beat 和 celery(四個工人)批量做一些處理步驟。 其中一項任務大致是“對於每個沒有創建 Y 的 X,創建一個 Y”。
該任務以半快速(10 秒)的速度定期運行。 任務完成得非常快。 還有其他任務正在進行中。
我多次遇到過這個問題,其中節拍任務顯然積壓了,因此同時執行相同的任務(來自不同的節拍時間),導致錯誤地重復工作。 任務似乎也是亂序執行的。
是否可以限制 celery beat 以確保一次僅完成一個任務的出色實例? 在任務上設置rate_limit=5
類的東西是“正確”的方法嗎?
是否可以確保按順序執行 beat 任務,例如,不是調度任務,而是將它添加到任務鏈中?
除了使這些任務本身以原子方式執行並且可以安全地同時執行之外,處理此問題的最佳方法是什么? 這不是我期望完成任務的限制......
任務本身是天真地定義的:
@periodic_task(run_every=timedelta(seconds=10))
def add_y_to_xs():
# Do things in a database
return
這是一個實際的(清理過的)日志:
[00:00.000]
foocorp.tasks.add_y_to_xs 已發送。 id->#1[00:00.001]
收到的任務:foocorp.tasks.add_y_to_xs[#1][00:10.009]
foocorp.tasks.add_y_to_xs 已發送。 id->#2[00:20.024]
foocorp.tasks.add_y_to_xs 已發送。 id->#3[00:26.747]
收到的任務:foocorp.tasks.add_y_to_xs[#2][00:26.748]
任務池:應用 #2[00:26.752]
收到的任務:foocorp.tasks.add_y_to_xs[#3][00:26.769]
接受任務:foocorp.tasks.add_y_to_xs[#2] pid:26528[00:26.775]
任務 foocorp.tasks.add_y_to_xs[#2] 在 0.0197986490093s 中成功:無[00:26.806]
任務池:應用 #1[00:26.836]
任務池:應用 #3[01:30.020]
接受任務:foocorp.tasks.add_y_to_xs[#1] pid:26526[01:30.053]
接受任務:foocorp.tasks.add_y_to_xs[#3] pid:26529[01:30.055]
foocorp.tasks.add_y_to_xs[#1]: 為 X id 添加 Y #9725[01:30.070]
foocorp.tasks.add_y_to_xs[#3]: 為 X id 添加 Y #9725[01:30.074]
任務 foocorp.tasks.add_y_to_xs[#1] 在 0.0594762689434s 中成功:無[01:30.087]
任務 foocorp.tasks.add_y_to_xs[#3] 在 0.0352867960464s 中成功:無我們目前使用 Celery 3.1.4 和 RabbitMQ 作為傳輸。
編輯丹,這是我想出的:
丹,這是我最終使用的:
from sqlalchemy import func
from sqlalchemy.exc import DBAPIError
from contextlib import contextmanager
def _psql_advisory_lock_blocking(conn, lock_id, shared, timeout):
lock_fn = (func.pg_advisory_xact_lock_shared
if shared else
func.pg_advisory_xact_lock)
if timeout:
conn.execute(text('SET statement_timeout TO :timeout'),
timeout=timeout)
try:
conn.execute(select([lock_fn(lock_id)]))
except DBAPIError:
return False
return True
def _psql_advisory_lock_nonblocking(conn, lock_id, shared):
lock_fn = (func.pg_try_advisory_xact_lock_shared
if shared else
func.pg_try_advisory_xact_lock)
return conn.execute(select([lock_fn(lock_id)])).scalar()
class DatabaseLockFailed(Exception):
pass
@contextmanager
def db_lock(engine, name, shared=False, block=True, timeout=None):
"""
Context manager which acquires a PSQL advisory transaction lock with a
specified name.
"""
lock_id = hash(name)
with engine.begin() as conn, conn.begin():
if block:
locked = _psql_advisory_lock_blocking(conn, lock_id, shared,
timeout)
else:
locked = _psql_advisory_lock_nonblocking(conn, lock_id, shared)
if not locked:
raise DatabaseLockFailed()
yield
芹菜任務裝飾器(僅用於周期性任務):
from functools import wraps
from preo.extensions import db
def locked(name=None, block=True, timeout='1s'):
"""
Using a PostgreSQL advisory transaction lock, only runs this task if the
lock is available. Otherwise logs a message and returns `None`.
"""
def with_task(fn):
lock_id = name or 'celery:{}.{}'.format(fn.__module__, fn.__name__)
@wraps(fn)
def f(*args, **kwargs):
try:
with db_lock(db.engine, name=lock_id, block=block,
timeout=timeout):
return fn(*args, **kwargs)
except DatabaseLockFailed:
logger.error('Failed to get lock.')
return None
return f
return with_task
from functools import wraps
from celery import shared_task
def skip_if_running(f):
task_name = f'{f.__module__}.{f.__name__}'
@wraps(f)
def wrapped(self, *args, **kwargs):
workers = self.app.control.inspect().active()
for worker, tasks in workers.items():
for task in tasks:
if (task_name == task['name'] and
tuple(args) == tuple(task['args']) and
kwargs == task['kwargs'] and
self.request.id != task['id']):
print(f'task {task_name} ({args}, {kwargs}) is running on {worker}, skipping')
return None
return f(self, *args, **kwargs)
return wrapped
@shared_task(bind=True)
@skip_if_running
def test_single_task(self):
pass
test_single_task.delay()
我使用celery-once解決了這個問題,我將其擴展到celery-one 。
兩者都為您的問題服務。 它使用 Redis 來鎖定正在運行的任務。 celery-one
還將跟蹤正在鎖定的任務。
下面是 celery beat 的一個非常簡單的使用示例。 在下面的代碼中, slow_task
每 1 秒安排一次,但它的完成時間是 5 秒。 普通 celery 會每秒安排一次任務,即使它已經在運行。 celery-one
可以防止這種情況。
celery = Celery('test')
celery.conf.ONE_REDIS_URL = REDIS_URL
celery.conf.ONE_DEFAULT_TIMEOUT = 60 * 60
celery.conf.BROKER_URL = REDIS_URL
celery.conf.CELERY_RESULT_BACKEND = REDIS_URL
from datetime import timedelta
celery.conf.CELERYBEAT_SCHEDULE = {
'add-every-30-seconds': {
'task': 'tasks.slow_task',
'schedule': timedelta(seconds=1),
'args': (1,)
},
}
celery.conf.CELERY_TIMEZONE = 'UTC'
@celery.task(base=QueueOne, one_options={'fail': False})
def slow_task(a):
print("Running")
sleep(5)
return "Done " + str(a)
我嘗試編寫一個裝飾器來使用Postgres 咨詢鎖定,類似於 erydo 在他的評論中提到的。
它不是很漂亮,但似乎工作正常。 這是在 Python 2.7 下的 SQLAlchemy 0.9.7。
from functools import wraps
from sqlalchemy import select, func
from my_db_module import Session # SQLAlchemy ORM scoped_session
def pg_locked(key):
def decorator(f):
@wraps(f)
def wrapped(*args, **kw):
session = db.Session()
try:
acquired, = session.execute(select([func.pg_try_advisory_lock(key)])).fetchone()
if acquired:
return f(*args, **kw)
finally:
if acquired:
session.execute(select([func.pg_advisory_unlock(key)]))
return wrapped
return decorator
@app.task
@pg_locked(0xdeadbeef)
def singleton_task():
# only 1x this task can run at a time
pass
(歡迎對改進方法提出任何意見!)
需要分布式鎖定系統,因為那些 Celery beat 實例本質上是不同的進程,可能跨不同的主機。
ZooKeeper 和 etcd 等中心坐標系適用於分布式鎖系統的實現。
我推薦使用 etcd,它是輕量級和快速的。 鎖定在 etcd 上有幾種實現,例如:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.