虚拟机创建流程-nova-compute篇

达芬奇密码2018-07-19 12:50
虚拟机创建主要流程
下图展示了虚拟机创建的主要流程,可以看到整个流程当中会涉及到多个组件,nova这边涉及到的核心组件是nova-api、nova-scheduler、nova-conductor以及nova-compute。

上述nova组件主要作用是:
  • nova-api :是一个WSGI App,通过HTTP协议对外提供REST API。无论是从openstack的命令行还是dashboard 界面,只要是虚拟机的操作请求最终都会发给nova-api服务。nova-api这个服务通过HTTP协议对外提供REST API。
  • nova-scheduler :根据特定策略来为某个请求筛选出一个合适的 nova-compute 来完成服务。
  • nova-compute : 直接操作虚拟机的服务,它接到请求之后,通过整合 libvirt, openvswitch/bridge, rbd/iscsi 等的操作来达成请求。
  • nova-conductor : nova-compute 之上的一个新服务层。一者它为 nova-compute 代理数据库的操作,即 nova-compute 不直接操作数据库,而是通过 nova-conductor 的 rpc 方法来读写;二者相对于一些启动、关闭、暂停、唤醒虚拟机等的简单操作,像迁移、重建虚拟机等复杂虚拟机操作,会经由 nova-conductor 来协调 nova-compute 共同完成。
当用户通过前端页面或者通过Nova命令行创建一台虚拟机的时候,就会产生一个HTTP请求,nova-api首先对请求进行处理,包括和keystone进行交互对用户身份进行检查、传入的参数的检查、用户配额检查等等,然后通过RPC调用将创建虚主机的请求(包括一系列虚拟机的规格参数)推送到消息队列当中,nova-scheduler监听消息队列,从中取出创建虚拟机的请求,并根据对应的参数(包括虚拟机的VCPU数量、内存大小等)来过滤计算节点,并对通过过滤的计算节点计算权重,从中选择一个最合适的计算节点,通过RPC调用将创建虚拟机的请求发送到消息队列中,相应的计算节点最终通过libvirt API来完成虚拟机的创建。
这里主要分析下nova-api的启动大致过程,主要是WSGI App加载以及路由的映射。
启动代码
# nova.cmd.api:main
def main():
    config.parse_args(sys.argv)
    logging.setup(CONF, "nova")
    utils.monkey_patch()
    objects.register_all()
    log = logging.getLogger(__name__)

    gmr.TextGuruMeditation.setup_autorun(version)

    launcher = service.process_launcher()
    started = 0
    for api in CONF.enabled_apis: # 默认是'osapi_compute', 'metadata'
        should_use_ssl = api in CONF.enabled_ssl_apis
        try:
            # 初始化一个nova.services.WSGIService类对象
            server = service.WSGIService(api, use_ssl=should_use_ssl)
            # 启动wsgi服务
            launcher.launch_service(server, workers=server.workers or 1)
            started += 1
        except exception.PasteAppNotFound as ex:
            log.warning(
                _LW("%s. ``enabled_apis`` includes bad values. "
                    "Fix to remove this warning."), six.text_type(ex))

    if started == 0:
        log.error(_LE('No APIs were started. '
                      'Check the enabled_apis config option.'))
        sys.exit(1)

    launcher.wait()
上述代码主要做了两件事情,初始化一个nova.services.WSGIServiced的类对象,然后启动它。注意到对于每一个可能的api,有对应的启动脚本,如osapi_compute,其对应的启动脚本为nova.cmd.api_osapi_compute.py,代码都是类似的。主要看下WSGIService的初始化过程。
# nova.services.WSGISerivce
class WSGIService(object):
    """Provides ability to launch API from a 'paste' configuration."""

    def __init__(self, name, loader=None, use_ssl=False, max_url_len=None):
        """Initialize, but do not start the WSGI server.

        :param name: The name of the WSGI server given to the loader.
        :param loader: Loads the WSGI application using the given name.
        :returns: None

        """
        self.name = name # 'osapi_compute', 'metadata', 'ec2'中的一个
        self.manager = self._get_manager() # 加载一个子类,默认为空
        self.loader = loader or wsgi.Loader() # 封装下paste.deploy的loadapp方法
        self.app = self.loader.load_app(name) # 加载指定的APP
        self.host = getattr(CONF, '%s_listen' % name, "0.0.0.0") # host ip地址
        self.port = getattr(CONF, '%s_listen_port' % name, 0) # 监听的端口
        self.workers = getattr(CONF, '%s_workers' % name, None) # api-worker的数量
        self.use_ssl = use_ssl
        # 初始化一个nova.wsgi.server类对象
        self.server = wsgi.Server(name,
                                  self.app,
                                  host=self.host,
                                  port=self.port,
                                  use_ssl=self.use_ssl,
                                  max_url_len=max_url_len)
        # Pull back actual port used
        self.port = self.server.port
        self.backdoor_port = None
