生产者消费者模式

在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。

单单抽象出生产者和消费者,还够不上是生产者消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据,如下图所示:

 

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过消息队列(缓冲区)来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给消息队列,消费者不找生产者要数据,而是直接从消息队列里取,消息队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个消息队列就是用来给生产者和消费者解耦的。------------->这里又有一个问题,什么叫做解耦?

解耦:假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。

因为太抽象,看过网上的说明之后,通过我的理解,我举了个例子:吃包子。

假如你非常喜欢吃包子(吃起来根本停不下来),今天,你妈妈(生产者)在蒸包子,厨房有张桌子(缓冲区),你妈妈将蒸熟的包子盛在盘子(消息)里,然后放到桌子上,你正在看巴西奥运会,看到蒸熟的包子放在厨房桌子上的盘子里,你就把盘子取走,一边吃包子一边看奥运。在这个过程中,你和你妈妈使用同一个桌子放置盘子和取走盘子,这里桌子就是一个共享对象。生产者添加食物,消费者取走食物。桌子的好处是,你妈妈不用直接把盘子给你,只是负责把包子装在盘子里放到桌子上,如果桌子满了,就不再放了,等待。而且生产者还有其他事情要做,消费者吃包子比较慢,生产者不能一直等消费者吃完包子把盘子放回去再去生产,因为吃包子的人有很多,如果这期间你好朋友来了,和你一起吃包子,生产者不用关注是哪个消费者去桌子上拿盘子,而消费者只去关注桌子上有没有放盘子,如果有,就端过来吃盘子中的包子,没有的话就等待。对应关系如下图:

 
 

考察了一下,原来当初设计这个模式,主要就是用来处理并发问题的,而Celery就是一个用python写的并行分布式框架。

Celery的定义

Celery(芹菜)是一个简单、灵活且可靠的,处理大量消息的分布式系统,并且提供维护这样一个系统的必需工具。

我比较喜欢的一点是:Celery支持使用任务队列的方式在分布的机器、进程、线程上执行任务调度。然后我接着去理解什么是任务队列。

任务队列

任务队列是一种在线程或机器间分发任务的机制。

消息队列

消息队列的输入是工作的一个单元,称为任务,独立的职程(Worker)进程持续监视队列中是否有需要处理的新任务。

Celery 用消息通信,通常使用中间人(Broker)在客户端和职程间斡旋。这个过程从客户端向队列添加消息开始,之后中间人把消息派送给职程,职程对消息进行处理。如下图所示:

 
 

Celery 系统可包含多个职程和中间人,以此获得高可用性和横向扩展能力。

Celery的架构

Celery的架构由三部分组成,消息中间件(message broker),任务执行单元(worker)和任务执行结果存储(task result store)组成。

消息中间件

Celery本身不提供消息服务,但是可以方便的和第三方提供的消息中间件集成,包括,RabbitMQ,Redis,MongoDB等,这里我先去了解RabbitMQ,Redis

任务执行单元

Worker是Celery提供的任务执行的单元,worker并发的运行在分布式的系统节点中

任务结果存储

Task result store用来存储Worker执行的任务的结果,Celery支持以不同方式存储任务的结果,包括Redis,MongoDB,Django ORM,AMQP等,这里我先不去看它是如何存储的,就先选用Redis来存储任务执行结果。

然后我接着去安装Celery,在安装Celery之前,我已经在自己虚拟机上安装好了Python,版本是2.7,是为了更好的支持Celery的3.0以上的版本。

安装Redis,它的安装比较简单,如下:

$ sudo pip install redis

然后进行简单的配置,只需要设置 Redis 数据库的位置:

BROKER_URL = 'redis://localhost:6379/0'

之后安装Celery,我是用标准的Python工具pip安装的,如下:

$ sudo pip install celery

为了测试Celery能否工作,我运行了一个最简单的任务,编写tasks.py,如下图所示:

 
 

编辑保存退出后,我在当前目录下运行如下命令:

$ celery -A tasks worker --loglevel=info

查询文档,了解到该命令中-A参数表示的是Celery APP的名称,这个实例中指的就是tasks.py,后面的tasks就是APP的名称,worker是一个执行任务角色,后面的loglevel=info记录日志类型默认是info,这个命令启动了一个worker,用来执行程序中add这个加法任务(task)。

