多线程和gevent如何提高celery性能及稳定性

2019-09-30 作者:网络时代   |   浏览(72)

有朋友问我,我那个任务队列是怎么实现,他的疑问其实主要是celery不支持多线程。先说说我那实现的方法,其实我的做法和celery、rq这样的框架很像的,都是把任务push到队列里面,然后pull取出任务而已,celery里面还可以取任务,我这个是通过传送uuid来实现的。   朋友问celery不支持多线程,那是他没有好好看文档。celery是支持多任务并发的,哎。。。 好好看文档呀。

1 关于greenlet

平台首页 1


队列存储brokers用的是rabbitmq,后面测试下用mongodb搞搞。我这里做个测试:

greelet指的是使用一个任务调度器和一些生成器或者协程实现协作式用户空间多线程的一种伪并发机制,即所谓的微线程。

下面是tasks.py文件,也就是celery能支持异步的函数。

greelet机制的主要思想是:生成器函数或者协程函数中的yield语句挂起函数的执行,直到稍后使用next()或send()操作进行恢复为止。可以使用一个调度器循环在一组生成器函数之间协作多个任务。

 代码如下

网络框架的几种基本的网络I/O模型:

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

阻塞式单线程:这是最基本的I/O模型,只有在处理完一个请求之后才会处理下一个请求。它的缺点是效能差,如果有请求阻塞住,会让服务无法继续接受请求。但是这种模型编写代码相对简单,在应对访问量不大的情况时是非常适合的。

后端启动含有100个线程的线程池。

阻塞式多线程:针对于单线程接受请求量有限的缺点,一个很自然的想法就是给每一个请求开一个线程去处理。这样做的好处是能够接受更多的请求,缺点是在线程产生到一定数量之后,进程之间需要大量进行切换上下文的操作,会占用CPU大量的时间,不过这样处理的话编写代码的难道稍高于单进程的情况。

 代码如下

非阻塞式事件驱动:为了解决多线程的问题,有一种做法是利用一个循环来检查是否有网络IO的事件发生,以便决定如何来进行处理(reactor设计模式)。这样的做的好处是进一步降低了CPU的资源消耗。缺点是这样做会让程序难以编写,因为请求接受后的处理过程由reactor来决定,使得程序的执行流程难以把握。当接受到一个请求后如果涉及到阻塞的操作,这个请求的处理就会停下来去接受另一个请求,程序执行的流程不会像线性程序那样直观。twisted框架就是应用这种IO模型的典型例子。

celery -A tasks worker  -c 100 --loglevel=info

非阻塞式Coroutine(协程):这个模式是为了解决事件驱动模型执行流程不直观的问题,它在本质上也是事件驱动的,加入了Coroutine的概念。

在ipython测试的结果:

2 与线程/进程的区别

平台首页 2


看看我自己输出的日志,结果很明显,是并发的:

线程是抢占式的调度,多个线程并行执行,抢占共同的系统资源;而微线程是协同式的调度。

平台首页 3

其实greenlet不是一种真正的并发机制,而是在同一线程内,在不同函数的执行代码块之间切换,实施“你运行一会、我运行一会”,并且在进行切换时必须指定何时切换以及切换到哪。greenlet的接口是比较简单易用的,但是使用greenlet时的思考方式与其他并发方案存在一定区别:

celery是支持好几个并发模式的,有prefork,threading,协程(gevent,eventlet)

1. 线程/进程模型在大逻辑上通常从并发角度开始考虑,把能够并行处理的并且值得并行处理的任务分离出来,在不同的线程/进程下运行,然后考虑分离过程可能造成哪些互斥、冲突问题,将互斥的资源加锁保护来保证并发处理的正确性。

prefork在celery的介绍是,用了multiprocess来实现的。多线程就补多少了,估计大家都懂。

2. greenlet则是要求从避免阻塞的角度来进行开发,当出现阻塞时,就显式切换到另一段没有被阻塞的代码段执行,直到原先的阻塞状况消失以后,再人工切换回原来的代码段继续处理。因此,greenlet本质是一种合理安排了的 串行 。

说说协程,进程 线程经常玩,也算熟悉,话说协程算是一种轻量级进程,但又不能叫进程,因为操作系统并不知道它的存在。什么意思呢,就是说,协程像是一种在程序级别来模拟系统级别的进程,由于是单进程,并且少了上下文切换,于是相对来说系统消耗很少,而且网上的各种测试也表明,协程确实拥有惊人的速度。并且在实现过程中,协程可以用以前同步思路的写法,而运行起来确是异步的,也确实很有意思。话说有一种说法就是说进化历程是多进程->多线程->异步->协程,当然协程也有弊端,但是如果你的任务类型不是那种cpu密集的,那选用协程是个好选择。

