[译]Dask vs Celery


原文链接:Dask and Celery

本文比较了两个Python分布式任务处理系统Dask.distributed和Celery。

免责声明:对比技术真的不好把握。我比较偏向于Dask,从而可能Celery的正确实践方法不了解。请记住这一点。欢迎Celery专家的批评指正。

Celery是用Python构建的分布式任务队列中间件,Python社区中大量的项目使用它来处理基于任务的工作。

Dask则是一个在PyData社区很受欢迎的并行计算库,它衍生出了一个相当成熟的分布式任务调度中间件。这篇文章探讨了Dask.distributed能否解决Celery式的问题。

比较技术项目很困难,一方面是作为一名Dask作者,我的意见会有一些偏见,另一方面是因为这俩项目的代码规模相当大。这使得两边作者倾向于在官网上展示各自的优势的功能,而对于差异或弱点闭口不提。幸运的是,一位Celery用户在Dask的Github上提了issue:Celery user asked how Dask compares on Github讨论dask和celery的不同,他们列出了一些具体的不同点:

  1. 多任务队列(Handling multiple queues)
  2. 任务调度图(Canvas (celery’s workflow))
  3. 频率限制(Rate limiting)
  4. 重试机制(Retrying)

这为我们提供了一个从Celery开发人员的角度,而不是Dask开发人员的角度,来对比Dask/Celery的机会。

我将在这篇文章中,对几个大的不同点进行划重点。然后在用Dask/Celery两种方式实现Celery的HelloWorld程序,然后讨论在Dask中实现或没能实现的功能。这样比较应该给我们一个大致的结论。

最大的不同:worker的状态和通讯

首先,最大的区别(从我的观点来看)是,Dask的worker会保留中间结果,并互相交流数据,而在Celery里,所有的结果都会流回中心化的后端(backend)。特别是Celery在构建大规模并行化的array和dataframe (Dask原生支持的两种数据结构)时,这种差异非常关键,因为这需要占用工作进程的内存和工作进程间通信带宽。像Dask这样的计算系统需要保留中间结果,而像Celery/Airflow/Luigi这样的数据工程系统则不能。这就是为什么最初Dask不是建立在Celery/Airflow/Luigi之上的主要原因。

其实这并不能撬动Celery/Airflow/Luigi的地位。通常情况下,它们被用于无关紧要的环境中,它们将精力集中在Dask同样不关心或做得很好的几个特性上。任务通常从一些可全局访问的存储中间件(例如数据库或AWS的S3)读取数据,然后返回非常小的结果,或者将较大的结果放回全局存储中间件中。

我现在想的问题是,Dask对于传统的松散任务调度问题是一个有用的解决方案吗?优点和缺点是什么?

Hello World

首先,我们将用Celery来进行第一步的演练,分别用Celery和Dask的实现方式来进行比较:

Celery

我将使用Celery 的快速开始教程,使用Redis而不是RabbitMQ作为中间人,因为碰巧我手头有Redis。

定义任务:

# tasks.py

from celery import Celery

app = Celery('tasks', broker='redis://localhost', backend='redis')

@app.task
def add(x, y):
    return x + y

启动服务端与消息队列(redis):

$ redis-server
$ celery -A tasks worker --loglevel=info

客户端调用:

In [1]: from tasks import add

In [2]: %time add.delay(1, 1).get()  # submit and retrieve roundtrip
CPU times: user 60 ms, sys: 8 ms, total: 68 ms
Wall time: 567 ms
Out[2]: 2

In [3]: %%time
...: futures = [add.delay(i, i) for i in range(1000)]
...: results = [f.get() for f in futures]
...:
CPU times: user 888 ms, sys: 72 ms, total: 960 ms
Wall time: 1.7 s
Dask

我们用dask实现一遍。采用dask.distributed 的concurrent.futures 接口,使用运行模式采用默认的single-machine部署。

In [1]: from distributed import Client

In [2]: c = Client()

In [3]: from operator import add

In [4]: %time c.submit(add, 1, 1).result()
CPU times: user 20 ms, sys: 0 ns, total: 20 ms
Wall time: 20.7 ms
Out[4]: 2

In [5]: %%time
...: futures = [c.submit(add, i, i) for i in range(1000)]
...: results = c.gather(futures)
...:
CPU times: user 328 ms, sys: 12 ms, total: 340 ms
Wall time: 369 ms
结果比较
  • 功能: 在Celery中,你需要提前在服务里定义并注册任务(Demo中的add函数)。如果已知的任务(比如分析一个系统的工作负载,任务基本固定),并且不希望允许用户在集群上随意修改任务从而造成安全隐患,那么Celery的设计时可以满足需求的。但对于想要不断尝试修改任务的用户来说,这就不爽了(任务没办法动态改)。在Dask中,我们选择在用户端运行函数,而不是在服务器端。这在数据探索中是非常关键的,但在更保守/安全的计算环境中是一个障碍。
  • 安装环境: 在Celery中,我们依赖于其他广泛部署的系统,如RabbitMQ或Redis。Dask依赖于底层的Torando TCP IOStreams和Dask自己的自定义路由逻辑。这使得Dask的设置变得琐碎,但也可能不那么耐用。Redis和RabbitMQ都解决了很多这些乱七八糟问题,学习这些消息队列你可以信心。
  • 性能: 它们的运行都有亚秒延迟和毫秒级的开销。Dask的开销略低,但对于数据工程工作负载而言,这个级别的差异很少显著。Dask是一个低延迟的数量级,这可能取决于您的应用程序。例如,如果你在用户点击网站上的按钮时启动任务,20ms通常在交互预期之内,而500ms感觉有点慢。