然后看到界面显示结果如下:

 
 

我们可以看到Celery正常工作在名称ubuntu的虚拟主机上,版本为3.1.23,在下面的[config]中我们可以看到当前APP的名称tasks,运输工具transport就是我们在程序中设置的中间人redis://127.0.0.1:6379/5,result我们没有设置,暂时显示为disabled,然后我们也可以看到worker缺省使用perfork来执行并发,当前并发数显示为1,然后可以看到下面的[queues]就是我们说的队列,当前默认的队列是celery,然后我们看到下面的[tasks]中有一个任务tasks.add.

了解了这些之后,根据文档我重新打开一个terminal,然后执行Python,进入Python交互界面,用delay()方法调用任务,执行如下操作:

 
 

这个任务已经由之前启动的Worker异步执行了,然后我打开之前启动的worker的控制台,对输出进行查看验证,结果如下:

 
 

绿色部分第一行说明worker收到了一个任务:tasks.add,这里我们和之前发送任务返回的AsyncResult对比我们发现,每个task都有一个唯一的ID,第二行说明了这个任务执行succeed,执行结果为12。

查看资料说调用任务后会返回一个AsyncResult实例,可用于检查任务的状态,等待任务完成或获取返回值(如果任务失败,则为异常和回溯)。但这个功能默认是不开启的,需要设置一个 Celery 的结果后端(backend),这块我在下一个例子中进行了学习。

通过这个例子后我对Celery有了初步的了解,然后我在这个例子的基础上去进一步的学习。

因为Celery是用Python编写的,所以为了让代码结构化一些,就像一个应用,我使用python包,创建了一个celery服务,命名为pj。文件目录如下:

 
 

celery.py

 
 
from __future __ import absolute_import

定义未来文件的绝对进口,而且绝对进口必须在每个模块的顶部启用。

from celery import Celery

从celery导入Celery的应用程序接口

App.config_from_object(‘pj.config’)

从config.py中导入配置文件

if __name__ == ‘__main__’:

app.start()

执行当前文件,运行celery

app = Celery(‘pj’,

broker=‘redis://localhost’,

backend=‘redis://localhost’,

include=[‘pj.tasks’]

)

首先创建了一个celery实例app,实例化的过程中,制定了任务名pj(与当前文件的名字相同),Celery的第一个参数是当前模块的名称,在这个例子中就是pj,后面的参数可以在这里直接指定,也可以写在配置文件中,我们可以调用config_from_object()来让Celery实例加载配置模块,我的例子中的配置文件起名为config.py,配置文件如下:

 
 

在配置文件中我们可以对任务的执行等进行管理,比如说我们可能有很多的任务,但是我希望有些优先级比较高的任务先被执行,而不希望先进先出的等待。那么需要引入一个队列的问题. 也就是说在我的broker的消息存储里面有一些队列,他们并行运行,但是worker只从对应 的队列里面取任务。在这里我们希望tasks.py中的add先被执行。task中我设置了两个任务:

所以我通过from celery import group引入group,用来创建并行执行的一组任务。然后这块现需要理解的就是这个@app.task,@符号在python中用作函数修饰符,到这块我又回头去看python的装饰器(在代码运行期间动态增加功能的方式)到底是如何实现的,在这里的作用就是通过task()装饰器在可调用的对象(app)上创建一个任务。

 
 

了解完装饰器后,我回过头去整理配置的问题,前面提到任务的优先级问题,在这个例子中如果我们想让add这个加法任务优先于subtract减法任务被执行,我们可以将两个任务放到不同的队列中,由我们决定先执行哪个任务,我们可以在配置文件中这样配置:

 
 

先了解了几个常用的参数的含义:

Exchange:交换机,决定了消息路由规则;

Queue:消息队列;

Channel:进行消息读写的通道;

Bind:绑定了Queue和Exchange,意即为符合什么样路由规则的消息,将会放置入哪一个消息队列

我将add这个函数任务放在了一个叫做for_add的队列里面,将subtract这个函数任务放在了一个叫做for_subtract的队列里面,然后我在当前应用目录下执行命令:

 
 

这个worker就只负责处理for_add这个队列的任务,执行这个任务:

 
 

