目录

  1. 消息总线
    1. 概述
    2. AMQP
    3. 基于AMQP实现RPC
    4. 常见消息总线实现
      1. RabbitMQ
      2. AMQP 1.0
      3. ØMQ(ZeroMQ)
      4. Kafka
  2. SQLAlchemy和数据库
    1. 架构
    2. 示例
  3. RESTful API和WSGI
    1. RESTful
    2. 资源与URI
    3. 统一资源接口
      1. GET
      2. POST
      3. PUT
      4. DELETE
    4. 资源的表述
    5. RESTful路由
    6. WSGI
      1. Paste
      2. WebOb
      3. Pecan
  4. Eventlet
    1. 协程

消息总线

概述

OpenStack遵循这样的设计原则:项目之间通过RESTful API进行通信;项目内部,不同服务进程之间通过消息总线进行通信。

oslo.messaging库通过以下两种方式来完成项目各个服务进程之间的通信。

  1. 远程过程调用(Remote Procedure Call,RPC)
    通过远程过程调用,一个服务进程可以调用其他远程服务进程的方法,并且有两种方法:call和cast。通过call的方式调用,远程方法会被同步执行,调用者会被阻塞直到结果返回;通过cast的方式调用,远程方法会被异步执行,结果不会立即返回,调用者也不会被阻塞,但是调用者需要用其他方式查询这次远程调用的结果。

  2. 事件通知(Event Notification)
    某个服务进程可以把事件通知发送到消息总线上,该消息总线上所有对此类事件感兴趣的服务进程,都可以获得此事件通知并进行进一步的处理,处理的结果并不会发送给事件发送者。这种通信方式,不但可以在同一个项目内部的各个服务进程之间发送通知,还可以实现跨项目之间的通知发送。Ceilometer就通过这种方式大量获取其他OpenStack项目的事件通知,从而进行计量和监控。

通过不同的配置项,远程过程调用进而事件通知可以使用不同的消息总线后端(backend),比如RPC使用RabbitMQ,事件通知使用Kafka,以满足不同环境下的特定应用需求,极大地增加灵活性。

AMQP

Openstack所支持的消息队列类型中,大部分都是基于AMQP(Advanced Message Queuing Protocol,高级消息队列协议)。

AMQP是一个异步消息传递所使用的开放的应用层协议规范,主要包括消息的导向、队列、路由、可靠性和安全性。oslo.messaging中支持的AMQP主要包括两版本,AMQP 0.9.1和AMQP 1.0,这两个版本有很大的差别。

AMQP架构。
AMQP

对与一个实现了AMQP的中间件服务(Server/Broker)来说,当不同的消息由生产者(Producer)发送到Server时,它会根据不同的条件把消息传递给不同的消费者(Consumer)。如果消费者无法接收消息或者接收消息不够快时,它会把消息缓存在内存或者磁盘上。

AMQP模型中,上述操作分别由Exchange(消息交换)和Queue(消息队列)来实现。
此处的虚拟主机(Virtual Host)指的是Exchange和Queue的集合。

生产者将消息发送给Exchange,由Exchange来决定消息的路由,即决定将消息发送到那个Queue,然后消费者从Queue中取出消息,进行处理。

Exchange本身不会保存消息,它接收由生产者发送来的消息,然后根据不同的条件将消息转发到不同的Queue。这里的条件又被成为绑定(Binding)。

接收到消息时,Exchange会查看消息的属性、消息头和消息体,从中提取相关的信息,然后用此信息再根据绑定表把消息转发给不同的Queue或者其他Exchange。

绝大情况下,这个用来查询绑定表的信息是一个单一的键值,称为routing key。每一个发送的消息都有一个routing key。同样,每一个Queue也有一个binding key,Exchange在进行消息路由时,会查询每一个Queue。如果某个Queue的binding key与某个消息的routing key匹配,这个消息会被转发到那个Queue。

Exchange消息交换类型。

类型 说明
Direct binding key和routing key必须完全一致,不支持通配符。
Topic 同Direct类型,但支持通配符。
‘*’,匹配一个单字。
‘#’,匹配零个或者多个单字。
单字之间是由’.’来分割的。
Fanout 忽略binding key和routing key,消息会被传递到所有绑定的队列上。

Direct是需要满足单一条件的路由,在Exchange判断要消息发送给哪个Queue时,判断的依据只能是一个条件。

