专栏名称: Python开发者
人生苦短,我用 Python。伯乐在线旗下账号「Python开发者」分享 Python 相关的技术文章、工具资源、精选课程、热点资讯等。
目录
相关文章推荐
Python中文社区  ·  狂暴大牛市该买点啥?用量化选股抓住机会 ·  2 天前  
Python爱好者社区  ·  来了!GPT4o接入个人微信! ·  2 天前  
Python中文社区  ·  2024仅剩100天!5个实现躺赚的AI变现工具 ·  5 天前  
Python爱好者社区  ·  裁员了,很严重,大家做好准备吧... ·  1 周前  
Python爱好者社区  ·  阿里云盘惊现他人私密照,网友:匪夷所思的 Bug ·  6 天前  
51好读  ›  专栏  ›  Python开发者

深入理解 nova-api 的 WSGI

Python开发者  · 公众号  · Python  · 2017-03-20 19:51

正文

(点击上方蓝字,快速关注我们)


来源: koala bear

wsfdl.com/openstack/2013/10/18/理解nova-api的WSGI框架.html

如有好文章投稿,请点击 → 这里了解详情


本文是 理解 WSGI 框架 的下篇,重点介绍 WSGI 框架下一些常用的 python module,并使用这些 module 编写一个类似 nova-api 里 WSGI 的简单样例,最后分析 nova 是如何使用这些 module 构建其 WSGI 框架。


  • eventlet: python 的高并发网络库

  • paste.deploy: 用于发现和配置 WSGI application 和 server 的库

  • routes: 处理 http url mapping 的库


Eventlet


Eventlet 是一个基于协程的 Python 高并发网络库,和上篇文章所用的 wsgiref 相比,它具有更强大的功能和更好的性能,OpenStack 大量的使用 eventlet 以提供并发能力。它具有以下特点:


  • 使用 epoll、kqueue 或 libevent 等 I/O 复用机制,对于非阻塞 I/O 具有良好的性能

  • 基于协程(Coroutines),和进程、线程相比,其切换开销更小,具有更高的性能

  • 简单易用,特别是支持采用同步的方式编写异步的代码


Eventlet.wsgi


Eventlet WSGI 简单易用,数行代码即可实现一个基于事件驱动的 WSGI server。本例主要使用了 eventlet.wsgi.server 函数:


eventlet.wsgi.server(sock, site, log=None, environ=None,

                     max_size=None, max_http_version='HTTP/1.1',

                     protocol=eventlet.wsgi.HttpProtocol, server_event=None,

                     minimum_chunk_size=None, log_x_forwarded_for=True,

                     custom_pool=None, keepalive=True,

                     log_output=True, log_format='%(client_ip)s...',

                     url_length_limit=8192, debug=True,

                     socket_timeout=None, capitalize_response_headers=True)


该函数的参数众多,重点介绍以下 2 个参数:


  • sock: 即 TCP Socket,通常由 eventlet.listen(‘IP’, PORT) 实现

  • site: WSGI 的 application


回顾上篇文章内容,本例采用 callable 的 instance 实现一个 WSGI application,利用 eventlet.server 构建 WSGI server,如下:


import eventlet

from eventlet import wsgi

 

 

class AnimalApplication(object):

    def __init__(self):

        pass

 

    def __call__(self, environ, start_response):

        start_response('200 OK', [('Content-Type', 'text/plain')])

        return ['This is a animal applicaltion!rn']

 

 

if '__main__' == __name__:

    application = AnimalApplication()

    wsgi.server(eventlet.listen(('', 8080)), application)


Eventlet.spawn


Eventlet.spawn 基于 greenthread,它通过创建一个协程来执行函数,从而提供并发处理能力。


eventlet.spawn(func, *args, **kw)

 

加入该函数后,样例如下:


import eventlet

from eventlet import wsgi

 

 

class AnimalApplication(object):

    def __init__(self):

        pass

 

    def __call__(self, environ, start_response):

        start_response('200 OK', [('Content-Type', 'text/plain')])

        return ['This is a animal applicaltion!rn']

 

 

if '__main__' == __name__:

    application = AnimalApplication()

    server = eventlet.spawn(wsgi.server,

                            eventlet.listen(('', 8080)), application)

    server.wait()


Paste.deploy


Paste.deploy 是一个用户发现和配置 WSGI server 和 application 的 python 库,它定义简洁的 loadapp 函数,用于从配置文件或者 python egg 中加载 WSGI 应用,它仅关注 application 的入口,不关心 application 的内部细节。


Paste.deploy 通常要求 application 实现一个 factory 的类方法,如下:


import eventlet

from eventlet import wsgi

from paste.deploy import loadapp

 

 

class AnimalApplication(object):

    def __init__(self):

        pass

 

    def __call__(self, environ, start_response):

        start_response('200 OK', [('Content-Type', 'text/plain')])

        return ['This is a animal applicaltion!rn']

 

    @classmethod

    def factory(cls, global_conf, **kwargs):

        return cls()

 

 

if '__main__' == __name__:

    application = loadapp('config:/path/to/animal.ini')

    server = eventlet.spawn(wsgi.server,

                            eventlet.listen(('', 8080)), application)

    server.wait()


配置文件的规则请参考官网介绍,相应的配置文件如下,其中 app:animal 给出了 application 的入口,pipeline:animal_pipeline 用于配置 WSGI middleware。