主要是对一些参数做了初始化,包括服务的名称、要监听的地址和端口,比较重要的两件事是,1)通过paste.deploy.loadapp来加载WSGI App;2)用上述参数初始化一个nova.wsgi.server类对象。
nova.wsgi.server
class Server(object):
    """Server class to manage a WSGI server, serving a WSGI application."""

    default_pool_size = 1000

    def __init__(self, name, app, host='0.0.0.0', port=0, pool_size=None,
                       protocol=eventlet.wsgi.HttpProtocol, backlog=128,
                       use_ssl=False, max_url_len=None):
        self.name = name
        self.app = app
        self._server = None
        self._protocol = protocol
        # 初始化一个默认大小为1000的绿色线程池
        self._pool = eventlet.GreenPool(pool_size or self.default_pool_size)
        self._logger = logging.getLogger("nova.%s.wsgi.server" % self.name)
        self._wsgi_logger = logging.WritableLogger(self._logger)
        self._use_ssl = use_ssl
        self._max_url_len = max_url_len

        if backlog < 1:
            raise exception.InvalidInput(
                    reason='The backlog must be more than 1')

        bind_addr = (host, port)
        # TODO(dims): eventlet's green dns/socket module does not actually
        # support IPv6 in getaddrinfo(). We need to get around this in the
        # future or monitor upstream for a fix
        try:
            info = socket.getaddrinfo(bind_addr[0],
                                      bind_addr[1],
                                      socket.AF_UNSPEC,
                                      socket.SOCK_STREAM)[0]
            family = info[0]
            bind_addr = info[-1]
        except Exception:
            family = socket.AF_INET
        # 建立socket,监听host:port
        self._socket = eventlet.listen(bind_addr, family, backlog=backlog)
        (self.host, self.port) = self._socket.getsockname()[0:2]
        LOG.info(_("%(name)s listening on %(host)s:%(port)s") % self.__dict__)
        
    def start(self):
        wsgi_kwargs = {
            'func': eventlet.wsgi.server,
            'sock': self._socket,
            'site': self.app,
            'protocol': self._protocol,
            'custom_pool': self._pool,
            'log': self._wsgi_logger,
            'log_format': CONF.wsgi_log_format
            }

        self._server = eventlet.spawn(**wsgi_kwargs)
在nova.wsgi.server中会有真正的socket创建监听过程,服务启动后会调用start这个方法,具体是调用eventlet.spawn。其中Eventlet是一个高性能的网络库,它依赖两个关键的库:
  • greenlet: 协程库,提供并发能力
  • epoll/kqueue: 基于事件驱动的网络库,处理网络请求
Eventlet 常用的 API 如下:
  • eventlet.spawn(func, *args, **kw):启动一个协程并获取其返回值
  • eventlet.spawn_n(func, *args, **kw):启动一个协程,但不获取返回值
  • eventlet.spawn_after(seconds, func, *args, **kw):过段时间启动一个协程并获取返回值
  • eventlet.sleep(seconds=0):切换运行中的协程,进入 sleep 状态
其中spawn的部分代码为其部分的代码为:
while True:
    try:
        client_socket = sock.accept()
        client_socket[0].settimeout(serv.socket_timeout)
        if debug:
            serv.log.write("(%s) accepted %r\n" % (
                    serv.pid, client_socket[1]))
            try:
                pool.spawn_n(serv.process_request, client_socket)
            except AttributeError:
            ···
