首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从消息队列看OpenStack

从消息队列看OpenStack

原创
作者头像
jiang
发布2021-02-23 14:25:17
1K0
发布2021-02-23 14:25:17
举报
文章被收录于专栏:yuncoderyuncoder

以往介绍openstack的文章通常都是从各个组件的整体角度来进行介绍,并没有深入的介绍组件内部服务究竟是如何通信的。本文这次将换一个角度,从消息队列的角度来看openstack。文章将以pike版本中的nova组件为例进行介绍,由于openstack中所有组件内部服务的通信方式都是一致的,因此下面的内容也同样适用于其它组件,如neutron、cinder等。

Nova整体架构

下面这个图只画出了nova组件中最核心的4个服务,即nova-api、nova-conductor、nova-scheduler和nova-compute。服务之间通过消息队列,即图中的mq进行通信(这里的mq几乎默认都是rabbitmq)。

其中api、conductor、scheduler服务都可以配置多进程、多副本以实现服务的高可用和高并发,而compute服务的数量则可能多达上千个。

nova-arch.png
nova-arch.png

在nova组件的众多功能中,创建虚拟机功能应该是最能够说明nova组件内部协作的功能了。创建虚拟机时,nova-api服务接收到来自用户的http请求,在进行一些必要的处理之后,通过消息队列将创建流程转交给nova-conductor,之后nova-api会给用户返回响应,而不会等待虚拟机创建完成,虚拟机创建将进入后台运行阶段。

nova-conductor服务从消息队列中收到虚拟机创建请求后,将会进入一个长时间的虚拟机创建流程。首先nova-conductor将会通过消息队列调用nova-scheduler,为虚拟机选择一个可用的计算节点;之后又会通过消息队列将创建请求发送给nova-compute服务。

nova-compute服务在收到虚拟机创建请求后,会执行一系列的虚拟机创建操作,其中还包括更新数据库。但更新数据库并不是由nova-compute自己实现,而是会通过消息队列将更新数据库操作委托给nova-conductor,由nova-conductor代理完成。

以上就是虚拟机创建流程的一个简要说明,从创建流程中可以看到,消息队列对于openstack至关重要。再举一个虚拟机启动的例子,启动虚拟机时nova-api服务将收到来自用户的http请求,之后nova-api将会通过消息队列将虚拟机启动请求发送给虚拟机所在的计算节点,对应计算节点上的nova-compute服务将会收到启动请求,并将指定的虚拟机启动。

但有时候可能会遇到这样的问题,就是通过nova service-list命令看到某个计算节点上的nova-compute服务明明是up的(这表明计算节点上的nova-compute服务是正常运行的,同时还能够正常的上报数据到nova数据库中),但是执行虚拟机启动操作时却没有任何效果,观察nova-compute服务日志找不到任何相关的记录,同时虚拟机卡在启动状态中。对于此类问题,仅仅通过前面的介绍是无法知道根本原因的,必须要进入到消息队列层面才能够明白为什么会发生这类问题。

从MQ来看Nova

注: 在openstack中,默认使用的消息队列是rabbitmq,因此下面的内容全部基于rabbitmq,关于rabbitmq的基础知识可以在官方文档https://www.rabbitmq.com/getstarted.html中找到。

打开rabbitmq management页面,在Exchanges标签页下面可以看到很多的rabbitmq exchange,如下图所示(由于篇幅限制,图中只过滤显示了部分exchange)。其中与openstack相关的exchange主要分为3类:

  • 以nova、neutron、openstack等命名的exchange;
  • 以reply开头的exchange;
  • 以fanout结尾的exchange;

下面会依次说明这3类exchange的作用。另外还有一些以amq开头的exchange,这些是rabbitmq默认的exchange,在openstack中不会使用,因此不用关注。

mq-exchanges.png
mq-exchanges.png

nova exchange

以组件名称命名的exchange是各个组件内部服务之间通信的核心。下面这个图显示了一个controller节点(控制+计算融合节点)和一个单独的compute节点组成的openstack环境中nova exchange的具体内容。

nova-exchange.png
nova-exchange.png

