启动nova-api服务解析(rocky版本)

启动nova的api服务时,需要调用nova-api命令,nova-api命令最终是调用nova.cmd.api模块里的main方法。

nova-api命令的main方法解析:

def main():
#此方法会解析命令行传入进来的参数(sys.argv),配置文件的参数管理使用oslo.config模块进行管理。
    config.parse_args(sys.argv)
    #调用solo.log模块建立nova域的日志空间
    logging.setup(CONF, "nova")
    #注册所有需要的模块对象
    objects.register_all()
   #oslo_reports模块,用于生成错误报告,如内存泄漏等
    gmr_opts.set_defaults(CONF)
   
    if 'osapi_compute' in CONF.enabled_apis:
        # NOTE(mriedem): This is needed for caching the nova-compute service
        # version.
    #这里对于缓存nova-compute服务版本是必要的,当使用网络ID为'auto'或'none',发出服务器创建请求时,将会查找该版本?(目前不理解)
        objects.Service.enable_min_version_cache()
    #构建获取logger
    log = logging.getLogger(__name__)
    #使用oslo.reports模块来管理收集错误报告
    gmr.TextGuruMeditation.setup_autorun(version, conf=CONF)
   #获取oslo.service模块的服务启动实例
    launcher = service.process_launcher()
    started = 0
   #定义了api-paste.ini两个默认的sections名,分别为osapi_compute,metadata
    for api in CONF.enabled_apis:
        should_use_ssl = api in CONF.enabled_ssl_apis
        try:
    #遍历启wsgi服务实例,会根据api-paste.ini配置文件加载app
            server = service.WSGIService(api, use_ssl=should_use_ssl)
            launcher.launch_service(server, workers=server.workers or 1)
            started += 1
        except exception.PasteAppNotFound as ex:
            log.warning("%s. ``enabled_apis`` includes bad values. "
                        "Fix to remove this warning.", ex)

    if started == 0:
        log.error('No APIs were started. '
                  'Check the enabled_apis config option.')
        sys.exit(1)
#服务阻塞
    launcher.wait()

api-paste.ini解析:

pasteDeploy通过文件配置的方式将各个功能以模块化的方式组装在一起。这样做的好处是把各个功能特性分块化后方便组装与拆卸,起到了松耦合的作用。

############
 # Metadata #
############
[composite:metadata]
use = egg:Paste#urlmap   #调用paste模块的urlmap来转发URL,配置文件中的enabled_apis的其中一个默认值就是metadata
/: meta    #将/的路径转发到[pipeline:meta]处理,:号也可以换成=号来映射

[pipeline:meta]
pipeline = cors metaapp

[app:metaapp]
paste.app_factory = nova.api.metadata.handler:MetadataRequestHandler.factory

#############
# OpenStack #
#############

[composite:osapi_compute] #这里的url路径映射是使用:号,也可以换成=号来代替
use = call:nova.api.openstack.urlmap:urlmap_factory #这里nova自定义了urlmap工厂来处理URL,配置文件中的enabled_apis的其中一个默认值就是osapi_compute
/: oscomputeversions         #将/的路径转发到[pipeline:oscomputeversions]处理
**v21是与v2完全匹配的功能,但它在wsgi表面上具有更严格的输入验证(防止API早期模糊化)。** 
**# 它还通过API微版本提供了新功能,这些微版本可供客户选择。不知情的客户端将接收到相同的冻结v2 API特性集,但有一些宽松的验证**

/v2: openstack_compute_api_v21_legacy_v2_compatible #将/v2开头的url转到[composite:openstack_compute_api_v21_legacy_v2_compatible]处理
/v2.1: openstack_compute_api_v21    #将/v2.1开头的url转到[composite:openstack_compute_api_v21]处理

[composite:openstack_compute_api_v21]
#调用自定义的composite工厂方法。方法里会根据配置文件的认证策略来决定加载下面哪一种应用,默认加载keystone,也可以将配置文件里的auth_strategy设置为noauth2。
use = call:nova.api.auth:pipeline_factory_v21
#在noauth2和keystone配置里不同的地方是noauth2和authtoken这两个认证filter,authtoken会先进行keystone登陆验证
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler noauth2 osapi_compute_app_v21
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler authtoken keystonecontext osapi_compute_app_v21

[composite:openstack_compute_api_v21_legacy_v2_compatible]
use = call:nova.api.auth:pipeline_factory_v21
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler noauth2 legacy_v2_compatible osapi_compute_app_v21
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap sizelimit osprofiler authtoken keystonecontext legacy_v2_compatible osapi_compute_app_v21

[filter:request_id]
# 它确保为每个API请求分配请求ID并将其设置到request environment。请求ID也被添加到API响应中。 
paste.filter_factory = oslo_middleware:RequestId.factory # 确保请求ID的中间件