[composite:main]

use = egg:Paste#urlmap

/ = animal_pipeline

 

[pipeline:animal_pipeline]

pipeline = animal

 

[app:animal]

paste.app_factory = animal:AnimalApplication.factory


现在我们新增一个 IPBlackMiddleware,用于限制某些 IP:


class IPBlacklistMiddleware(object):

    def __init__(self, application):

        self.application = application

 

    def __call__(self, environ, start_response):

        ip_addr = environ.get('HTTP_HOST').split(':')[0]

        if ip_addr not in ('127.0.0.1'):

            start_response('403 Forbidden', [('Content-Type', 'text/plain')])

            return ['Forbidden']

 

        return self.application(environ, start_response)

 

    @classmethod

    def factory(cls, global_conf, **local_conf):

        def _factory(application):

            return cls(application)

        return _factory


相关配置文件:


[composite:main]

use = egg:Paste#urlmap

/ = animal_pipeline

 

[pipeline:animal_pipeline]

pipeline = ip_blacklist animal

 

[filter:ip_blacklist]

paste.filter_factory = animal:IPBlacklistMiddleware.factory

 

[app:animal]

paste.app_factory = animal:AnimalApplication.factory


Route


Routes 是基于 ruby on rails 的 routes system 开发的 python 库,它根据 http url 把请求映射到具体的方法,routes 简单易用,可方便的构建 Restful 风格的 url。


本例增加 CatController 和 DogController,对于 url_path 为 cats 的 HTTP 请求,映射到 CatController 处理,对于 url_path 为 dogs 的 HTTP 请求,映射到 DogController 处理,最终样例如下:



测试如下:


$ curl 127.0.0.1:8080/test

The resource could not be found.

$ curl 127.0.0.1:8080/cats

List cats.

$ curl -X POST 127.0.0.1:8080/cats

create cat.

$ curl -X PUT 127.0.0.1:8080/cats/kitty

update cat.

$ curl -X DELETE 127.0.0.1:8080/cats/kitty

delete cat.

$ curl 127.0.0.1:8080/dogs

List dogs.

$ curl -X DELETE 127.0.0.1:8080/dogs/wangcai

delete dog.


WSGI In Nova-api


WSGI Server


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


class Server(object):

    """Server class to manage a WSGI server, serving a WSGI application."""

 

    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."""

        self.name = name

        self.app = app

        self._server = None

        self._protocol = protocol

        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)

 

        if backlog < 1:

            raise exception.InvalidInput(

                    reason='The backlog must be more than 1')

 

        bind_addr = (host, port)

 

        # 建立 socket,监听 IP 和端口

        self._socket = eventlet.listen(bind_addr, family, backlog=backlog)

 

    def start(self):

        """Start serving a WSGI application.

 

        :returns: None

        """

 

        # 构建所需参数

        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

            }

 

        if self._max_url_len:

            wsgi_kwargs['url_length_limit'] = self._max_url_len

 

        # 由 eventlet.sawn 启动 server

        self._server = eventlet.spawn(**wsgi_kwargs)


Application Side & Middleware


Application 的加载由 nova/wsgi.py 的类 Loader 完成,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 如下所示,我们通常使用 v2 API,即 composite:openstack_compute_api_v2,也通常使用 keystone 做认证,即 keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2,从 fautlwrap 到 ratelimit 均是 middleware,我们也可根据需求增加某些 middleware。


[composite:osapi_compute]

use = call:nova.api.openstack.urlmap:urlmap_factory

/v2: openstack_compute_api_v2

/v3: openstack_compute_api_v3

 

[composite:openstack_compute_api_v2]

use = call:nova.api.auth:pipeline_factory

noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v2

keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2

keystone_nolimit = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2

 

[composite:openstack_compute_api_v3]

...

 

[filter:keystonecontext]

paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory

 

[filter:authtoken]

paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory

 

[app:osapi_compute_app_v2]

paste.app_factory = nova.api.openstack.compute:APIRouter.factory

 

[app:osapi_compute_app_v3]

paste.app_factory = nova.api.openstack.compute:APIRouterV3.factory


Routes


在 nova/api/openstack/compute/__init__.py 定义了类 APIRouter,它定义了各种 url 和 controller 之间的映射关系,最终由 nova/wsgi.py 的类 Router 加载这些 mapper。


nova/wsgi.py 中的 Router class 如下:


class Router(object):

    """WSGI middleware that maps incoming requests to WSGI apps."""

 

    def __init__(self, mapper):

        """Create a router for the given routes.Mapper."""

 

        self.map = mapper

        self._router = routes.middleware.RoutesMiddleware(self._dispatch,

                                                          self.map)

 

    @webob.dec.wsgify(RequestClass=Request)

    def __call__(self, req):

        """Route the incoming request to a controller based on self.map.

 

        If no match, return a 404.

 

        """

        return self._router

 

    @staticmethod

    @webob.dec.wsgify(RequestClass=Request)

    def _dispatch(req):

        """Dispatch the request to the appropriate controller."""

 

        match = req.environ['wsgiorg.routing_args'][1]

        if not match:

            return webob.exc.HTTPNotFound()

        app = match['controller']

        return app


看完本文有收获?请转发分享给更多人

关注「Python开发者」,提升Python技能