Celery作为一个分布式任务框架,提供了events对外进行状态和信息传递,而程序运行过程的中的数据是较为关键的。
事件关注点
对于事件,有以下关注点
- 事件是如何产生的
- 事件是如何传递的
- 事件是如何捕获的
对于项目结构,有以下关注点
- 对象之间的关系
- 实现的是否耦合,是否可插拔
evnet sender
Celery内置的事件的类型根据使用者的不同大致可分为Worker/Task的事件。从worker的事件看起。
先看个自己写的demo:
def event_sender():
# use to send events
with app.events.default_dispatcher() as d:
d.send('task-result', msg='i am cute result {}'.format(datetime.datetime.now()), name='gin')
d.flush()
dispatcher作为事件的分发器,并且由于dispatcher内部实现了上下文管理器,所以这里直接就用with来进行初始化了(上下文管理器非常方便~实名推荐一波)。这里发送的事件task-result是我们自定义的一个类型,用于和内置类型区分开,方便观察结果。在这里先实例化了一个dispatcher,然后调用了dispatcher.send方法。这里我们的broker使用redis,所以我们看一下redis的dispatcher的send方法。
def send(self, type, blind=False, utcoffset=utcoffset, retry=False,
retry_policy=None, Event=Event, **fields):
if self.enabled:
groups, group = self.groups, group_from(type)
if groups and group not in groups:
return
if group in self.buffer_group:
clock = self.clock.forward()
event = Event(type, hostname=self.hostname,
utcoffset=utcoffset(),
pid=self.pid, clock=clock, **fields)
buf = self._group_buffer[group]
buf.append(event)
if len(buf) >= self.buffer_limit:
self.flush()
elif self.on_send_buffered:
self.on_send_buffered()
else:
return self.publish(type, fields, self.producer, blind=blind,
Event=Event, retry=retry,
retry_policy=retry_policy)
前面根据type分组然后判断flush or store in the buffer,然后用参数实例化events,这里做的比较人性化的一点是可传入自定义的fields来初始化Events,因为作为事件来说,通常有自定义的数据的需求,而这样处理就比较优雅了。然后发送者发送整个事件,也就意味着所有发送者希望发送的消息能够被完整的传递。with mutex,用了一个线程的互斥锁,然后进行了publish, 然后是这里最为关键是publish,看看publish的底层实现
from kombu import Producer
def _publish(self, event, producer, routing_key, retry=False,
retry_policy=None, utcoffset=utcoffset):
exchange = self.exchange
try:
res = producer.publish(
event,
routing_key=routing_key,
exchange=exchange.name,
retry=retry,
retry_policy=retry_policy,
declare=[exchange],
serializer=self.serializer,
headers=self.headers,
delivery_mode=self.delivery_mode,
)
except Exception as exc: # pylint: disable=broad-except
if not self.buffer_while_offline:
raise
self._outbound_buffer.append((event, routing_key, exc))
def enable(self):
self.producer = Producer(self.channel or self.connection,
exchange=self.exchange,
serializer=self.serializer,
auto_declare=False)
self.enabled = True
for callback in self.on_enabled:
callback()
主要逻辑是调用了self.producer.publish,而self.producer是在dispatcher的enable里创建的,可以看到enable用了kombu的producer,kombu是celery’内部对于amqp的封装,用于实现消息传递,其支持了各种的broker,比如说redis,rabbitmq。鉴于咱们使用的是redis的broker,所以也很明显这里使用redis为broker的pub/sub模式。所以就很清楚啦,celery events的实现是基于redis的pub/sub模式,这也解释了为什么在笔者测试的时候,没有消息的存储,以及events在开启监听以后才能够收到。
event receiver
最后贴一个自己写的事件监听。
def monitor_events():
def on_event(event):
# this is the callback when events come in
print('[recv] {} '.format(event))
with app.connection() as conn:
recv = app.events.Receiver(conn, handlers={'task-result':
on_event})
recv.capture(limit=None, timeout=None, wakeup=True)
events使用场景
基于celery内置的事件,可以对于task的执行状态信息,和执行结果信息进行实时处理,例如可以将所有的事件执行结果进行暂存然后结合时间序列数据库类似influxdb和展示平台类似grafana进行展示,这样就有了比较完善的一套结果数据的流程。
也可以自定义事件,对一些关注的信息进行实时处理。例如在task执行过程中的信息收集,可以通过events来完成。
总结
所以对于上面我们提出的问题,有以下总结
- 事件是如何产生的:事件由事件的发送者的初始化,并且发送,在celery里事件的发送往往伴随着状态的改变,例如对于 task的事件中包括 发送,被接收,被执行,执行成功,执行失败,重试。
- 事件是如何传递的:如果采用redis作为broker,那么事件是基于redis的pub/sub模式而传递的。
- 事件是如何捕获的:同上,基于订阅模式,事件得以被接收。
对于对象的关系呢
在这一部分我们接触到的对象有
app,dispatcher,events,publisher,receiver
其中app作为拉起整个celery项目的核心对象,events对象被挂载到app上,而dispatcher和receiver则被挂载到了events上。dispatcher和receiver借由kombu的底层实现无关性,直接传入不同的connection uri实例化kombu的message queue。整个结构还是比较清晰的,并且面向接口编程也少了很多的耦合。总的来说,还是非常值得借鉴的一种写法。
参考
- Monitoring and Management Guide — Celery 3.1.7 文档
- Kombu Documentation — Kombu 4.6.0 documentation
- 立强的博客