这里就是循环的调用sock.accept() 接收请求,每次接收到一个http的请求,就调用 pool.spawn_n() 启动一个协程处理该请求,由此来实现并发请求处理。
App加载过程
前面提到nova.services.WSGIService其中过程中对App的加载,其中的loader其实是wsgi.Loader类对象,对应的load_app方法代码为:
# nova.wsgi.Loader:load_app
    def load_app(self, name):
        try:
            LOG.debug(_("Loading app %(name)s from %(path)s") %
                      {'name': name, 'path': self.config_path})
            return deploy.loadapp("config:%s" % self.config_path, name=name)
        except LookupError as err:
            LOG.error(err)
            raise exception.PasteAppNotFound(name=name, path=self.config_path)
是利用paste.deploy库中的loadapp来加载配置文件/etc/nova/api-pasted.ini(默认文件位置)中指定的App,这里就是osapi_compute。paste.deploy是一个可以配置WSGI App的工具,可以让服务器运行时,按照配置文件执行一系列的程序,简单来说就是它提供一个了方法 loadapp,可以用来从config配置文件或者Python egg文件加载App(包括middleware/filter和app),它只要求app 给它提供一个入口函数,该函数通过配置文件告诉paste depoly loader。
简单的来看下api-paste.ini文件的内容,只截取了osapi_compute这个App v2版本的相关部分内容。
[composite:osapi_compute]
use = call:nova.api.openstack.urlmap:urlmap_factory
/: oscomputeversions
/v1.1: openstack_compute_api_v2
/v2: openstack_compute_api_v2
/v3: openstack_compute_api_v3

[composite:openstack_compute_api_v2]
keystone = statsd faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2

[filter:statsd]
paste.filter_factory = nova.api.openstack.compute.stats_notifier:filter_factory

[filter:faultwrap]
paste.filter_factory = nova.api.openstack:FaultWrapper.factory

[filter:ratelimit]
paste.filter_factory = nova.api.openstack.compute.limits:RateLimitingMiddleware.factory

[filter:sizelimit]
paste.filter_factory = nova.api.sizelimit:RequestBodySizeLimiter.factory

[app:osapi_compute_app_v2]
paste.app_factory = nova.api.openstack.compute:APIRouter.factory

[filter:keystonecontext]
paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory

[filter:authtoken]
paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory
配置文件由若干section组成,每个section声明的格式为type:name,这里的type有多中类型,包括app、composite、filter、pipeline、filter-app等。其中composite负责将URL请求分配给其他的WSGI应用,如这里的/v2就会分发给openstack_compute_api_v2这个App。filter作为过滤器,作用于WSGI应用上,以app为唯一的参数,并返回一个“过滤”后的app。而pipeline可以对一个应用添加多个过滤器,将他们以类似管道的形式连接起来。
我们以/v2开头的请求为例,根据配置文件它将交给openstack_compute_api_v2处理,而在请求到达App之前还要经过6个filter的处理,这6个filter分别做了如下事情:
  • 获取response的status
  • 异常捕获,处理服务内部异常,防止wsgi server挂掉
  • 限制HTTP请求body大小,对于太大的body,将返回HTTPRequestEntityTooLarge错误,默认的请求大小是112k
  • 对请求keystone对header中token id进行验证;
  • 利用headers初始化一个nova.context.RequestContext实例,并赋给req.environ['nova.context'];
  • 限制用户的访问速度
而osapi_compute_app_v2这个APP返回一个nova.api.openstack.compute:APIRouter.factory实例,nova.api.openstack.compute.APIRouter继承nova.api.openstack.APIRouter,nova.api.openstack.APIRouter又继承nova.wsgi.APIRouter。其中factory在nova.api.openstack.APIRouter中实现。
相关的继承关系如下图所示。
# nova.api.openstack.APIRouter:fatory
    @classmethod
    def factory(cls, global_config, **local_config):
        """Simple paste factory, :class:`nova.wsgi.Router` doesn't have one."""
        return cls()

    def __init__(self, ext_mgr=None, init_only=None):
        if ext_mgr is None:
            if self.ExtensionManager:
                ext_mgr = self.ExtensionManager()
            else:
                raise Exception(_("Must specify an ExtensionManager class"))

        mapper = ProjectMapper()
        self.resources = {}
        self._setup_routes(mapper, ext_mgr, init_only)  # 加载核心资源
        self._setup_ext_routes(mapper, ext_mgr, init_only) # 加载扩展资源
        self._setup_extensions(ext_mgr)                   # 加载扩展控制类
        super(APIRouter, self).__init__(mapper)