图中第1部分定义了当前exchange的名称,不同的openstack项目,其exchange的名字不同,但通常和项目的名称一致(cinder默认的exchange名称为openstack,可以通过cinder.conf进行修改;但对于nova和neutron这两个项目,则都是在代码中写死的)。对于nova项目,可以在nova/config.py中找到默认的exchange名称定义。

第2部分指明了nova exchange的type为topic类型(即主题交换机)。rabbitmq支持3种类型的交换机,分别是direct、fanout和topic,关于这三种交换机类型可以从rabbitmq的官方文档中找到详细的说明,在后面的内容中也将简要的说明这几种交换机的特点。

第3部分则展示了当前连接到nova exchange上的所有队列。其中To所在列表示当前连接到nova交换机的所有队列名称,Routing key则指明了nova交换机与指定队列之间的关联关系。当客户端发送消息给nova exchanges时:

  • 如果指定了路由关键字为compute.controller,则消息将被发送到compute.controller队列中;
  • 如果指定了路由关键字为scheduler.controller,则消息将被发送到scheduler.controller队列中;

还可以点击图中的队列名称,进一步查看队列的详细信息,下图是scheduler.controller队列的部分信息,其中包括当前在队列中的消息数量,向队列中添加消息的速率以及消费速率等信息。红框部分则表明了当前连接到队列的消费者,该消费者来自192.168.60.211节点,对应40054端口的进程。通过命令ss -pn | grep 40054可以看到该进程实际上就是nova-scheduler服务进程。

schedule-queue.png
schedule-queue.png

通过前面观察rabbitmq中的exchange以及队列等信息,我们可以画出生产者、消息队列、消费者之间的简要关系。图中最左边部分画出了集群中部分nova服务进程,此处这些nova服务进程的作用是作为生产者向rabbitmq发送消息;中间灰色线框部分是rabbitmq;最右边部分也是集群中的nova服务进程,它们和左边的生产者实际上是相同的进程,只不过在此处它们作为消费者接收并处理指定队列中的消息。(nova组件中的服务即是生产者,也是消费者)

nova-mq-arch.png
nova-mq-arch.png

以上一章节中提到的虚拟机启动为例,根据这里的消息队列模型再看一下虚拟机的启动流程,按照上图红色部分从左向右。首先controller节点上的nova-api服务进程收到来自用户的虚拟机启动请求;nova-api查询到虚拟机位于计算节点compute上,因此构造rpc请求消息,将消息发送给nova exchange,并指定routing keycompute.compute;消息将根据路由键被发送到compute.compute队列中;最终绑定并消费该队列的nova-compute服务(计算节点compute上的进程)将获取到消息,并调用相应的函数执行虚拟机开机操作。相关的rpc调用代码可以在nova/compute/rpcapi.py中找到

# 1. nova-compute服务默认的rpc topic
RPC_TOPIC = "compute"

@profiler.trace_cls("rpc")
class ComputeAPI(object):
    def __init__(self):
        super(ComputeAPI, self).__init__()
        # 2. 所有nova-compute服务都默认将消息发送到compute topic
        target = messaging.Target(topic=RPC_TOPIC, version='4.0')
        #...

    # 3. 启动虚拟机涉及的rpc调用方法
    def start_instance(self, ctxt, instance):
        version = '4.0'
        # 4. _compute_host(None, instance)将会返回虚拟机所在计算节点主机名,因此最终消息将会发送
        # 给compute.<compute_hostname>队列,如果不指定server参数,则消息将被发送给compute队列
        cctxt = self.router.client(ctxt).prepare(
                server=_compute_host(None, instance), version=version)
        # 5. 此处cast表明是异步rpc调用,即只是将消息发送给nova-compute服务,不等待计算节点执行完成
        cctxt.cast(ctxt, 'start_instance', instance=instance)

replay exchange

在前面虚拟机启动相关的rpc调用函数中提到cctxt.cast方法是用于异步rpc调用的,即不会等待被调用方执行完成。在openstack中,还有另外一种rpc调用,即同步rpc调用,对应的方法为cctxt.call,该方法被执行后,将会等待被调用方执行完成。下面的代码同样来自nova/compute/rpcapi.py文件,该方法用于获取计算节点的运行时间。

