深入理解nova api服务

Posted by xue on May 7, 2017

一、wsgi简介

在构建 Web 应用时,通常会有 Web Server 和 Application Server 两种角色。其中 Web Server 主要负责接受来自用户的请求,解析 HTTP 协议,并将请求转发给 Application Server,Application Server 主要负责处理用户的请求,并将处理的结果返回给 Web Server,最终 Web Server 将结果返回给用户。

由于有很多动态语言和很多种 Web Server,他们彼此之间互不兼容,给程序员造成了很大的麻烦。

WSGI是一种 web server or gateway 和 python web application or framework 之间简单通用的接口,符合这种接口的 application 可运行在所有符合该接口的 server 上。通俗的讲,WSGI 规范了一种简单的接口,解耦了 server 和 application,使得双边的开发者更加专注自身特性的开发。

WSGI 标准中主要定义了两种角色:

  • “server” 或 “gateway” 端
  • “application” 或 “framework” 端

“application” 或 “framework” 端

Application/framework 端必须定义一个 callable object,callable object 可以是以下三者之一:

  • function, method
  • class
  • instance with a call method

Callable object 必须满足以下两个条件:

  • 接受两个参数:字典(environ),回调函数(start_response,返回 HTTP status,headers 给 web server)
  • 返回一个可迭代的值

一个简单的callable object 如下所示:

def simple_app(environ, start_response):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return ['Hello World']

“server” 或 “gateway” 端

Server/gateway 端主要专注 HTTP 层面的业务,重点是接收 HTTP 请求和提供并发。每当收到 HTTP 请求,server/gateway 必须调用 callable object:

  • 接收 HTTP 请求,但是不关心 HTTP url, HTTP method 等
  • 为 environ 提供必要的参数,实现一个回调函数
  • start_response,并传给 callable object 调用 callable object

一个简单的例子如下所示:

# server/gateway side
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    server = make_server('0.0.0.0', 8080, application)
    server.serve_forever()

二、Paste Deployment简介

paste deployment是一个配置WSGI APP和发现服务的一个系统,对于WSGI APP的开发者,它提供了一个简单的函数(loadapp),以便从配置文件和python egg中加载一个wsgi app,对于提供的WSGI APP,它只请求一个简单的入口,因此你不需要提供你的app详情给它。

通俗来讲:可以将它理解成是一种机制或者流程模式,在一个Server中,可以通过他将server中的app通过它进行连接组合。其中的连接方式他通过一个简单的接口提供给用户,用户只需要配置好paste deployment即可完成app的连接组合,这对用户完全是透明的。

与paste deployment的主要交互方式是配置文件,让整体server中app的流程处理按照配置文件中的流向去运行。

配置文件中由多个sections(段)组成,每个sections是由[type:name]这样的格式组成,如果不是这样的格式,将会被忽略

2.1 解读nova中api-paste.ini

/etc/nova/api-paste.ini的部分内容如下:


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

[composite:osapi_compute]
use = call:nova.api.openstack.urlmap:urlmap_factory
/: oscomputeversions
/v1.1: openstack_compute_api_v2
/v2: openstack_compute_api_v2
/v2.1: openstack_compute_api_v21
/v3: openstack_compute_api_v3
这里利用composite将xxx/xxx形式的请求交给oscomputeversions,形似xxxx/v1.1/xxxxx请求交给openstack_compute_api_v2,来实现API版本控制


[composite:openstack_compute_api_v2]
use = call:nova.api.auth:pipeline_factory
noauth = compute_req_id faultwrap sizelimit noauth ratelimit osapi_compute_app_v2
noauth2 = compute_req_id faultwrap sizelimit noauth2 ratelimit osapi_compute_app_v2
keystone = compute_req_id faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2
keystone_nolimit = compute_req_id faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2
针对openstack_compute_api_v2的实现来看,首先调用了nova.api.auth中pipeline_factory方法,从源代码可以看出,实际上_load_pipeline调用了keystone,
keystone属于是一个filter的集合,请求会依次通过前面的这些filter,最后到达osapi_compute_app_v2这个app

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

[composite:openstack_compute_api_v3]
use = call:nova.api.auth:pipeline_factory_v21
noauth = request_id faultwrap sizelimit noauth_v3 osapi_compute_app_v3
noauth2 = request_id faultwrap sizelimit noauth_v3 osapi_compute_app_v3
keystone = request_id faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v3

[filter:request_id]
paste.filter_factory = oslo.middleware:RequestId.factory

[filter:compute_req_id]
paste.filter_factory = nova.api.compute_req_id:ComputeReqIdMiddleware.factory

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

[filter:noauth]
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddlewareOld.factory

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

[filter:noauth_v3]
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddlewareV3.factory

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

[filter:sizelimit]
paste.filter_factory = oslo.middleware:RequestBodySizeLimiter.factory

[app:osapi_compute_app_v2]
paste.app_factory = nova.api.openstack.compute:APIRouter.factory
该app直接调用了nova.api.openstack.compute中的APIRouter类中的factory函数。

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

[app:osapi_compute_app_v3]
paste.app_factory = nova.api.openstack.compute:APIRouterV3.factory

[pipeline:oscomputeversions]
pipeline = faultwrap oscomputeversionapp

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

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

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

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

从上面来看在OpenStack中nova-api的请求流程:依次经过filter[compute_req_id faultwrap sizelimit authtoken keystonecontext ratelimit],最后到达osapi_compute_app_v2这个app。