[filter:compute_req_id]
# 它确保为每个请求到compute服务的api分配请求ID并将其设置到request environ。请求ID也被添加到API响应中。
paste.filter_factory = nova.api.compute_req_id:ComputeReqIdMiddleware.factory

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

[filter:noauth2]
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory

[filter:osprofiler]
paste.filter_factory = nova.profiler:WsgiMiddleware.factory

[filter:sizelimit]
paste.filter_factory = oslo_middleware:RequestBodySizeLimiter.factory #限制传入请求的大小的中间件。 

[filter:http_proxy_to_wsgi]
# 此中间件使用远程HTTP反向代理提供的环境变量重载WSGI环境变量。 
paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory # 到WSGI终止中间件的HTTP代理。 

[filter:legacy_v2_compatible]
paste.filter_factory = nova.api.openstack:LegacyV2CompatibleWrapper.factory

[app:osapi_compute_app_v21]
paste.app_factory = nova.api.openstack.compute:APIRouterV21.factory

[pipeline:oscomputeversions]
pipeline = cors faultwrap http_proxy_to_wsgi oscomputeversionapp

[app:oscomputeversionapp]
paste.app_factory = nova.api.openstack.compute.versions:Versions.factory

##########
# Shared #
##########

[filter:cors]
# 这个中间件允许WSGI应用程序为多个配置的域提供CORS报头.
paste.filter_factory = oslo_middleware.cors:filter_factory # CORS中间件 
oslo_config_project = nova

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

[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory

方法调用关系分析:

缓存版本实现方法:作用目前不明

openstack nova的state 都是down openstack nova api_openstack


classmethod 修饰符对应的函数不需要实例化,不需要 self 参数,但第一个参数需要是表示自身类的 cls 参数,可以来调用类的属性,类的方法,实例化对象等。

openstack nova的state 都是down openstack nova api_配置文件_02


模块服务进程启动:

openstack nova的state 都是down openstack nova api_配置文件_03


Openstack中有一个叫Launcher的概念,即专门用来启动服务的,这个类被放在了oslo_service这个包里面。Launcher分为两种,一种是ServiceLauncher,另一种为ProcessLauncher。ServiceLauncher用来启动单进程的服务,而ProcessLauncher用来启动有多个worker子进程的服务。

ProcessLauncher直接继承于Object,所以其对launch_service方法进行了实现。后面会详细介绍。

openstack nova的state 都是down openstack nova api_openstack_04


核心代码:

server = service.WSGIService(api, use_ssl=should_use_ssl)

launcher.launch_service(server, workers=server.workers or 1)

openstack nova的state 都是down openstack nova api_云计算_05


WSGIService的定义在nova/service.py文件中,代码如下:

openstack nova的state 都是down openstack nova api_python_06

openstack nova的state 都是down openstack nova api_openstack_07


根据传入的参数name创建一个wsgi server,首先通过

wsgi.Loader.load_app(name)载入app。

openstack nova的state 都是down openstack nova api_python_08

openstack nova的state 都是down openstack nova api_openstack_09

通过Python库中的deploy.loadapp加载在文件/etc/nova/api-paste.ini中定义的osapi_compute,如下所以,v1版本的api请求由oscomputeversions处理,v2版本的请求由openstack_compute_api_v21_legacy_v2_compatible处理,v2.1版本的api请求由openstack_compute_api_v21处理。

#############
# OpenStack #
#############

[composite:osapi_compute]
use = call:nova.api.openstack.urlmap:urlmap_factory
/: oscomputeversions
# v21 is an exactly feature match for v2, except it has more stringent
# input validation on the wsgi surface (prevents fuzzing early on the
# API). It also provides new features via API microversions which are
# opt into for clients. Unaware clients will receive the same frozen
# v2 API feature set, but with some relaxed validation
/v2: openstack_compute_api_v21_legacy_v2_compatible
/v2.1: openstack_compute_api_v21

[composite:openstack_compute_api_v21]
use = call:nova.api.auth:pipeline_factory_v21
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler noauth2 osapi_compute_app_v21
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler authtoken keystonecontext osapi_compute_app_v21

[composite:openstack_compute_api_v21_legacy_v2_compatible]
use = call:nova.api.auth:pipeline_factory_v21
noauth2 = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler noauth2 legacy_v2_compatible osapi_compute_app_v21
keystone = cors http_proxy_to_wsgi compute_req_id faultwrap request_log sizelimit osprofiler authtoken keystonecontext legacy_v2_compatible osapi_compute_app_v21

回到nova/service.py文件中,载入app以后,根据配置文件设置的IP和端口,设置app的监听端口

openstack nova的state 都是down openstack nova api_python_10

openstack nova的state 都是down openstack nova api_配置文件_11


然后创建一个wsgi类的Server,代码如下:

class Server(service.ServiceBase):
    """Server class to manage a WSGI server, serving a WSGI application."""

    default_pool_size = CONF.wsgi.default_pool_size

    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):
        """Initialize, but do not start, a WSGI server.
        :param name: Pretty name for logging.
        :param app: The WSGI application to serve.
        :param host: IP address to serve the application.
        :param port: Port number to server the application.
        :param pool_size: Maximum number of eventlets to spawn concurrently.
        :param backlog: Maximum number of queued connections.
        :param max_url_len: Maximum length of permitted URLs.
        :returns: None
        :raises: nova.exception.InvalidInput
        """
        # Allow operators to customize http requests max header line size.
        eventlet.wsgi.MAX_HEADER_LINE = CONF.wsgi.max_header_line
        self.name = name
        self.app = app
        self._server = None
        self._protocol = protocol
        self.pool_size = pool_size or self.default_pool_size
        self._pool = eventlet.GreenPool(self.pool_size)
        self._logger = logging.getLogger("nova.%s.wsgi.server" % self.name)
        self._use_ssl = use_ssl
        self._max_url_len = max_url_len
        self.client_socket_timeout = CONF.wsgi.client_socket_timeout or None

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

        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

        try:
            self._socket = eventlet.listen(bind_addr, family, backlog=backlog)
        except EnvironmentError:
            LOG.error("Could not bind to %(host)s:%(port)s",
                      {'host': host, 'port': port})
            raise

        (self.host, self.port) = self._socket.getsockname()[0:2]
        LOG.info("%(name)s listening on %(host)s:%(port)s",
                 {'name': self.name, 'host': self.host, 'port': self.port})