# 获取计算节点的运行时间
def get_host_uptime(self, ctxt, host):
    version = '4.0'
    # 通过server参数,指定将rpc调用请求发送给哪个队列,相应的计算节点将会收到消息并处理
    cctxt = self.router.client(ctxt).prepare(
            server=host, version=version)
    # 同步rpc调用`cctxt.call`会等待被调用方执行完成并返回结果
    return cctxt.call(ctxt, 'get_host_uptime')

将查询主机运行时间的rpc消息发送给指定的计算节点,这一过程与前面一节是完全一样的。不同点在于同步rpc调用与异步rpc调用,同步rpc调用由于需要获取远端方法的执行结果,因此需要有一种方法能够将远端方法的执行结果返回给调用者。关于这一过程的实现原理,可以参考rabbitmq的官方文档: https://www.rabbitmq.com/tutorials/tutorial-six-python.html

用通俗易懂的方式来说就是,同步rpc调用时,客户端在发送给服务端的请求中,还会附加一个队列的名字,该队列用于告诉服务端,在方法执行完成后将执行结果发送到我给你的队列里面。而客户端在发送了rpc调用请求后,则会一直监听用于返回结果的队列,直到有结果返回或者响应超时。(在返回结果时,原来的服务端变成了消息的生产者,客户端变成了消息的消费者。)

在这里提到的用于返回函数执行结果的队列,就是那些以reply开头的队列,后面跟着一个随机生成的uuid。这些队列不是绑定到nova exchange上的,而是为这些reply队列创建了同名的exchange,这些exchange的类型为direct类型。并且在服务第一次调用call方法时会生成该队列,之后在服务重启之前会一直使用该队列作为reply队列。至此,同步rpc调用的简要流程可以通过下面这个图简要的表示出来

reply_queue.png
reply_queue.png

fanout exchange

以fanout结尾的exchange的作用是对所有相关的服务进行广播,以nova-scheduler服务为例,当有多个nova-scheduler服务进程时,每个nova-scheduler进程都会生成一个队列并绑定到scheduler_fanout exchange上。在通过这个scheduler_fanout进行消息广播时,所有的nova-scheduler进程都将接收到消息。下图是在一个控制节点上启动了3个nova-scheduler进程时,与scheduler_fanout exchange绑定的队列

fanout_exchange.png
fanout_exchange.png

使用广播给服务发送消息的方式,在nova中主要用于通知nova-scheduler服务更新缓存信息,比如通知所有的nova-scheduler服务进程更新主机可用域信息。在用户调用nova-api接口修改主机所在可用域的时候,nova-api服务就会通过广播的方式将计算节点的可用域信息广播给所有的nova-scheduler服务进程,使得nova-scheduler服务能够及时的更新内存中缓存的可用域信息,以便于正确的完成虚拟机调度。下面的代码来自nova/scheduler/rpcapi.py文件,其作用就是通过广播的方式通知所有nova-scheduler服务进程完成内存数据的更新

def update_aggregates(self, ctxt, aggregates):
    # 通过fanout参数指定操作为广播操作
    cctxt = self.client.prepare(fanout=True, version='4.1')
    cctxt.cast(ctxt, 'update_aggregates', aggregates=aggregates)

这里同样使用一个图来简要的表示一下fanout exchange与nova服务之间的关系。下面这个图展示了在有两个控制节点,且每个控制节点上都有两个nova-scheduler进程时的scheduerfanout exchange及其绑定的队列信息。从图中可以看到,每个nova-scheduler服务都会有一个队列连接到scheduler_fanout exchange上。因此nova-api在进行广播消息时,每个scheduler_fanout\<uuid>队列里面都将收到消息,所有的nova-scheduler服务进程都能够处理消息。

fanout_queue.png
fanout_queue.png

Nova高可用

nova组件的高可用分为两种,一种是以暴露端口对外提供http调用的服务,比较典型的是nova-api服务,另外还有像placement服务和nova-novncproxy服务;第二种就是像nova-scheduler、nova-conductor这样的服务,这些服务是通过消息队列来接收和处理请求的。