简单任务依赖关系

本节主要讨论Celery的任务依赖关系模块Canvas
通常任务取决于其他任务的结果。这Celery/Dask都提供了帮助用户表达这些任务依赖关系的方法。

Celery

apply_async方法有一个link参数,可用于在其他任务运行后调用任务。例如,我们可以计算(1 + 2)+ 3,在Celery实现如下:

add.apply_async((1, 2), link=add.s(3))

Dask.distributed

借助Dask concurrent.futures API,加法功能可以在submit调用中使用,并且依赖是隐式的。

x = c.submit(add, 1, 2)
y = c.submit(add, x, 3)

我们还可以使用dask.delayed修饰符来注释任意函数,然后使用标准Python。

对比

主观上来说,我觉得Dask的解决比较优雅。

复杂依赖关系

用Celery和Dask实现 (i= 0 ~99):

Σ ( i + i ) \Sigma (i+i) Σ(i+i)

Celery

Celery包括了许多丰富的预防与属于来连接更复杂的任务,像:chains, chords, maps, starmaps等等。更多细节可以看Celery Canvas的文档。

In [1]: from tasks import add, tsum  
# 求和操作要提前注册到服务端,所以此处不得不定义一个tsum来实现求和并注册到celery
In [2]: from celery import chord

In [3]: %time chord(add.s(i, i) for i in range(100))(tsum.s()).get()
CPU times: user 172 ms, sys: 12 ms, total: 184 ms
Wall time: 1.21 s
Out[3]: 9900

Dask.distributed

In [4]: %%time
...: futures = [c.submit(add, i, i) for i in range(100)]
...: total = c.submit(sum, futures)
...: total.result()
...:
CPU times: user 52 ms, sys: 0 ns, total: 52 ms
Wall time: 60.8 ms

或者在py文件中使用Dask.delayed做异步任务

futures = [add(i, i) for i in range(100)]
total = dask.delayed(sum)(futures)
total.result()

多任务队列

Celery中是使用队列的概念(准确的说交中间人),任务可以提交到队列中,Worker可以订阅队列。一个场景是“high priority”(高优先级)的Worker只消费“high priority”队列中的任务。每个工作进程都可以订阅高优先级队列,但某些工作进程将以独占方式订阅该队列:

celery -A my-project worker -Q high-priority  # only subscribe to high priority
celery -A my-project worker -Q celery,high-priority  # subscribe to both
celery -A my-project worker -Q celery,high-priority
celery -A my-project worker -Q celery,high-priority

Dask有几个概念与之类似,或者可以在紧要关头满足这个需求,但并没有和Celery完全类似的概念。

首先,对于上述常见情况,任务具有优先级。这些通常由Dask的scheduler设置,以最小化内存使用,但可以由用户直接覆盖,让某些任务优先于其他任务。

其次,还可以限制任务在Worker子集上运行。它最初是为数据本地存储系统(如Hadoop文件系统(HDFS))或具有特殊硬件(如gpu)的集群而设计的,但也可以用于队列情况。它不是完全相同的抽象,但可以用来在紧要关头获得相同的结果。对于每个任务,您可以限制运行它的工作池。

Dask解决方案参考文档:http://distributed.readthedocs.io/en/latest/locality.html#user-control

任务重试

Celery允许任务失败的时候,自行重试。

@app.task(bind=True)
def send_twitter_status(self, oauth, tweet):
    try:
        twitter = Twitter(oauth)
        twitter.update_status(tweet)
    except (Twitter.FailWhaleError, Twitter.LoginError) as exc:
        raise self.retry(exc=exc)

# Example from http://docs.celeryproject.org/en/latest/userguide/tasks.html#retrying

不幸的是Dask目前不具备这个功能。所有的函数被认为是纯业务功能。如果任务失败了,返回真实的异常。这一点可以用户自己在函数里实现这个功能

def send_twitter_status(self, oauth, tweet, n_retries=5):
    for i in range(n_retries):
        try:
            twitter = Twitter(oauth)
            twitter.update_status(tweet)
            return
        except (Twitter.FailWhaleError, Twitter.LoginError) as exc:
            pass

频率限制

Celery可以让您指定任务的速率限制,这可以帮助避免由于阻塞导致的外部api受阻

@app.task(rate_limit='1000/h')
def query_external_api(...):
    ...

Dask没有这功能,也没打算加。因为,这可以很容易地在外部对Dask进行。例如,Dask支持在任意Python队列上映射函数。如果发送队列,则该队列中的所有当前和未来元素都将被映射。通过限制输入队列的速率,您可以在客户端轻松处理纯Python中的速率限制。Dask的低延迟和开销使得在客户端管理这样的逻辑相当容易。虽然不方便,但还是很简单。

>>> from queue import Queue

>>> q = Queue()

>>> out = c.map(query_external_api, q)
>>> type(out)
Queue

总结

基于对Celery的肤浅的理解,我(作者)会愚蠢地宣称,如果您不深入到深层API,Dask可以干Celery干的事。然而,所有这些深层API实际上都非常重要。Celery在这一领域不断发展,并形成了大量的功能,解决了一次又一次出现的问题。这段历史为用户节省了大量时间。Dask在一个非常不同的空间进化,并发展了一套非常不同的方法。dask的许多技巧都很一般,只要花一点功夫就能解决Celery的问题,但可能需要一些额外的代码实现。我现在看到人们把这种努力用在解决问题上,我想看看结果会很有趣。

通过Celeyr API对我(作者)个人来说是一个很好的经验。我认为Celery中有一些好的概念可以为Dask未来的发展提供参考。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值