3. greenlet本质是串行,因此在没有进行显式切换时,代码的其他部分是无法被执行到的,如果要避免代码长时间占用运算资源造成程序假死,那么还是要将greenlet与线程/进程机制结合使用(每个线程、进程下都可以建立多个greenlet,但是跨线程/进程时greenlet之间无法切换或通讯)。

但是需要说明的是,虽然celery官网提示说,只要在启动worker的时候,指明下类型就行了,但是如果你逻辑里面的模块有些不支持协程 gevent或者是eventlet异步的话,他还是会堵塞的。

3 使用

gevent1.x之后虽然是支持subprocess的用法,gevent这个模块给非堵塞了,和他有同样功能的os.popen('sleep 10').read() 是会堵塞的,据说gevent官方不支持popen的协程的用法。


看了下celery 针对gevent方面的调用,他其实就是引入了gevent的patch  。 那这样会造成堵塞的问题,如果gevent不支持这些模块,那。。。。

一个 “greenlet” 是一个很小的独立微线程。可以把它想像成一个堆栈帧,栈底是初始调用,而栈顶是当前greenlet的暂停位置。你使用greenlet创建一堆这样的堆栈,然后在他们之间跳转执行。跳转不是绝对的:一个greenlet必须选择跳转到选择好的另一个greenlet,这会让前一个挂起,而后一个恢复。两 个greenlet之间的跳转称为 切换(switch) 。

 代码如下

当你创建一个greenlet,它得到一个初始化过的空堆栈;当你第一次切换到它,他会启动指定的函数,然后切换跳出greenlet。当最终栈底 函数结束时,greenlet的堆栈又编程空的了,而greenlet也就死掉了。greenlet也会因为一个未捕捉的异常死掉。

from gevent import monkey
monkey.patch_all()

示例:来自官方文档示例

反之threading的用法倒是简单明了,支持把任务放在线程pool里面来处理。

from greenlet import greenlet 
def test1(): 
   print 12 
   gr2.switch() 
   print 34 
def test2(): 
   print 56 
   gr1.switch() 
   print 78 
gr1 = greenlet(test1) 
gr2 = greenlet(test2) 
gr1.switch()

话说回来,我的title为什么说gevent来提高性能。我和小伙伴做一些gevent支持的模块写函数,做多任务处理的时候,性能确实要比threading要高,还要稳定。

最后一行跳转到 test1() ,它打印12,然后跳转到 test2() ,打印56,然后跳转回 test1() ,打印34,然后 test1() 就结束,gr1死掉。这时执行会回到原来的 gr1.switch() 调用。注意,78是不会被打印的,因为gr1已死,不会再切换。


小计:

4 基于greenlet的框架

清理celery产生的数据


 代码如下

平台首页,4.1 eventlet

#清空
celery purge
#清空
from celery.task.control import discard_all
discard_all()


查看celery rabbitmq队列信息

eventlet 是基于 greenlet 实现的面向网络应用的并发处理框架,提供“线程”池、队列等与其他 Python 线程、进程模型非常相似的 api,并且提供了对 Python 发行版自带库及其他模块的超轻量并发适应性调整方法,比直接使用 greenlet 要方便得多。

 代码如下

其基本原理是调整 Python 的 socket 调用,当发生阻塞时则切换到其他 greenlet 执行,这样来保证资源的有效利用。需要注意的是:

rabbitmqctl list_queues

  • eventlet 提供的函数只能对 Python 代码中的 socket 调用进行处理,而不能对模块的 C 语言部分的 socket 调用进行修改。对后者这类模块,仍然需要把调用模块的代码封装在 Python 标准线程调用中,之后利用 eventlet 提供的适配器实现 eventlet 与标准线程之间的协作。

  • 虽然 eventlet 把 api 封装成了非常类似标准线程库的形式,但两者的实际并发执行流程仍然有明显区别。在没有出现 I/O 阻塞时,除非显式声明,否则当前正在执行的 eventlet 永远不会把 cpu 交给其他的 eventlet,而标准线程则是无论是否出现阻塞,总是由所有线程一起争夺运行资源。所有 eventlet 对 I/O 阻塞无关的大运算量耗时操作基本没有什么帮助。

 

4.2 gevent


4.2.1 gevent是一个基于协程(coroutine)的Python网络函数库,通过使用greenlet提供了一个在libev事件循环顶部的高级别并发API。

主要特性有以下几点:

本文由澳门新葡亰网址大全发布于网络时代,转载请注明出处:多线程和gevent如何提高celery性能及稳定性

关键词: