从消息队列看OpenStack

By | 2021年12月23日

本文这次将换一个角度,从消息队列的角度来看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组件的众多功能中,创建虚拟机功能应该是最能够说明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中不会使用,因此不用关注。

nova exchange

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

图中第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服务进程。

通过前面观察rabbitmq中的exchange以及队列等信息,我们可以画出生产者、消息队列、消费者之间的简要关系。

图中最左边部分画出了集群中部分nova服务进程,此处这些nova服务进程的作用是作为生产者向rabbitmq发送消息;中间灰色线框部分是rabbitmq;最右边部分也是集群中的nova服务进程,它们和左边的生产者实际上是相同的进程,只不过在此处它们作为消费者接收并处理指定队列中的消息。(nova组件中的服务即是生产者,也是消费者)

以上一章节中提到的虚拟机启动为例,根据这里的消息队列模型再看一下虚拟机的启动流程,按照上图红色部分从左向右。

首先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 topicRPC_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调用的简要流程可以通过下面这个图简要的表示出来

fanout exchange
以fanout结尾的exchange的作用是对所有相关的服务进行广播,以nova-scheduler服务为例,当有多个nova-scheduler服务进程时,每个nova-scheduler进程都会生成一个队列并绑定到scheduler_fanout exchange上。

在通过这个scheduler_fanout进行消息广播时,所有的nova-scheduler进程都将接收到消息。下图是在一个控制节点上启动了3个nova-scheduler进程时,与scheduler_fanout exchange绑定的队列

使用广播给服务发送消息的方式,在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进程时的scheduer_fanout exchange及其绑定的队列信息。

从图中可以看到,每个nova-scheduler服务都会有一个队列连接到scheduler_fanout exchange上。因此nova-api在进行广播消息时,每个scheduler_fanout_<uuid>队列里面都将收到消息,所有的nova-scheduler服务进程都能够处理消息。

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服务进程消费,如下图所示

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服务出现了异常。

参考资料

1、https://www.rabbitmq.com/getstarted.html

2、https://opendev.org/openstack/openstack-helm/src/branch/master/nova/templates/bin/_health-probe.py.tpl

发表评论

您的电子邮箱地址不会被公开。