主要的代码为self._socket = eventlet.listen(bind_addr, family, backlog=backlog),创建一个socket监听,至此nova/cmd/api.py中的核心代码server = service.WSGIService(api, use_ssl=should_use_ssl)执行完毕了,接下来执行

launcher.launch_service(server, workers=server.workers or 1)

launcher = service.process_launcher() 代码定义在nova/service.py中

def process_launcher():
return service.ProcessLauncher(CONF, restart_method=‘mutate’)

类ProcessLauncher在文件oslo_service/service.py中定义,launch_service的代码如下:

class ProcessLauncher(object):
    ...
    def launch_service(self, service, workers=1):
        """Launch a service with a given number of workers.

       :param service: a service to launch, must be an instance of
              :class:`oslo_service.service.ServiceBase`
       :param workers: a number of processes in which a service
              will be running
        """
        _check_service_base(service)
        wrap = ServiceWrapper(service, workers)

        LOG.info('Starting %d workers', wrap.workers)
        while self.running and len(wrap.children) < wrap.workers:
            self._start_child(wrap)

lauch_service除了接受service参数以外,还需要接受一个workers参数,即子进程的个数,然后调用_start_child启动多个子进程。

553     def _start_child(self, wrap):
554         if len(wrap.forktimes) > wrap.workers:
555             # Limit ourselves to one process a second (over the period of
556             # number of workers * 1 second). This will allow workers to
557             # start up quickly but ensure we don't fork off children that
558             # die instantly too quickly.
559             if time.time() - wrap.forktimes[0] < wrap.workers:
560                 LOG.info('Forking too fast, sleeping')
561                 time.sleep(1)
562 
563             wrap.forktimes.pop(0)
564 
565         wrap.forktimes.append(time.time())
566 
567         pid = os.fork()
568         if pid == 0:
569             self.launcher = self._child_process(wrap.service)
570             while True:
571                 self._child_process_handle_signal()
572                 status, signo = self._child_wait_for_exit_or_signal(
573                     self.launcher)
574                 if not _is_sighup_and_daemon(signo):
575                     self.launcher.wait()
576                     break
577                 self.launcher.restart()
578 
579             os._exit(status)
580 
581         LOG.debug('Started child %d', pid)
582 
583         wrap.children.add(pid)
584         self.children[pid] = wrap
585 
586         return pid

_start_child只是简单的调用了一个os.fork(),然后子进程开始运行,子进程调用_child_process。

534     def _child_process(self, service):
535         self._child_process_handle_signal()
536 
537         # Reopen the eventlet hub to make sure we don't share an epoll
538         # fd with parent and/or siblings, which would be bad
539         eventlet.hubs.use_hub()
540 
541         # Close write to ensure only parent has it open
542         os.close(self.writepipe)
543         # Create greenthread to watch for parent to close pipe
544         eventlet.spawn_n(self._pipe_watcher)
545 
546         # Reseed random number generator
547         random.seed()
548        #类Launcher下的launch_service方法
549         launcher = Launcher(self.conf, restart_method=self.restart_method)
550         launcher.launch_service(service)
551         return launcher