Fanout是指广播式的路由,将消息发送给所有的Queue。

Topic是需要满足多个条件的路由,转发消息需要依据多个条件。

基于AMQP实现RPC

基于AMQP实现远程调用RPC的过程。

amqp-rpc


* 客户端发送一个请求消息给Exchange,指定routing key为“op_queue”,同时指明一个消息队列名用来获取响应,图中为“res_queue”。 * Exchange把消息转发到消息队列op_queue。 * 消息队列op_queue把消息推送给服务器,服务端执行此RPC调用的对应的任务。执行结束后,服务端把响应结果发送给消息队列,指明routing key为“res_queue”s。 * Exchange把此消息转发到消息队列res_queue。 * 客户端从消息队列res_queue获取响应。

常见消息总线实现

RabbitMQ

RabbitMQ是一个实现了AMQP的消息中间件服务。它包括Server/Broker,支持多种协议的网关(HTTP、STOMP、MQTT等),支持多种语言(Erlang、Java、.NET Framework等)的客户端开发库,支持用户自定义插件开发的框架以及多种插件。

RabbitMQ的Server/Broker使用Erlang语言编写,使用Mozilla Public License(MPL)许可证发行。

oslo.messaging底层实现了两种不同的driver来支持RabbitMQ,分别是kombu和pika。它们的主要区别在于使用了不同的Python library。

AMQP 1.0

支持实现了AMQP 1.0协议的消息总线应用,相比AMQP 0.9协议,AMQP 1.0更加灵活和复杂。

除了常见的AMQP broker模式,AMQP 1.0还实现了一种消息路由模式,位于调用者和服务器之间的不再是单节点的broker,而是有一群互相连接的消息路由组成的路由网,路由不具备队列(queue),没有储存信息的能力,它们的作用就是单纯地传递消息,路由节点之间通过TCP链接进行通信,调用者通过TCP链接连接到路由网中的某个路由,从而接入路由网。

AMQP
当RPC Caller1远程调用RPC Server2上的某个方法时,消息会根据最短路径算法经过RouterB和RouterD,最后到达RPC Server2。

ØMQ(ZeroMQ)

ZeroMQ是一个开源的高性能异步消息库,和实现了AMQP的RabbitMQ和Qpid不同,ZeroMQ系统可以在没有Server/Broker的情况下工作,消息的发送者需要负责消息路由以找到正确的消息目的地,消息接收者需要负责消息的入队/出对等操作。

由于没有了集中式的Broker,ZeroMQ可以实现一般AMQP Broker所达不到的很低的延迟和较大的带宽,特别适合消息数量特别巨大的应用场景。

ZeroMQ使用自己的通信协议ZMTP(ZeroMQ Message Transfer Protocol)来进行通信。ZeroMQ的库使用C++编写,使用LGPL许可证发行。

Kafka

Kafka是一个分布式的系统,有着较好的扩展能力,可以为发布和订阅提高吞吐量,被广泛应用于收集日志等海量消息应用场景中。

SQLAlchemy和数据库

架构

SQLAlchemy提供了SQL工具包以及对象关系映射器(Object Relational Mapper,ORM),这样SQLAlchemy能让Python开发人员灵活地运用SQL操作后台数据库。

SQLAlchemy主要分成两个部分:SQLAlchemy Core(SQLAlchemy核心)和SQLAlchemy ORM(SQLAlchemy对象关系映射)。

SQLAlchemy Core包括SQL语言表达式、数据引擎、连接池等,所有的这些实现,都是为了连接不同类型的数据库、提交查询和更新SQL请去后台执行、定义数据库数据类型和定义Schema等目的。

SQLAlchemy ORM提供数据映射模式,即把程序语言的对象数据映射成数据库中的关系数据,或把关系数据映射成对象数据。

SQLAlchemy架构。
sqlalchemy

示例

users表。

id name fullname password
1 w wang 123
2 k kun abc
3 t tian xyz

addresses表。

id user_id email_address
1 1 wang@qq.com
2 2 kun@qq.com
3 3 tian@qq.com

SQL语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE users (
id INTEGER NOT NULL,
name VARCHAR,
fullname VARCHAR,
password VARCHAR,
PRIMARY KEY (id)
);