任务已经被执行,我在worker控制台查看结果:

 
 

可以看到worker收到任务,并且执行了任务。

在这里我们还是在交互模式下手动去执行,我们想要crontab的定时生成和执行,我们可以用celery的beat去周期的生成任务和执行任务,在这个例子中我希望每10秒钟产生一个任务,然后去执行这个任务,我可以这样配置:

 
 

使用了scheduler,要制定时区: CELERY_TIMEZONE = 'Asia/Shanghai' ,启动celery加上-B的参数:

 
 

并且要在config.py中加入 from datetime import timedelta 。

更近一步,如果我希望在每周四的19点30分生成任务,分发任务,让worker取走执行,可以这样配置:

 
 

看完这些基础的东西,我回过头对celery在回顾了一下,用图把它的框架大致画出来,如下图:

 
 

使用模块配置

BROKER_URL = 'amqp://'                           broker设置
CELERY_RESULT_BACKEND = 'amqp://'              存储任务结果
CELERY_TASK_RESULT_EXPIRES = 18000         celery任务结果有效期  

CELERY_TASK_SERIALIZER = 'json'                 任务序列化结构
CELERY_RESULT_SERIALIZER = 'json'               结果序列化结构
CELERY_ACCEPT_CONTENT=['json']                  celery接收内容类型
CELERY_TIMEZONE = 'Asia/Shanghai'                  celery使用的时区
CELERY_ENABLE_UTC = True                          启动时区设置
CELERYD_LOG_FILE="/var/log/celery/celery.log"  celery日志存储位置
from kombu.common import Broadcast
CELERY_QUEUES = (Broadcast('broadcast_logger'), )   任务队列的类型
CELERY_ROUTES = {                                     任务队列
'log_analysis.run': {'queue': 'api.log'},
'logrotate': {'queue': 'broadcast_logger'},
}
CELERY_SEND_TASK_ERROR_EMAILS = True             celery接收错误邮件
ADMINS = (
    ("*****", "*****@***.com"),      celery接收错误邮件地址
)
SERVER_EMAIL = ****@***.com       从哪里发送的错误地址
EMAIL_HOST = "*.*.*.*"
EMAIL_PORT = 25
EMAIL_HOST_USER = SERVER_EMAIL
CELERYBEAT_SCHEDULE = {                                定期执行任务
# 接口中心每小时
'api.hour':{'task': 'api.hour', 'schedule': crontab(minute=15), 'args': ()},
# 接口中心每日
'api.day':{'task': 'api.day', 'schedule': crontab(minute=30, hour=0), 'args': ()},
}
celery = Celery()
celery.config_from_object('celeryconfig1')     celery配置文档

高级用法

1、group

from celery import group
>>> res = group(add.s(i, i) for i in xrange(10))()
>>> res.get(timeout=1)
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
是多个相同任务

2、chain

>>> from celery import chain
# 2 + 2 + 4 + 8
>>> res = chain(add.s(2, 2), add.s(4), add.s(8))()
>>> res.get()  

是一个任务  

3、chord

from celery import chord
res = chord((add.s(i, i) for i in xrange(10)), xsum.s())()
res.get()
90
多个不同任务,必须有backend配置,配置文件中增加CELERY_CHORD_PROPAGATES = True

celery队列

1、CELERY_QUEUES(定义celery队列)

from kombu import Queue
CELERY_DEFAULT_QUEUE = 'default'
CELERY_QUEUES = (
    Queue('default',    routing_key='task.#'),
    Queue('feed_tasks', routing_key='feed.#'),
)
CELERY_DEFAULT_EXCHANGE = 'tasks'
CELERY_DEFAULT_EXCHANGE_TYPE = 'topic'
CELERY_DEFAULT_ROUTING_KEY = 'task.default' 

2、CELERY_ROUTES(用来决定在任务哪个队列上执行)

CELERY_ROUTES = {
        'feeds.tasks.import_feed': {
            'queue': 'feed_tasks',
            'routing_key': 'feed.import',
        },
}  

3、只让队列单独工作: celery worker -Q feed_tasks