_child_process其实很简单,创建一个Launcher,调用Laucher.launch_service方法,调用service.start方法启动服务。

271     def launch_service(self, service, workers=1):
272         """Load and start the given service.
273 
274         :param service: The service you would like to start, must be an
275                         instance of :class:`oslo_service.service.ServiceBase`
276         :param workers: This param makes this method compatible with
277                         ProcessLauncher.launch_service. It must be None, 1 or
278                         omitted.
279         :returns: None
280 
281         """
282         if workers is not None and workers != 1:
283             raise ValueError(_("Launcher asked to start multiple workers"))
284         _check_service_base(service)
285         service.backdoor_port = self.backdoor_port
286         self.services.add(service)

laucher_service就是将服务添加到self.services成员里面,services成员的类型是class Services,看看它的add方法。

744 class Services(object):
745 
746     def __init__(self):
747         self.services = []
748         self.tg = threadgroup.ThreadGroup()
749         self.done = event.Event()
750 
751     def add(self, service):
752         """Add a service to a list and create a thread to run it.
753 
754         :param service: service to run
755         """
756         self.services.append(service)
757         self.tg.add_thread(self.run_service, service, self.done)
758         ...
786     @staticmethod
787     def run_service(service, done):
788         """Service start wrapper.
789 
790         :param service: service to run
791         :param done: event to wait on until a shutdown is triggered
792         :returns: None
793 
794         """
795         try:
796             service.start()
797         except Exception:
798             LOG.exception('Error starting thread.')
799             raise SystemExit(1)
800         else:
801             done.wait()

oslo_service/threadgroup.py

def add_thread(self, callback, *args, **kwargs):
        """Spawn a new thread.
        This call will block until capacity is available in the thread pool.
        After that, it returns immediately (i.e. *before* the new thread is
        scheduled).
        :param callback: the function to run in the new thread.
        :param args: positional arguments to the callback function.
        :param kwargs: keyword arguments to the callback function.
        :returns: a :class:`Thread` object
        """
        gt = self.pool.spawn(callback, *args, **kwargs)
        th = Thread(gt, self, link=False)
        self.threads.append(th)
        gt.link(_on_thread_done, self, th)
        return th

Services这个类的初始化很简单,即创建一个ThreadGroup,ThreadGroup其实是eventlet的GreenPool,Openstack利用eventlet实现并发。add方法,将self.run_service这个方法放入pool中,而service就是它的参数。run_service方法很简单,就是调用service的start方法,这样就完成了服务的启动。

openstack nova的state 都是down openstack nova api_配置文件_12

至此,api.py启动完毕。

方法调用关系:

nova/cmd/api.py  nova/objects/service.py    nova/objects/service.py   nova/objects/service.py
main()->enable_min_version_cache()->clear_min_version_cache()->_MIN_VERSION_CACHE = {}
                                  ->_SERVICE_VERSION_CACHING = True(初始为False)
        nova/service.py   site-packages/oslo_service/service.py
      ->process_launcher()->ProcessLauncher()
        nova/service.py
      ->WSGIService()
            nova/api/wsgi.py
         ->loader.load_app(name) 
            ->deploy.loadapp() 
             #通过Python库中的deploy.loadapp加载在文件/etc/nova/api-paste.ini中定义的osapi_compute,如下所以,v1版本的api请求由#oscomputeversions处理,v2版本的请求由openstack_compute_api_v21_legacy_v2_compatible处理,v2.1版本的api请求由#openstack_compute_api_v21处理。
              回到nova/service.py文件中,载入app以后,根据配置文件设置的IP和端口,设置app的监听端口,然后创建一个wsgi类的Server:
            nova/wsgi.py
          ->wsgi.Server(name,self.app,host=self.host,port=self.port,use_ssl=self.use_ssl,max_url_len=max_url_len)
            ->._socket = eventlet.listen()#创建一个socket监听
        #launcher = service.process_launcher() 代码定义在nova/service.py中,
        #def process_launcher():
           #return service.ProcessLauncher(CONF, restart_method='mutate')类ProcessLauncher在文件oslo_service/service.py中定义
      ->launcher.launch_service(server, workers=server.workers or 1) 
           #lauch_service除了接受service参数以外,还需要接受一个workers参数,即子进程的个数,然后调用_start_child启动多个子进程。
        ->_start_child(wrap)
           #_start_child只是简单的调用了一个os.fork(),然后子进程开始运行,子进程调用_child_process。
          ->_child_process()
            ->launcher = Launcher()
            ->launcher.launch_service(service)
              ->self.services.add(service)
                  oslo_service/threadgroup.py
                ->self.tg.add_thread(self.run_service, service, self.done)
                ->self.run_service
                  ->service.start()