本文是 理解 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!\r\n']


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!\r\n']


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!\r\n']

    @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 railsroutes system 开发的 python 库,它根据 http url 把请求映射到具体的方法,routes 简单易用,可方便的构建 Restful 风格的 url。

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

import eventlet
from eventlet import wsgi
from paste.deploy import loadapp
import routes
import routes.middleware as middleware
import webob.dec
import webob.exc


class Resource(object):
    def __init__(self, controller):
        self.controller = controller()

    @webob.dec.wsgify
    def __call__(self, req):
        match = req.environ['wsgiorg.routing_args'][1]
        action = match['action']
        if hasattr(self.controller, action):
            method = getattr(self.controller, action)
            return method(req)
        return webob.exc.HTTPNotFound()


class CatController(object):

    def index(self, req):
        return 'List cats.'

    def create(self, req):
        return 'create cat.'

    def delete(self, req):
        return 'delete cat.'

    def update(self, req):
        return 'update cat.'


class DogController(object):

    def index(self, req):
        return 'List dogs.'

    def create(self, req):
        return 'create dog.'

    def delete(self, req):
        return 'delete dog.'

    def update(self, req):
        return 'update dog.'


class AnimalApplication(object):
    def __init__(self):
        self.mapper = routes.Mapper()
        self.mapper.resource('cat', 'cats', controller=Resource(CatController))
        self.mapper.resource('dog', 'dogs', controller=Resource(DogController))
        self.router = middleware.RoutesMiddleware(self.dispatch, self.mapper)

    @webob.dec.wsgify
    def __call__(self, req):
        return self.router

    @classmethod
    def factory(cls, global_conf, **local_conf):
        return cls()

    @staticmethod
    @webob.dec.wsgify
    def dispatch(req):
        match = req.environ['wsgiorg.routing_args'][1]
        return match['controller'] if match else  webob.exc.HTTPNotFound()


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


if '__main__' == __name__:
    application = loadapp('config:/path/to/animal.ini')
    server = eventlet.spawn(wsgi.server,
                            eventlet.listen(('', 8080)), application)
    server.wait()

测试如下:

$ 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