celery学习笔记1的更多相关文章

  1. celery 学习笔记 01-介绍

    celery 学习笔记 01-介绍 celery 是 python 中的常用的任务队列框架,经常用于异步调用.后台任务等工作.celery 本身以 python 写,但协议可在不同的语言中实现,其它语 ...

  2. Celery学习笔记

    转载请注明出处:点我 我的第一篇博客!嘿嘿! 在公司实习,接触到的第一个项目就用到了Celery,之前是完全没有接触过Celery这玩意,然后花了点时间仔细的研究了下怎么用.在学习过程中也遇到了些问题 ...

  3. celery学习笔记2

    1.定义: Celery是一个异步的任务队列(也叫做分布式任务队列) 2.工作结构 Celery分为3个部分 (1)worker部分负责任务的处理,即工作进程(我的理解工作进程就是你写的python代 ...

  4. Swift学习笔记一

    最近计划把Swift语言系统学习一下,然后将MagViewer用这种新语言重构一次,并且优化一下,这里记录一下Swift的学习笔记. Swift和Objective-C相比,在语法和书写形式上做了很多 ...

  5. Knockout.js快速学习笔记

    原创纯手写快速学习笔记(对官方文档的二手理解),更推荐有时间的话读官方文档 框架简介(Knockout版本:3.4.1 ) Knockout(以下简称KO)是一个MVVM(Model-View-Vie ...

  6. 【目录】Python学习笔记

    目录:Python学习笔记 目标:坚持每天学习,每周一篇博文 1. Python学习笔记 - day1 - 概述及安装 2.Python学习笔记 - day2 - PyCharm的基本使用 3.Pyt ...

  7. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  8. PHP-自定义模板-学习笔记

    1.  开始 这几天,看了李炎恢老师的<PHP第二季度视频>中的“章节7:创建TPL自定义模板”,做一个学习笔记,通过绘制架构图.UML类图和思维导图,来对加深理解. 2.  整体架构图 ...

  9. PHP-会员登录与注册例子解析-学习笔记

    1.开始 最近开始学习李炎恢老师的<PHP第二季度视频>中的“章节5:使用OOP注册会员”,做一个学习笔记,通过绘制基本页面流程和UML类图,来对加深理解. 2.基本页面流程 3.通过UM ...

随机推荐

  1. LeetCode之旅(19)-Power of Two

    题目 Given an integer, write a function to determine if it is a power of two. Credits: Special thanks ...

  2. Which SQL statement is the trump card to the senior software developer

    Which SQL statement is the trump card to the senior software developer                    MA Genfeng ...

  3. Java中使用有返回值的线程

    在创建多线程程序的时候,我们常实现Runnable接口,Runnable没有返回值,要想获得返回值,Java5提供了一个新的接口Callable,可以获取线程中的返回值,但是获取线程的返回值的时候,需 ...

  4. 途牛java实习面试(失败)

    一进去让自己介绍.简单介绍了一下.然后让我自己说说框架.问题太大一紧张卡住了. 然后面试官开始问,让我介绍多线程,我就简单介绍了多线程.然后问我有没有做过多线程的项目,我说没有. 问了MySQL的锁和 ...

  5. MySQL的日志(一)

    本文目录:1.日志刷新操作2.错误日志3.一般查询日志4.慢查询日志5.二进制日志 5.1 二进制日志文件 5.2 查看二进制日志 5.2.1 mysqlbinlog 5.2.2 show binar ...

  6. Day19 Django

    老师代码博客: http://www.cnblogs.com/yuanchenqi/articles/7552333.html 上节内容回顾: class Book(models.Model): ti ...

  7. eclipse web开发Server配置

    用 Tomcat 和 Eclipse 开发 Web 应用程序:http://www.ibm.com/developerworks/cn/opensource/os-eclipse-tomcat/ Ec ...

  8. pg_dump命令帮助信息

    仅为参考查阅方便,完全命令行帮助信息,无阅读价值. pg_dump dumps a database as a text file or to other formats. Usage:  pg_du ...

  9. Top Open Source Projects to Watch in 2017

    https://opensource.com/article/16/12/yearbook-projects-watch-2017 No one has a crystal ball to see t ...

  10. No module named zope.interface error的解决

    明明安装了 zope.interface,还是出现标题错误,错误语句是 from zope.interface import ooxx 根据 http://stackoverflow.com/ques ...