目录
消息总线
概述
OpenStack遵循这样的设计原则:项目之间通过RESTful API进行通信;项目内部,不同服务进程之间通过消息总线进行通信。
oslo.messaging库通过以下两种方式来完成项目各个服务进程之间的通信。
远程过程调用(Remote Procedure Call,RPC)
通过远程过程调用,一个服务进程可以调用其他远程服务进程的方法,并且有两种方法:call和cast。通过call的方式调用,远程方法会被同步执行,调用者会被阻塞直到结果返回;通过cast的方式调用,远程方法会被异步执行,结果不会立即返回,调用者也不会被阻塞,但是调用者需要用其他方式查询这次远程调用的结果。事件通知(Event Notification)
某个服务进程可以把事件通知发送到消息总线上,该消息总线上所有对此类事件感兴趣的服务进程,都可以获得此事件通知并进行进一步的处理,处理的结果并不会发送给事件发送者。这种通信方式,不但可以在同一个项目内部的各个服务进程之间发送通知,还可以实现跨项目之间的通知发送。Ceilometer就通过这种方式大量获取其他OpenStack项目的事件通知,从而进行计量和监控。
AMQP
Openstack所支持的消息队列类型中,大部分都是基于AMQP(Advanced Message Queuing Protocol,高级消息队列协议)。
AMQP是一个异步消息传递所使用的开放的应用层协议规范,主要包括消息的导向、队列、路由、可靠性和安全性。oslo.messaging中支持的AMQP主要包括两版本,AMQP 0.9.1和AMQP 1.0,这两个版本有很大的差别。
AMQP架构。
对与一个实现了AMQP的中间件服务(Server/Broker)来说,当不同的消息由生产者(Producer)发送到Server时,它会根据不同的条件把消息传递给不同的消费者(Consumer)。如果消费者无法接收消息或者接收消息不够快时,它会把消息缓存在内存或者磁盘上。
生产者将消息发送给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,消息会被传递到所有绑定的队列上。 |
基于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链接连接到路由网中的某个路由,从而接入路由网。
当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架构。
示例
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 | CREATE TABLE users ( |
1 | from sqlalchemy.ext.declarative import declarative_base |
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 | https://github.com/git |
统一资源接口
RESTful架构应该遵循统一接口原则,统一接口包含了一组受限的预定义操作,不论什么样的资源,都是通过使用相同的接口进行资源的访问。
接口应该使用标准的HTTP方法如GET,PUT和POST,并遵循这些方法的语义。
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 | from routes import Mapper |
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 | def application(environ, start_response): |
参数start_response指向一个回调函数,形如:
1 | start_response(status, response_headers, exc_info=None) |
参数start_response所指向的这个回调函数需要返回类似write(body_data)的可被调用对象。
有请求到来时,WSGI Server会准备好environ和start_response参数,然后调用WSGI Application获得对应请求的响应。
1 | def call_application(app, environ): |
WSGI中间件同时实现了服务端和应用端的API,因此可以在两端之间起协调作用。从服务器端看,中间件就是一个WSGI应用;从应用端方面看,中间件是一个WSGI服务器。
WSGI中间件可以将客户端的HTTP请求,路由给不同的应用对象,然后将应用处理后的结果返回给客户端。
Paste
OpenStack使用Paste的Deploy组件来完成WSGI服务器和应用的构建,每个项目源码的etc目录下都有一个Paste配置文件,比如Nova中的etc/nova/api-paste.ini,部署时,这些配置文件会被复制到系统/etc/[project]/目录下。Paste Deploy的工作便是基于这些配置文件。
1 | [composite:main] |
Paste配置文件分为多个section。每个section以type:name的格式命名。
type = composite
这个类型的section会把URL请求分发到对应的Application,use表明具体的分发方式,比如:”egg:Paste#urlmap“表示使用Paste包中的urlmap模块,这个section里的其他形如”key = value“的行是使用urlmap进行分发时的参数。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_factoryapp_factory格式。
1
2def factory(loader, global_config, **local_conf):
return wsgi_app
type = filter-app
收到一个请求后,会首先调用filter-app中的use所指定的app进行过滤,如果这个请求没有被过滤,就会被转发到next指定的app进行下一步的处理。type = filter
与filter-app类型类似,只是没有next。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 | from paste.deploy import loadapp |
对于OpenStack,这里以Nova为例。
1 | # nova/wsig.py |
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 |
|
调用的时候可以有两种选择。
1 | app_iter = myfunc(environ, start_response) |
或
1 | resp = myfunc(req) |
第一种是最原始最标准的WSGI格式,第二种是WebOb封装过后的格式。
用户也可以使用参数对wsgify修饰符进行定制,比如使用webob.Request的子类,对真正的Request做一些判断或者过滤。
1 | class MyRequest(webob.Request): |
以Nova为例。
1 | # nova/wsgi.py |
Pecan
随着OpenStack项目的发展,Paste组合框架的Restful API代码的弊端也渐渐显现,代码过于臃肿,导致项目的可维护性变差。为了解决这个问题,一些新项目选了Pecan框架来实现Restful API。
Pecan是一个轻量级的WSGI网络框架,其设计并不想解决Web世界的所有问题,而是主要集中在对象路由和Restful支持上,并不提供对话(session)和数据库支持,用户可以选择其他模块与之组合。
Pecan的配置对位于config.py文件内,以neutron项目为例。
1 | CONF = cfg.CONF |
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 | import eventlet |
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性能。
协程的实现主要是在协程休息时把当前的寄存器保存起来,然后重新工作时再将其恢复,可以简单地理解为,在单个线程内部有多个栈去保存切换时的线程上下文,因此,协程可以理解为一个线程内的伪并发方式。