三、Nova API服务的启动

3.1 WSGI Server

Nova-api(nova/cmd/api.py) 服务启动时,初始化 nova/wsgi.py 中的类 Server,建立了 socket 监听 IP 和端口,再由 eventlet.spawn 和 eventlet.wsgi.server 创建 WSGI server:

class Server(object):
    def __init__(self, name, app, host='0.0.0.0', port=0, pool_size=None,
        ...
        eventlet.wsgi.MAX_HEADER_LINE = CONF.max_header_line
        self.name = name
        self.app = app
        self._server = None
        bind_addr = (host, port)
        ...
        # 建立 socket,监听 IP 和端口
        self._socket = eventlet.listen(bind_addr, family, backlog=backlog)
    def start(self):
        ...
        # 构建所需参数
        wsgi_kwargs = {
            'func': eventlet.wsgi.server,
            'sock': dup_socket,
            'site': self.app,
            'protocol': self._protocol,
            'custom_pool': self._pool,
            'log': self._wsgi_logger,
            'log_format': CONF.wsgi_log_format,
            'debug': False,
            'keepalive': CONF.wsgi_keep_alive,
            'socket_timeout': self.client_socket_timeout
            }

        if self._max_url_len:
            wsgi_kwargs['url_length_limit'] = self._max_url_len
        # 由 eventlet.sawn 启动 server
        self._server = eventlet.spawn(**wsgi_kwargs)
        

3.2 Application Side & Middlewar

nova/wsgi.py的__init__方法中加载了Loader类,Application 的加载就是由它完成的,Loader 的 load_app 方法调用了 paste.deploy.loadapp 加载了 WSGI 的配置文件 /etc/nova/api-paste.ini:

class Loader(object):
    """Used to load WSGI applications from paste configurations."""

    def __init__(self, config_path=None):

        # 获取 WSGI 配置文件的路径
        self.config_path = config_path or CONF.api_paste_config

    def load_app(self, name):

        # paste.deploy 读取配置文件并加载该配置
        return paste.deploy.loadapp("config:%s" % self.config_path, name=name)

前面的api-paste.ini提到过,nova-api请求会到osapi_compute_app_v2这个app,根据api-paste.ini里面的定义:

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

nova/api/openstack/compute/__init__.py下APIRouter类中的factory方法继承自nova.api.openstack.APIRouter,factory方法创建了一个APIRouter对象,在APIRouter的__init__方法中调用了_setup_routes方法。其方法定义如下:

#nova/api/openstack/compute/__init__.py
class APIRouter(nova.api.openstack.APIRouter):
    def _setup_routes(self, mapper, ext_mgr, init_only):
        ...
        if init_only is None or 'consoles' in init_only:
            self.resources['consoles'] = consoles.create_resource()
            mapper.resource("console", "consoles",
                        controller=self.resources['consoles'],
                        parent_resource=dict(member_name='server',
                        collection_name='servers'))

        if init_only is None or 'consoles' in init_only or \
                'servers' in init_only or 'ips' in init_only:
            #ext_mgr是一个ExtensionManager对象,定义在nova/api/openstack/compute/extensions.py
            self.resources['servers'] = servers.create_resource(ext_mgr)
            mapper.resource("server", "servers",
                            controller=self.resources['servers'],
                            collection={'detail': 'GET'},
                            member={'action': 'POST'})
        ...
        
#nova/api/openstack/compute/consoles.py
def create_resource():
    #console资源的controller对象是一个wsgi.Resource对象
    return wsgi.Resource(Controller())
    

在_setup_routes方法中定义了许多nova资源的url映射,以上代码中给出了console,server资源的url映射。可以看到,对于每种资源,都是先调用资源的create_resource方法。返回一个resource对象。然后再调用mapper.resource添加资源的url.

以server资源为例:

mapper.resource("server", "servers",
                            controller=self.resources['servers'],
                            collection={'detail': 'GET'},
                            member={'action': 'POST'})

为了让代码更加简洁,mapper.resource将一些最基本的url映射封装了起来,即它已经封装了一些PUT,GET,DELETE之类的常见方法映射。
mapper.resource的参数含义为:server是成员名,servers是集合名,collection参数指定额外的集合操作,member指定额外的成员操作。
上面的代码的含义是,在最基本的url映射方法的基础上,额外添加了如下url映射:

map.connect("servers","/servers/detail",controller=self.resources['servers'],action="detail",conditions=dict(method=["GET"]))
map.connect("server","/server/{id}/action",controller=self.resources['servers'],action="action",conditions=dict(method=["POST"]))

总结

概括一下nova api的大致启动流程:

1.执行nova/cmd/api.py中main函数,解析参数,设置日志,启动WSGIService
2.WSGI的服务启动过程中,主要涉及WSGIService的初始化(init)和启动(start)方法
3.init方法会根据/etc/nova/api-paste.ini中的Paste规则,通过wsgiLoader来load_app
4.start方法通过 eventlet.spawn 和 eventlet.wsgi.server 创建 WSGI server

wsgi的application端和server端的实现:

server端通过Eventlet来实现。通过Paste deployment 配置来发现和配置 WSGI server 和 application 。

application端需要实现实现一个factory的类方法,收到HTTP请求时,会调用__call__方法,返回response.

APIRouter根据 http url把请求映射到具体的方法.

参考

Paste Deployment简介及nova-api-paste.ini解析
JiYou