其中self._setup_routes的具体实现在nova.api.openstack.compute:APIRouter中,主要是加载了一些核心资源,包括创建、删除云主机、获取云主机列表等,这里只列了云主机相关的资源。
if init_only is None or 'consoles' in init_only or \
        'servers' in init_only or ips in init_only:
    self.resources['servers'] = servers.create_resource(ext_mgr)
    mapper.resource("server", "servers",
                    controller=self.resources['servers'],
                    collection={'detail': 'GET'},
                    member={'action': 'POST'})
这里是使用Routes这个模块来实现URL到具体action的映射。Routes是一个python重新实现的Rails routes system,用来将urls映射到应用具体的action上,同时还可以生成url。主要了解下Routes的匹配规则,Routes有两种方式可以定义匹配规则,Mapper.connect和Mapper.resource,在Nova中,用的最多的是Mapper.resource方式。以上面的代码举例,后缀为servers/detail的GET请求会交给action这个方法来处理,而POST请求则交给由@wsig.action修饰的方法处理。除此之外还会生成一些默认的规则。创建虚拟机的就是匹配的下面第二条规则。
map.connect('/servers',controller=a,action='index',conditions={'method':['GET']})
map.connect('/servers',controller=a,action='create',conditions={'method':['POST']})
map.connect('/servers/{id}',controller=a,action='show',conditions={'method':['GET']})
map.connect('/servers/{id}',controller=a,action='update',conditions={'method':['PUT']})
map.connect('/servers/{id}',controller=a,action='delete',conditions={'method':['DELETE'])
这里主要是看下servers.create_resource(ext_mgr)的代码。
# nova.api.openstack.compute.servers.py
def create_resource(ext_mgr):
    return wsgi.Resource(Controller(ext_mgr))
Controller对象放入wsgi.Resource方法调用,Controller中是本servers.py中最重要的部分,里面有主要的API包括create、detail、show等,看下wsgi.Resource方法。
# api.openstack.compute.wsgi.py:Resource
class Resource(wsgi.Application):
    def __init__(self, controller, action_peek=None, inherits=None,
                 **deserializers):
        self.controller = controller
        ···
        self.wsgi_actions = {}
        if controller:
            self.register_actions(controller)
        ···

    def register_actions(self, controller):
        """Registers controller actions with this resource."""

        actions = getattr(controller, 'wsgi_actions', {})
        for key, method_name in actions.items():
            self.wsgi_actions[key] = getattr(controller, method_name)
在register_actions中加载进对应的controller中的所有方法,即:将所有API通过wsgi.Resource将对应Controller中的所有方法加载进来。到这里,核心资源加载完毕了。除了核心资源之外,nova中还有扩展资源和扩展的控制类,这类资源位于nova.api.openstack.compute.contrib中。扩展资源由@wsig.extend修饰extends 是对某个 Core Resource 的某个 CURD 方法比如 create, index,show,detail 等的扩展,在使用标准 HTTP Method 访问 Core resource 时,可以附加 extension 信息,在 response 中你可以得到这些方法的output。另外还有扩展的controller,wsgi服务启动的时候会调用其get_resource方法,把Contrroler中的资源加载进来。
接下来就是nova-api中对应的create方法对请求进行处理了。
# nova/api/openstack/compute/servers.py:Controller.create
class Controller(wsgi.Controller):
    @wsgi.response(202)
    @wsgi.serializers(xml=FullServerTemplate)
    @wsgi.deserializers(xml=CreateDeserializer)
    def create(self, req, body):
        """Creates a new server for a given user."""
        if not self.is_valid_body(body, 'server'):
            raise exc.HTTPUnprocessableEntity()
        context = req.environ['nova.context']
         # 各种参数处理。。
         ···
        (instances, resv_id) = self.compute_api.create(context,
                            inst_type,
                            image_uuid,
                            display_name=name,
                            display_description=name,
                            key_name=key_name,
                            metadata=server_dict.get('metadata', {}),
                            ····)
nova-api这边处理相对简单,主要就是从请求body中获取各种参数并检查,然后调用create_api的create方法,后者则是对参数进一步处理后,通过RPC调用讲请求发送到消息队列当中。下一篇再分析nova-scheduler的处理流程。

本文来自网易实践者社区,经作者廖跃华授权发布。