CREATE TABLE addresses (
id INTEGER NOT NULL,
email_address VARCHAR NOT NULL,
user_id INTEGER,
PRIMARY KEY (id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey, create_engine
from sqlalchemy.orm import relationship, backref, sessionmaker

Base = declarative_base()


class User(Base):
def __init__(self):
pass

__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
fullname = Column(String)
password = Column(String)


class Address(Base):
def __init__(self):
pass

__tablename__ = 'addresses'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id'))
user = relationship('User', backref=backref('addresses', order_by=id))


engine = create_engine('mysql+mysqlconnector://root:19941221@localhost:3306/test')
Session = sessionmaker(bind=engine)

session = Session()

for u, a in session.query(User, Address).filter(User.id == 1).filter(User.id == Address.user_id).all():
print u, a

RESTful API和WSGI

OpenStack项目都是通过RESTful API向外提供服务,这使得OpenStack的接口在性能、可扩展性、可移植性、易用性等方面达到比较好的平衡。

RESTful

RESTful是目前流行的一种互联网软件架构,是一种架构风格、设计风格,而不是标准,只是提供了一组设计原则。REST(Representational State Transfer,表述性状态转移)一次最早是由Roy Thomas Feilding在他2000年的论文中提出,定义了他对互联网软件的架构原则,如果一个架构符合REST原则,就称它为RESTful架构。

资源与URI

REST全称是表述性状态转移,那究竟指的是什么的表述? 其实指的就是资源。任何事物,只要有被引用到的必要,它就是一个资源。资源可以是实体(例如手机号码),也可以只是一个抽象概念(例如价值) 。

要让一个资源可以被识别,需要有个唯一标识,在Web中这个唯一标识就是URI,统一资源标识符(Uniform Resource Identifier)。

URI既可以看成是资源的地址,也可以看成是资源的名称。如果某些信息没有使用URI来表示,那它就不能算是一个资源, 只能算是资源的一些信息而已。URI的设计应该遵循可寻址性原则,具有自描述性,需要在形式上给人以直觉上的关联。例如:

1
2
3
4
5
6
7
https://github.com/git
https://github.com/git/git
https://github.com/git/git/blob/master/block-sha1/sha1.h
https://github.com/git/git/commit/e3af72cdafab5993d18fae056f87e1d675913d08
https://github.com/git/git/pulls
https://github.com/git/git/pulls?state=closed
https://github.com/git/git/compare/master…next

使用_或-来让URI可读性更好。
使用/来表示资源的层级关系。

统一资源接口

RESTful架构应该遵循统一接口原则,统一接口包含了一组受限的预定义操作,不论什么样的资源,都是通过使用相同的接口进行资源的访问。

接口应该使用标准的HTTP方法如GET,PUT和POST,并遵循这些方法的语义。

GET和HEAD请求都是安全的,无论请求多少次,都不会改变服务器状态。
GET、HEAD、PUT和DELETE请求都是幂等的,无论资源操作多少次,结果总是一样的,后面请求并不会产生比第一次更多的影响。

GET

  • 安全且幂等
  • 表示获取
  • 变更时获取表示(缓存)
  • 200(OK)- 表示已在响应中发出
  • 204(无内容)- 资源有空表示
  • 301(Moved Permanently)- 资源的URI已被更新
  • 303(See Other)- 其他
  • 304(Not Modified)- 资源未更改(缓存)
  • 400(Bad Request)- 坏请求(如:参数错误)
  • 404(Not Found)- 资源不存在
  • 406(Not Acceptable)- 服务端不支持所需表示
  • 500(Internal Server Error)- 通用错误响应
  • 503(Service Unavailable)- 服务端当前无法处理请求

POST

  • 不安全且不幂等
  • 使用服务端管理的(自动产生)的实例号创建资源
  • 创建子资源
  • 部分更新资源
  • 200(OK)- 现有资源被更改
  • 201(Create)- 新资源被创建
  • 202(Accepted)- 已接受处理请求但尚未完成(异步处理)
  • 301(Moved Permanently)- 资源的URI已被更新
  • 303(See Other)- 其他
  • 400(Bad Request)- 坏请求(如:参数错误)
  • 404(Not Found)- 资源不存在
  • 406(Not Acceptable)- 服务端不支持所需表示
  • 409(Confilct)- 通用冲突
  • 412(Precondition Failed)- 前置条件失败(如:执行条件更新时的冲突)
  • 415(Unsupported Media Type)- 接收到的表示不支持
  • 500(Internal Server Error)- 通用错误响应
  • 503(Service Unavailable)- 服务端当前无法处理请求

PUT

  • 不安全但幂等
  • 用客户端管理的实例号创建一个资源
  • 通过替换的方式更新资源
  • 200(OK)- 已存在资源被修改
  • 201(Create)- 新资源被创建
  • 301(Moved Permanently)- 资源的URI已被更新
  • 303(See Other)- 其他
  • 400(Bad Request)- 坏请求(如:参数错误)
  • 404(Not Found)- 资源不存在
  • 406(Not Acceptable)- 服务端不支持所需表示
  • 409(Confilct)- 通用冲突
  • 412(Precondition Failed)- 前置条件失败(如:执行条件更新时的冲突)
  • 415(Unsupported Media Type)- 接收到的表示不支持
  • 500(Internal Server Error)- 通用错误响应
  • 503(Service Unavailable)- 服务端当前无法处理请求

DELETE

  • 不安全但幂等
  • 删除资源
  • 200(OK)- 资源被删除
  • 301(Moved Permanently)- 资源的URI已被更新
  • 303(See Other)- 其他
  • 400(Bad Request)- 坏请求(如:参数错误)
  • 404(Not Found)- 资源不存在
  • 409(Confilct)- 通用冲突
  • 500(Internal Server Error)- 通用错误响应
  • 503(Service Unavailable)- 服务端当前无法处理请求

资源的表述

JSON和XML

RESTful路由

OpenStack各个项目都提供了RESTful架构的API作为对外提供的接口,而RESTful架构的核心是资源与资源上的操作。
OpenStack中所用的路由模块Routes源自于对Rails路由系统的重新实现。Rails(Ruby on Rails)是Ruby语言的Web开发框架,采用MVC(Model-View-Controller)模式,收到浏览器发出的HTTP请求后,Rails路由系统会将这个请求指派到对应的Controller。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from routes import Mapper

mapper = Mapper()
mapper.connect(None, '/error/{action}/{id}', controller='error')
mapper.connect('home', '/', controller='main', action='index')

result = mapper.match('/error/test/1')
print result

result = mapper.match('/')
print result

{'action': u'test', 'controller': u'error', 'id': u'1'}
{'action': u'index', 'controller': u'main'}

WSGI

WSGI(Web Server Gateway Interface,Web服务器网关接口)是Python语言中定义的Web服务器和Web应用程序或框架之间的通用接口标准。当处理一个WSGI请求时,服务端为应用端提供上下文信息和一个回调函数,应用端处理完请求后,使用服务端所提供的回调函数返回相对应请求的响应。

作为一个桥梁,WSGI将Web组件分成3类:

  • Web服务器(WSGI Server)
  • Web中间件(WSGI Middleware)
  • Web应用程序(WSGI Application)。

WSGI Server接收HTTP请求,封装一系列环境变量,按照WSGI接口标准调用注册的WSGI Application,最后将响应返回给客户端。

WSGI Application是一个可被调用的(Callable)的Python对象,它接收两个参数,通常为environ和start_response。

1
2
3
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
yield 'Hello World \n'

参数environ指向一个Python字典,要求里面至少包含一些在CGI(通用网关接口规范)中定义的环境变量,如:

  • AUTH-TYPE
  • CONTENT_LENGTH:HTTP请求中Content-Length的部分。
  • CONTENT_TYPE:HTTP请求中Content-Type的部分。
  • GATEWAY_INTERFACE
  • HTTP_*:包含一系列变量,如HTTP_HOST、HTTP_ACCEPT等。
  • PATH_INFO:浏览器用GET方式发送数据时的附加路径。
  • PATH_TRANSLATED
  • QUERY_STRING:表单输入的数据,URL中问号后的内容。
  • REMOTE_ADDR
  • REMOTE_HOST
  • REMOTE_IDENT
  • REMOTE_USER
  • RESQUST_METHOD:请求方法(POST、GET)
  • SCRIPT_NAME:CGI程序的路径名。
  • SERVER_NAME
  • SERVER_PORT
  • SERVER_PROTOCOL:客户端的请求协议(HTTP/1.1 HTTP/1.0)
  • SERVER_SOFTWARE

除此之外,environ里面还至少要包含其他7个WSGI所定义的环境变量,如:

  • wsgi.version:值形式为(1,0)表示WSGI版本1.0
  • wsgi.url_scheme:表示url的模式,如:http或者https。
  • wsgi.input:输入流,HTTP请求的body部分可以从这里读取。
  • wsgi.errors:输入流,如果出现错误,可以写在这里。
  • wsgi.multithread:如果应用程序对象可以被同一进程的另一线程同时调用,这个值为True。
  • wsgi.multiprocess:如果应用程序对象可以同时被另一个进程调用,这个值为True。
  • wsgi.run_once:如果服务器希望应用程序对象包含它的进程中只被调用一次,这个值为True。

参数start_response指向一个回调函数,形如:

1
start_response(status, response_headers, exc_info=None)

status参数是一个形如“200 OK”的表示响应状态的字符串。
response_headers参数是一个包含了(header_name,header_value)元组的列表,分别表示HTTP响应中的HTTP头及内容。
exec_info 一般会在出现错误时使用,用来让浏览器显示相关错误信息。

参数start_response所指向的这个回调函数需要返回类似write(body_data)的可被调用对象。

有请求到来时,WSGI Server会准备好environ和start_response参数,然后调用WSGI Application获得对应请求的响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def call_application(app, environ):
body = []
status_headers = [None, None]

# 定义start_response回调函数
def start_response(status, headers):
status_headers[:] = [status, headers]
return body.append(status_headers)

app_iter = app(environ, start_response)
try:
for item in app_iter:
body.append(item)
finally:
if hasattr(app_iter, 'close'):
app_iter.close()
return status_headers[0], status_headers[1], ''.join(body)

WSGI中间件同时实现了服务端和应用端的API,因此可以在两端之间起协调作用。从服务器端看,中间件就是一个WSGI应用;从应用端方面看,中间件是一个WSGI服务器。

WSGI中间件可以将客户端的HTTP请求,路由给不同的应用对象,然后将应用处理后的结果返回给客户端。

Paste

OpenStack使用Paste的Deploy组件来完成WSGI服务器和应用的构建,每个项目源码的etc目录下都有一个Paste配置文件,比如Nova中的etc/nova/api-paste.ini,部署时,这些配置文件会被复制到系统/etc/[project]/目录下。Paste Deploy的工作便是基于这些配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[composite:main]
use = egg:Paste#urlmap
/ = home
/blog = blog
/wiki = wiki
/cms = config:cms.ini

[app:home]
use = egg:Paste#static
document_root = %(here)s/htdocs

[filter-app:blog]
use = egg:Authentication#auth
next = blogapp
roles = admin
htpasswd = /home/me/users.htpasswd

[app:blogapp]
use = egg:BlogApp
database = sqlite:/home/me/blog.db

[app:wiki]
use = call:mywiki.main:application
database = sqlite:/home/me/wiki.db

Paste配置文件分为多个section。每个section以type:name的格式命名。

  1. type = composite
    这个类型的section会把URL请求分发到对应的Application,use表明具体的分发方式,比如:”egg:Paste#urlmap“表示使用Paste包中的urlmap模块,这个section里的其他形如”key = value“的行是使用urlmap进行分发时的参数。

  2. type = app
    一个app就是一个具体的WSGI Application,这个app对应的Python代码则由use来指定。共有两种方法。

  • 第一种

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [app:myapp]
    # 从另外一个config.ini文件中寻找app
    use = config:another_config_file.ini#app_name

    [app:myotherapp]
    # 从Python EGG中寻找
    use = egg:MyApp

    [app:mythirdapp]
    # 直接调用另外一个模块的MyApp
    use = call:my.project:MyApp

    [app:mylastapp]
    # 从另外一个section中
    use = myotherapp
  • 另一种指定方法是明确指明对应的Python代码,这时必须给出代码所应该符合的格式,比如paste.app_factory。

    1
    2
    3
    [app:myapp]
    # myapp.modulename将被加载,并从中获取app_factory对象
    paste.app_factory = myapp.modulename:app_factory

    app_factory格式。

    1
    2
    def factory(loader, global_config, **local_conf):
    return wsgi_app
  1. type = filter-app
    收到一个请求后,会首先调用filter-app中的use所指定的app进行过滤,如果这个请求没有被过滤,就会被转发到next指定的app进行下一步的处理。

  2. type = filter
    与filter-app类型类似,只是没有next。

  3. type = pipeline
    pipeline由一系列的filter组成,这个filter链条末尾就是一个app。pipeline类型主要是对filter-app进行了简化,否则,如果有多个filter,就会需要写多个filter-app,然后使用next进行连接。

    1
    2
    3
    4
    5
    [pipeline:main]
    pipeline = filter1 filter2 filter3 app

    [filter:filter1]
    ...

使用Paste Deploy的主要目的就是从配置文件中生成一个WSGI Application,有了配置文件之后,只需要使用下面的调用方式。

1
2
from paste.deploy import loadapp
wsgi_app = loadeapp('config:/path/to/config.ini')

对于OpenStack,这里以Nova为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# nova/wsig.py

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

def __init__(self, config_path=None):
"""Initialize the loader, and attempt to find the config.

:param config_path: Full or relative path to the paste config.
:returns: None

"""
self.config_path = None

config_path = config_path or CONF.wsgi.api_paste_config
if not os.path.isabs(config_path):
self.config_path = CONF.find_file(config_path)
elif os.path.exists(config_path):
self.config_path = config_path

if not self.config_path:
raise exception.ConfigNotFound(path=config_path)

def load_app(self, name):
"""Return the paste URLMap wrapped WSGI application.

:param name: Name of the application to load.
:returns: Paste URLMap object wrapping the requested application.
:raises: `nova.exception.PasteAppNotFound`

"""
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:
LOG.exception(_LE("Couldn't lookup app: %s"), name)
raise exception.PasteAppNotFound(name=name, path=self.config_path)

WebOb

除了Routes与Paste Deploy外,OpenStack中另一个与WSGI密切相关的是WebOb。WebOb通过对WSGI的请求与响应进行封装,来简化WSGI应用的编写。

WebOb中两个最重要的对象,一是webob.Request,对WSGI请求的environ参数进行封装;一是webob.Response,包含了标准WSGI响应的所有要素。此外,还有一个webob.exc对象,针对HTTP错误代码进行封装。

除了这三个对象,WebOb还提供了一个修饰符(decorator),即webob.dec.wsgify,以便可以不使用原始的WSGI参数和返回格式,而全部使用WebOb替代。

1
2
3
@wsgify
def myfunc(req):
return webob.Response('Hello world!')

调用的时候可以有两种选择。

1
app_iter = myfunc(environ, start_response)

1
resp = myfunc(req)

第一种是最原始最标准的WSGI格式,第二种是WebOb封装过后的格式。

用户也可以使用参数对wsgify修饰符进行定制,比如使用webob.Request的子类,对真正的Request做一些判断或者过滤。

1
2
3
4
5
6
7
8
9
10
11
class MyRequest(webob.Request):
@property
def is_local(self):
return self.remote_addr == '127.0.0.1'

@wsgify(RequestClass=MyRequest)
def myfunc(req):
if req.is_local:
return Response('Hello world!')
else:
raise webob.exc.HTTPForbidden

以Nova为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# nova/wsgi.py

import webob.dec
import webob.exc

class Request(webob.Request):
def __init__(self, environ, *args, **kwargs):
if CONF.wsgi.secure_proxy_ssl_header:
scheme = environ.get(CONF.wsgi.secure_proxy_ssl_header)
if scheme:
environ['wsgi.url_scheme'] = scheme
super(Request, self).__init__(environ, *args, **kwargs)


class Middleware(Application):
"""Base WSGI middleware.

These classes require an application to be
initialized that will be called next. By default the middleware will
simply call its wrapped app, or you can override __call__ to customize its
behavior.

"""
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, req):
response = self.process_request(req)
if response:
return response
response = req.get_response(self.application)
return self.process_response(response)

Pecan

随着OpenStack项目的发展,Paste组合框架的Restful API代码的弊端也渐渐显现,代码过于臃肿,导致项目的可维护性变差。为了解决这个问题,一些新项目选了Pecan框架来实现Restful API。

Pecan是一个轻量级的WSGI网络框架,其设计并不想解决Web世界的所有问题,而是主要集中在对象路由和Restful支持上,并不提供对话(session)和数据库支持,用户可以选择其他模块与之组合。

Pecan的配置对位于config.py文件内,以neutron项目为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
CONF = cfg.CONF
CONF.import_opt('bind_host', 'neutron.common.config')
CONF.import_opt('bind_port', 'neutron.common.config')


def setup_app(*args, **kwargs):
config = {
'server': {
'port': CONF.bind_port,
'host': CONF.bind_host
},
'app': {
'root': 'neutron.pecan_wsgi.controllers.root.RootController',
'modules': ['neutron.pecan_wsgi'],
}
#TODO(kevinbenton): error templates
}
pecan_config = pecan.configuration.conf_from_dict(config)

app_hooks = [
hooks.ExceptionTranslationHook(), # priority 100
hooks.ContextHook(), # priority 95
hooks.BodyValidationHook(), # priority 120
hooks.OwnershipValidationHook(), # priority 125
hooks.QuotaEnforcementHook(), # priority 130
hooks.NotifierHook(), # priority 135
hooks.PolicyHook(), # priority 140
]

app = pecan.make_app(
pecan_config.app.root,
debug=False,
wrap_app=_wrap_app,
force_canonical=False,
hooks=app_hooks,
guess_content_type_from_ext=True
)
startup.initialize_all()

return app


def _wrap_app(app):
app = request_id.RequestId(app)
if cfg.CONF.auth_strategy == 'noauth':
pass
elif cfg.CONF.auth_strategy == 'keystone':
app = auth_token.AuthProtocol(app, {})
else:
raise n_exc.InvalidConfigurationOption(
opt_name='auth_strategy', opt_value=cfg.CONF.auth_strategy)

# version can be unauthenticated so it goes outside of auth
app = versions.Versions(app)

# This should be the last middleware in the list (which results in
# it being the first in the middleware chain). This is to ensure
# that any errors thrown by other middleware, such as an auth
# middleware - are annotated with CORS headers, and thus accessible
# by the browser.
app = cors.CORS(app, cfg.CONF)
app.set_latent(
allow_headers=['X-Auth-Token', 'X-Identity-Status', 'X-Roles',
'X-Service-Catalog', 'X-User-Id', 'X-Tenant-Id',
'X-OpenStack-Request-ID'],
allow_methods=['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
expose_headers=['X-Auth-Token', 'X-Subject-Token', 'X-Service-Token',
'X-OpenStack-Request-ID']
)

return app

Pecan的配置文件使用的也是Python,每个配置项都是一个Python字典,其中server指定了WSGI应用运行的主机和端口,app指定了WSGI app有关的一些配置。

modules项是一个Python模块列表,Pecan会在modules里寻找WSGI应用。Pecan使用了对象路由的方式把一个HTTP请求映射到Controller的方法上。具体来说,当用户访问某个URL的时候,Pecan会将路径分割成许多部分,从根控制器(Root Controller)开始,沿着对象路径寻找到要执行的函数,root项指定了根控制器的位置。

Eventlet

Eventlet将协程又称为GreenThread(绿色线程)。所谓并发,就是创建多个GreenThread,并对其进行管理。

1
2
3
4
5
6
7
8
9
import eventlet

def output(a):
print a


gt = eventlet.spawn(output, 'Hello world')

gt.wait()

eventlet.spawn会新建一个GreenThread来运行my_func函数。由于GreenThread不会进行抢占式调度,所以此时这个新建的GreenThread只是标记为可调度,并不会立即调度执行。只有当主程序执行到gt.wait()时,这个GreenThread才有机会被调度去执行output()函数。

协程

目前,OpenStack中的绝大部分项目都采用所谓的协程(coroutine)模型。从操作系统的角度来看,一个OpenStack服务只会运行在一个进程中,但是在这个进程中,OpenStack利用Python库Eventlet可以产生出许多个协程。这些个协程之间只有在调用到某些特殊的Eventlet库函数的时候(比如睡眠sleep,I/O调用等)才会发生切换。

与线程类似,协程也是一个执行序列,拥有自己的独立的栈与局部变量,同时又其他协程共享全局变量。协程与线程的主要区别是,多个线程可以同时运行,而同一时间内只能有一个协程在运行,无须考虑很多锁的问题,因此开发和调试也更加简单方便。

使用协程时,线程的执行完全由操作系统控制,进程的调度会决定什么时候哪个线程应该占用CPU。而使用协程时,协程的执行顺序与时间完全由程序自己决定。如果某个工作比较消耗时间或需要等待某些资源,协程可以自己主动让出CPU休息,其他的协程工作一段时间后同样会主动把CPU让出,这样一来,我们可以控制各个任务的执行顺序,从而最大可能地利用CPU性能。

协程的实现主要是在协程休息时把当前的寄存器保存起来,然后重新工作时再将其恢复,可以简单地理解为,在单个线程内部有多个栈去保存切换时的线程上下文,因此,协程可以理解为一个线程内的伪并发方式。