对于nova-api这样通过http对外提供接口的服务,高可用可以借助keepalived+haproxy这样的组合来完成服务的高可用和横向扩展。但本文的主要目的是从MQ来看openstack,因此nova-api这样的服务的高可用并不是本文的重点,这里想要介绍的是nova-scheduler、nova-conductor这些服务的高可用和横向扩展是如何实现的。下面将以nova-scheduler服务为例进行介绍。

nova-scheduler服务是用于虚拟机调度的组件,如果仅在一台主机上进行部署,则很容易出现单点故障。在实际的部署中,通常会将其部署在3台不同的物理主机上,以实现服务的高可用,同时还能提高虚拟机调度的并发性能(python进程cpu使用率不能超过100%,对于以计算为主的nova-scheduler服务,会严重限制其并发处理性能,必须通过多进程的方式实现高并发)。在前面介绍nova exchange时提到有一个scheduler队列,该队列会被所有的nova-scheduler服务进程消费,如下图所示

scheduler-ha.png
scheduler-ha.png

controller01~03节点上的nova-scheduler服务进程都会消费scheduler队列,当有消息被发送到scheduler队列中时,将会由一个进程获取到该消息并进行处理。当controller02节点上的nova-scheduler服务发送异常时,消息将会由controller01或controller03节点上的nova-scheduler服务消费。这就是nova-scheduler服务的高可用实现,同时由于有3个nova-scheduler进程在同时消费scheduler队列中的数据,因此消息的处理速度也得到了很大的提升,从而提升了虚拟机调度的并发性能。下面的代码来自nova/scheduler/rpcapi.py,其功能就是通过rpc同步调用nova-scheduler完成虚拟机的调度

# 1. 通过rpc同步调用nova-scheduler完成虚拟机调度,通常由nova-conductor服务发起调用
def select_destinations(self, ctxt, spec_obj, instance_uuids):
    version = '4.4'
    #...
    # 2. 注意这里prepare方法没有指定server参数,因此消息将被发送给scheduler队列。
    # 如果指定了server=controller01,则消息将被发送给scheduler.controller01队列,此时
    # 虚拟机调度请求消息将只能被controller01节点上的nova-scheduler服务进程获取并处理。
    cctxt = self.client.prepare(version=version)
    # 3. rpc同步调用,等待虚拟机调度结果
    return cctxt.call(ctxt, 'select_destinations', **msg_args)

Nova健康检查

最后介绍一下如何去判断nova服务是否在正常运行。同nova高可用部分一样,这里对nova的健康检查也分为两种情况,一种是提供http接口的服务,这类服务可以简单地通过curl等命令进行服务状态检查,比如通过curl命令访问8774端口,然后检查一下返回的状态码即可知道nova-api服务是否正常运行;另外一种就是nova-scheduler、nova-conductor这样的内部服务。

对于通过消息队列才能访问到的nova-schduler等服务来说,是没有办法通过curl命令检查其健康状态的。要检查这些服务的健康状态,需要发送rpc同步调用请求,如果目的节点上的指定服务能够正常响应,则说明对应节点上的nova服务运行正常。下面以检测计算节点compute01上的nova-compute服务为例进行说明。

根据nova exchange部分的说明可以知道,compute01节点上的nova-compute服务将会消费两个队列,一个是compute队列,另外一个是compute.compute01队列。由于compute队列会被所有的nova-compute服务消费,所以如果将消息发送给compute队列(即prepare方法不指定server参数),则消息可能被任意一个nova-compute服务进程消费,即使要检测的nova-compute服务已经无法正常功能,检测仍然会成功。因此在发送消息时,必须指定server参数为目的计算节点compute01,此时消息将被发送到compute.compute01队列中,同时该队列仅被compute01节点上的nova-compute服务消费,如果此时该服务异常,则消息将不会被消费,直到客户端等待响应超时,就可以知道compute01节点上的nova-compute服务出现了异常。

参考资料

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Nova整体架构
  • 从MQ来看Nova
    • nova exchange
      • replay exchange
        • fanout exchange
        • Nova高可用
        • Nova健康检查
        相关产品与服务
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档