RPC通信原理(未完,先睡觉)
一 背景
OpenStack 各组件之间是通过 REST 接口进行相互通信,比如Nova、Cinder、Neutron、Glance直间的通信都是通过keystone获取目标的endpoint,即api(至于到底什么是restful风格的api请点击红色链接)
而各组件内部则采用了基于 AMQP 模型的 RPC 通信。
为了让大家有更为直观的认识,我们单拿出cinder的架构来举例,至于cinder内部包含的具体组件,大家不必过于纠结,我们在后续的章节会详细介绍,这里只需要有一个直观的体会:rest和rpc分别在openstack的组件间和组件内通信所处位置。
1.Cinder-api 是 cinder 服务的 endpoint,提供 rest 接口,负责处理 client 请求,并将 RPC 请求发送至 cinder-scheduler 组件。 2. Cinder-scheduler 负责 cinder 请求调度,其核心部分就是 scheduler_driver, 作为 scheduler manager 的 driver,负责 cinder-volume 具体的调度处理,发送 cinder RPC 请求到选择的 cinder-volume。 3. Cinder-volume 负责具体的 volume 请求处理,由不同后端存储提供 volume 存储空间。 目前各大存储厂商已经积极地将存储产品的 driver 贡献到 cinder 社区。目前支持的后端存储系统,可参见:https://wiki.Openstack.org/wiki/CinderSupportMatrix
(注意:glance的内部组件的通信是直接调用的自己的api,除此之外,cinder,nova,neutron的内部组件通信都是基于rpc机制)
二 为何
二 OpenStack RPC通信
Openstack 组件内部的 RPC(Remote Producer Call)机制的实现是基于 AMQP(Advanced Message Queuing Protocol)作为通讯模型,从而满足组件内部的松耦合性。AMQP 是用于异步消息通讯的消息中间件协议,AMQP 模型有四个重要的角色:
- Exchange:根据 Routing key 转发消息到对应的 Message Queue 中
- Routing key:用于 Exchange 判断哪些消息需要发送对应的 Message Queue
- Publisher:消息发送者,将消息发送的 Exchange 并指明 Routing Key,以便 Message Queue 可以正确的收到消息
- Consumer:消息接受者,从 Message Queue 获取消息 (收邮件的人)
消息发布者 Publisher 将 Message 发送给 Exchange 并且说明 Routing Key。Exchange 负责根据 Message 的 Routing Key 进行路由,将 Message 正确地转发给相应的 Message Queue。监听在 Message Queue 上的 Consumer 将会从 Queue 中读取消息。
Routing Key 是 Exchange 转发信息的依据,因此每个消息都有一个 Routing Key 表明可以接受消息的目的地址,而每个 Message Queue 都可以通过将自己想要接收的 Routing Key 告诉 Exchange 进行 binding,这样 Exchange 就可以将消息正确地转发给相应的 Message Queue。
图 AMQP 消息模型
Publisher(发邮件的人)
Exchange(相当于邮局)
Message Queue(相当于自己家门口的邮筒)
Routing Key(自己的家门口邮筒都需要在邮局注册/binding)
Consumer(收邮件的人)
AMQP 定义了三种类型的 Exchange,不同类型 Exchange 实现不同的 routing 算法:
- Direct Exchange:Point-to-Point 消息模式,消息点对点的通信模式,Direct Exchange 根据 Routing Key 进行精确匹配,只有对应的 Message Queue 会接受到消息
- Topic Exchange:Publish-Subscribe(Pub-sub)消息模式,Topic Exchange 根据 Routing Key 进行模式匹配,只要符合模式匹配的 Message Queue 都会收到消息
- Fanout Exchange:广播消息模式,Fanout Exchange 将消息转发到所有绑定的 Message Queue
OpenStack 目前支持的基于 AMQP 模型的 RPC backend 有 RabbitMQ、QPid、ZeroMQ,对应的具体实现模块在 cinder 项目下 Openstack/common/RPC/目录下,impl_*.py 分别为对应的不同 backend 的实现。其中RabbitMQ是最常用的一个。
三 RabbitMQ详解
RabbitMQ是一个消息代理。它的主要原理相当简单:接收并且转发消息。
你可以把它想象成一个邮局:当你把你的邮箱放到油筒里时,肯定有邮递员帮你把它分发给你的接收者。RabiitMQ同时是:一个邮筒、一个邮局,一个邮递员。
RabbitMQ和邮局的主要不同是RabbitMQ处理的不是纸质,它接收、存储并且分发二进制数据(即消息)
3.1 RabbitMQ的常用术语如下
producer(生产者):即一个生产消息的程序,只负责生产(sending),简称"P"
queue(队列):即一个队列就是一个邮筒的名字,它集成在RabbitMQ内部,虽然消息流通过RabbitMQ和你的应用程序,但其实消息可以只存储在一个队列里。队列不受任何限制,它可以如你所愿存放很多消息,它本质上就是一个无限的缓冲区。许多生产者producer可以发消息到一个队列,许多消费者consumer可以尝试从一个队列queue里接收数据。
consumer(消费者):即一个接等待接收消息的程序,简称“C”
注意:producer,consumer,以及broker(即消息代理,指的就是RabbitMQ)不是必须要在同一台机器上,实际上,大多数情况下,三者是分布在不同的机器上
3.2 最简单的用法:'Hello World'
RabbitMQ libraries
RabbitMQ遵循AMQP 0.9.1,AMQP是开源的,通用的消息协议,RabbitMQ支持很多不同语言的客户端,作为python用户,我们使用Pika模块,可以使用pip工具安装之,目前pika最新版本为0.10.0。
这里举一个很简单的例子,发送一个消息,接收它并且打印到屏幕。我们需要做的是:写两个程序,一个负责发消息,另外一个负责接收消息并且打印,如下图
=============发送端=============
我们的第一个程序名send.py,用来发送一个消息到队列queue中
第一步:第一件我们需要做的事情就是与RabbitMQ服务建立链接
#!/usr/bin/env python import pika connection=pika.BlockingConnection(pika.ConnectionParameters( host='localhost', port=5672, virtual_host='/', #虚拟主机,起到一个命名空间的作用 credentials=pika.PlainCredentials('admin','admin') #用户名,密码 )) channel=connection.channel()
现在我们可以连接RabbitMQ服务了(broker),只不过此时连接的是本机的,如果RabbitMQ位于远程主机,那么host="远程主机ip"
第二步:接下来,我们需要确定接收消息的队列是存在的,如果我们发送一条消息给一个不存在的队列,RabbitMQ将把这条消息当做垃圾处理。因此就让我们为将要分发的消息创建一个队列,我们可以将该队列命名为hello
channel.queue_declare(queue='hello')
第三步:此时,我们就已经为发送消息做好准备了,我们的第一个消息就只包含一个字符串“Hello world”吧,并且我们将这条消息发送到hello队列。
需要强调的一点是:在RabbitMQ中,任何消息都不可能直接发送到队列queue,消息必须先发送给exchange,后面我们会详细介绍exchane,此处不必细究,我们只需要知道如何使用默认的exchange就可以了,即定义参数exchange='',空代表使用默认的。exchange允许我们明确地标识消息应该发往哪个队列,对列名需要通过routing_key这个参数被定义。
channel.basic_publish(exchange='', routing_key='hello', body='Hello World!') print(" [x] Sent 'Hello World!'")
第四步:在退出这个程序之前,我们需要确定网络缓存被刷新并且我们的消息真的被传给了RabbitMQ,因此我们需要优雅地退出这个连接(这种退出方式可以确定缓存刷新和消息传到了RabbitMQ)
connection.close()
注意: 如果执行send.py后没有看到'Sent'这条打印内容,可能的原因是RabbitMQ的启动盘没有足够的剩余空间(默认情况,安装RabbitMQ的磁盘最小需要1Gb的剩余空间),没有足够的剩余空间就会拒绝接收消息。我们可以坚持RabbitMQ的日志文件来减少这种空间的限制,点击来查看如何设置disk_free_limit
=============接收端=============
我们的第二个程序receive.py将从队列中接收消息并且打印
第一步:同样的,首先我们需要做的也是连接RabbitMQ,负责连接RabbitMQ的代码和send.py中的一样。
第二步:这一步需要做的也要确定队列的存在,我们可以执行多次queue_declare,但是无论执行多少次,将只创建一次队列
channel.queue_declare(queue='hello') #这个操作是幂等的
你肯定会问:为什么我们要再次声明队列呢,我们已经在send.py中声明过一次了啊,没错,如果你能确定队列已经存在了,完全没有必要重新再定义一次,比如send.py先运行了,那么队列肯定是存在的。
但问题的所在就在于,我们根本无法确定,到底是send.py先运行还是receive.py先运行,因此最保险的做法就是在两个程序中都定义上这一条,反正是如果队列已经有了就不会重新创建。
查看RabbitMQ有多少条消息,(授权用户)可以使用rabbitmqctl tool:
$ sudo rabbitmqctl list_queues
Listing queues ...
hello 0
...done.
第三步:比起发消息来说,从队列中收消息是更加复杂一点,它订阅一个callback函数到一个队列,一旦收到消息,这个callback函数就会被(Pika库)调用。此处我们就写一个简单的callback函数(只完成打印功能)
def callback(ch, method, properties, body): print(" [x] Received %r" % body)
第四步:我们需要告诉RabbitMQ,这个特殊的callback函数应该从我们的队列接收消息。
channel.basic_consume(callback, queue='hello', no_ack=True)
要想让上面这条命令正确执行,我们必须保证队列我们订阅的队列是存在的,幸运的是,我们很有信心,因为我们已经在上面创建了一个队列‒使用queue_declare。
no_ack参数将在后面描述
第五步:最后,我们进入一个无休止的循环,等待数据,并且在必要时运行回调callback
print(' [*] Waiting for messages. To exit press CTRL+C') channel.start_consuming()
=============完整版=============
send.py
#python AMQP SDK import pika #获得连接对象 connection=pika.BlockingConnection(pika.ConnectionParameters( host='localhost', port=5672, virtual_host='/', #虚拟主机,起到一个命名空间的作用 credentials=pika.PlainCredentials('admin','admin') #用户名,密码 )) #连接rabbitmq channel=connection.channel() channel.queue_declare(queue='hello') #创建队列hello channel.basic_publish(exchange='', routing_key='hello', body='Hello World') print(" [x] Sent 'Hello World!'") connection.close()
receive.py
import pika connection=pika.BlockingConnection(pika.ConnectionParameters( host='localhost', port=5672, virtual_host='/', #虚拟主机,起到一个命名空间的作用 credentials=pika.PlainCredentials('admin','admin') #用户名,密码 )) channel=connection.channel() channel.queue_declare(queue='hello') def callback(ch, method, properties, body): print(" [x] Received %r" % body) channel.basic_consume(callback, queue='hello', no_ack=True) print(' [*] Waiting for messages. To exit press CTRL+C') channel.start_consuming()
=============测试=============
运行send.py发消息
$ python send.py [x] Sent 'Hello World!'
producer程序,即send.py每次运行完毕后都会停止,我们可以接收信息,即运行receive.py
$ python receive.py [*] Waiting for messages. To exit press CTRL+C [x] Received 'Hello World!'
此时reveive.py不会退出,我们可以在其他终端开启send.py来发消息,然后观察receive.py收消息
3.3 Work Queues
使用pika 0.10.0 python客户端
在3.2小节我们编写了程序:从一个被命名的队列中send和receive消息。本节,我们将创建一个Work队列,它将被用来在多个wokers中分发耗费时间的任务。
Work队列(又称为任务队列)背后的主要思想是为了避免立即执行一个资源密集型任务(耗时),并等待它完成。相反,我们不会等它的完成,我们会调度它之后要完成的任务。我们将任务封装为消息并将其发送给队列。后台运行的worker进程将弹出任务并最终执行任务。当您运行许多worker矜持的时候,这些任务将在它们之间共享。
这个概念在web应用领域非常有用,比如web应用不能在一个短HTTP请求窗口期间处理一个复杂的任务
=============发送端=============
在3.2小节,我们发送一个消息“Hello World”,现在我们将发送代表复杂任务的字符串(用来模拟耗时的任务,即将揭晓)。我们没有一个真实的任务,如图像进行调整或PDF文件被渲染,所以让我们通过time.sleep()函数伪造一个耗时的任务,比如,一个伪造的任务被描述成Hello...,该任务将花费三秒钟(几个.就花费几秒钟)。
我们稍微修改下3.2小节中send.py的代码,来允许任意的数据能通过命令行被发送,这个程序就将调度任务给work队列,程序的文件名new_task.py
import sys message = ' '.join(sys.argv[1:]) or "Hello World!" channel.basic_publish(exchange='', routing_key='task_queue', body=message, properties=pika.BasicProperties( delivery_mode = 2, # make message persistent )) print(" [x] Sent %r" % message)
=============接收端=============
同样我们在3.2小节定义的receive.py也需要做一些修改:消息体中包含几个点,它就需要伪造执行几秒钟。它将从队列中弹出消息并且执行,文件名worker.py
import time def callback(ch, method, properties, body): print(" [x] Received %r" % body) time.sleep(body.count(b'.')) print(" [x] Done")
=============round-robin调度=============
new_task.py
#_*_coding:utf-8_*_ #!/usr/bin/env python import pika import sys #获得连接对象 connection=pika.BlockingConnection(pika.ConnectionParameters( host='192.168.31.106', port=5672, virtual_host='/', #虚拟主机,起到一个命名空间的作用 credentials=pika.PlainCredentials('admin','admin') #用户名,密码 )) #连接rabbitmq channel=connection.channel() channel.queue_declare(queue='task_queue') #创建队列hello message=''.join(sys.argv[1:]) or 'Hello World' channel.basic_publish(exchange='', routing_key='task_queue', body=message, properties=pika.BasicProperties( delivery_mode=2, #make message persistent )) print(" [x] Sent %r" % message) connection.close()
worker.py(建立多个这种文件)
#_*_coding:utf-8_*_ #!/usr/bin/env python import pika import time connection=pika.BlockingConnection(pika.ConnectionParameters( host='192.168.31.106', port=5672, virtual_host='/', #虚拟主机,起到一个命名空间的作用 credentials=pika.PlainCredentials('admin','admin') #用户名,密码 )) channel=connection.channel() channel.queue_declare(queue='task_queue') def callback(ch, method, properties, body): print(" [x] Received %r" % body) time.sleep(body.count(b'.')) print(" [x] Done") channel.basic_consume(callback, queue='task_queue', no_ack=True) print(' [*] Waiting for messages. To exit press CTRL+C') channel.start_consuming()
同时启动多个worker.py
shell1$ python worker.py [*] Waiting for messages. To exit press CTRL+C shell2$ python worker.py [*] Waiting for messages. To exit press CTRL+C
多次运行new_tasks.py
shell3$ python new_task.py First message.
shell3$ python new_task.py Second message..
shell3$ python new_task.py Third message...
shell3$ python new_task.py Fourth message....
shell3$ python new_task.py Fifth message.....
查看workers,即worker.py的执行结果
shell1$ python worker.py [*] Waiting for messages. To exit press CTRL+C [x] Received 'First message.' [x] Received 'Third message...' [x] Received 'Fifth message.....' shell2$ python worker.py [*] Waiting for messages. To exit press CTRL+C [x] Received 'Second message..' [x] Received 'Fourth message....'
结论:默认,rabbitmq将发送每个消息到下一个consumer(即worker.py),按顺序一个个的来。平均每个consumer将得到相同数量的消息。这种分发消息的方式叫做round-robin。
=============Message acknowledgment消息确认 =============
处理一个任务可能需要花费几秒钟,你肯定会好奇,如果consumers中的一个(即woker.py),在开始一个耗时很长的任务但是还没来得及完成任务的情况下就死掉了,那应该怎么办。就我们当前的代码而言,RabbitMQ一旦发消息分发给consumer了,它就会立即从内存中移除这条消息。这种情况下,如果你杀死了一个woker(比如worker.py,我们将丢失这条正在处理的消息),我们也将丢失发送给该特定woker但尚未处理的所有信息。
很明显,我们并不想丢失任何消息/任务,如果一个woker死了,我们希望把这个任务分发给另外一个woker。
为了确定消息永不丢失,RabbitMQ支持消息确认( message acknowledgments)
consumer返回一个ack给RabbitMQ,告知RabbitMQ一条特定的消息已经被接收了、执行完了、并且RabbitMQ可以自由删除该任务/消息了
如果一个consumer死了(它的channel是挂壁了,连接是关闭了,或者TCP连接丢失)并且没有发送ack,RabbitMQ将会知道一个消息没有被完全执行完毕,并且将该消息重新放入队列中。这种方式你可以确认没有消息会丢失 。
这里没有任何消息超时;当consumer死掉以后,RabbitMQ将重新分发这个消息。即使执行一个消息需要花很长很长的时间,这种方式仍然是好的处理方式。
消息确认(Message acknowledgments)默认是被打开的,在前面的例子里,我们明确地将其关闭掉了,通过no_ack=True.移除这条配置那么就默认开启了。
def callback(ch, method, properties, body): print " [x] Received %r" % (body,) time.sleep( body.count('.') ) print " [x] Done" ch.basic_ack(delivery_tag = method.delivery_tag) #发送ack channel.basic_consume(callback, queue='hello') #去掉no_ack=True,默认就是False
使用这种代码,我们可以确定是否你杀死了一个worker(当它正在处理一个消息的时候通过CTRL+C杀死它),没有消息会丢失,没返回ack就死掉的worker,不久后消息就会被重新分发
注意:一个经常性的错误是,callback函数中缺少back_ack,它是一个简单的工作,但是结果是五花八门的,当你的客户端退出后,消息将会重新分发,rabbitmq将占用越来越多的内存由于它不能够释任何没有ack回应(unacked)的消息
为了调试这种错误,你可以使用rabbitmqctl来打印messages_unacknowledged
$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
hello 0 0
...done.
=============Message durability消息持久化 =============
我们已经学习了如何确定该
=============Fair dispatch合理调度 =============
3.2小节我们介绍了
3.4
3.5
四 参考文档
http://www.rabbitmq.com/tutorials/tutorial-one-python.html
https://www.ibm.com/developerworks/cn/cloud/library/1403_renmm_opestackrpc/