我们以keystone为例一步步看wsgi服务器的启动与paste.deploy的url映射
先截取一部分ini文件先做大致说明
[composite:main]
use = egg:Paste#urlmap
/v2.0 = public_api
/v3 = api_v3
/ = public_version_api
[pipeline:api_v3]
# The last item in this pipeline must be service_v3 or an equivalent
# application. It cannot be a filter.
pipeline = cors sizelimit url_normalize request_id admin_token_auth build_auth_context token_auth json_body ec2_extension_v3 s3_extension service_v3
[filter:token_auth]
use = egg:keystone#token_auth
token_auth = keystone.middleware:TokenAuthMiddleware.factory
[app:service_v3]
use = egg:keystone#service_v3
service_v3 = keystone.version.service:v3_app_factory
当一个请求过来的时候,先去composite:main找到对应url path 比如来了一个v3请求, 这时候被映射到pipeline:api_v3, 然后这个请求会被pipeline:api_v3列表中的所有filter处理一遍,里面每个filter都要定义, pipeline的最后的一个是为
service_v3 在paste.ini里他指向一个app
[app:service_v3]
use = egg:keystone#service_v3
问题1:名字相同怎么办——不会存在名字相同,否则会在启动时报错
问题2:这玩意怎么工作的——这就比较复杂了
urlmap映射用的是paste.deploy, paste是一个包,包名叫Paste,rpm名字为python-paste,可以yum, deploy是paste的扩展包,包名字叫PasteDeploy,这玩意要自己打包,版本很久没更新了,最新1.5.2, 附上spec文件(openstack用的旧版)
%define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(0)")
Name: python-PasteDeploy
Version: 1.5.2
Release: 0%{?dist}
Summary: Load, configure, and compose WSGI applications and servers
Group: Libraries/Python
License: Python
URL: http://pythonpaste.org/deploy/
Source0: PasteDeploy-%{version}.tar.gz
BuildArch: noarch
BuildRequires: python-devel
BuildRequires: python-setuptools >= 0.6-0.a9.1
Requires: python >= 2.6
Requires: python-paste
%description
This tool provides code to load WSGI applications and servers from
URIs; these URIs can refer to Python Eggs for INI-style configuration
files. Paste Script provides commands to serve applications based on
this configuration file.
%prep
%setup -q -n PasteDeploy-%{version}
%build
CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
%install
%{__python} setup.py install -O1 --skip-build --root %{buildroot}
install -D -m 644 README %{buildroot}%{_docdir}/%{name}-%{version}
install -D -m 644 PKG-INFO %{buildroot}%{_docdir}/%{name}-%{version}
install -D -m 644 MANIFEST.in %{buildroot}%{_docdir}/%{name}-%{version}
#install -D -m 644 docs/news.txt %{buildroot}%{_docdir}/%{name}-%{version}
#install -D -m 644 docs/license.txt.in %{buildroot}%{_docdir}/%{name}-%{version}
#install -D -m 644 doc/index.txt %{buildroot}%{_docdir}/%{name}-%{version}
%files
%doc README PKG-INFO MANIFEST.in
%{python_sitearch}/paste/deploy
%{python_sitearch}/PasteDeploy-%{version}-py*.egg-info
%{python_sitearch}/PasteDeploy-%{version}-py*-nspkg.pth
%changelog
* Sat Aug 20 2016 gcy - 1.5.2
- gcy build it for Redhat EL 6
paste.deploy这玩意比较蛋痛,通过egg来动态载入模块(openstack里自己的模块使用import util)
所以必须安装setuptool,并要求python的包里带有egg信息
因为这玩意load的时候,都是通过包内egg-info下的entry_points.txt来映射的
用起来有点像java的struct, 所以python的rpm包的时候必须带入egg info
deploy中有3种load函数
loadapp
配置中的composite:、pipeline:、app:都由他来处理(还有一个filter-app,keyston未使用)
因为这三个开头的都是用loadapp,所以他们的后面的name不可以相同(会报错)
比如有了composite:main,就不能再有app:main ---
loadfilter
配置中的filter:都由他处理 ---
loadserver
配置中的server:都由他处理,openstack的各个服务没有用它来管理自己的wsig,都是自己管理
keystone甚至可以用uwsig来运行,配合nginx、apache ---
我们现在来看启动过程, init里直接调用的keyston-all来启动, 实际就是启动两个server
# 通过绿色线程启动两个进程的过程大致如下,count不配置的话直接用cpu的数量
def create_servers():
admin_worker_count = _get_workers('admin_workers')
public_worker_count = _get_workers('public_workers')
servers = []
servers.append(create_server(paste_config,
'admin',
CONF.eventlet_server.admin_bind_host,
CONF.eventlet_server.admin_port,
admin_worker_count))
servers.append(create_server(paste_config,
'main',
CONF.eventlet_server.public_bind_host,
CONF.eventlet_server.public_port,
public_worker_count))
return servers
后面
def create_server(conf, name, host, port, workers):
app = keystone_service.loadapp('config:%s' % conf, name)
实际调用
def loadapp(conf, name):
# NOTE(blk-u): Save the application being loaded in the controllers module.
# This is similar to how public_app_factory() and v3_app_factory()
# register the version with the controllers module.
controllers.latest_app = deploy.loadapp(conf, name=name)
return controllers.latest_app
两个server对应的name分别为admin和main,都使用loadapp启动. 因为调用的是deploy.loadapp,所以会匹配到[composite:main]和[composite:admin]这两段配置
eventlet的封装里初始化了全局变量, 不存在因为全局变量问题必须多进程.
public 和admin的 worker count可以多进程也可以单进程
keyston主进程在调用launch_service之前(最终fork之前)先调用了listen,fork后通过
self.launcher = self._child_process(wrap.service)
下的launcher.launch_service(service)启动循环. socket数据接收直接在各个子进程 通过dup_socket = self.socket.dup() 复制出来的socket accept(看下面的说明来理解多进程接受数据). socket如何接受数据 处理分包粘包都是在eventlet.wsgi的代码中 对于监听一个socket来说,多个进程同时在accept处阻塞,当有一个连接进入,多个进程同时被唤醒,但之间只有一个进程能成功accept,而不会同时有多个进程能拿到该连接对象,操作系统保证了进程操作这个连接的安全性。
扩展:上述过程,多个进程同时被唤醒,去抢占accept到的资源,这个现象叫“惊群”,
而根据网上资料,Linux 内核2.6以下,accept响应时只有一个进程accept成功,其他都失败,
重新阻塞,也就是说所有监听进程同时被内核调度唤醒,这当然会耗费一定的系统资源。
而2.6以上,则已经不存在惊群现象了,但是由于开发者开发程序时使用了如epoll等异
这时子进程保存了父进程的文件描述符的所有副本,可以进程跟父进程一样的操作了。
我之前一直以为复制的socket的fd和普通文件的fd一样会有共同读写问题
原来操作系统级别已经避免掉这问题了
因为eventlet中socket accept的时候没有用异步,所以这里的多进程写起来就很简单了
现在终于搞清楚了,顺便,每个进程中也使用了协程来支持多个请求
接下来我们继续loapp的源码
def loadapp(uri, name=None, **kw):
return loadobj(APP, uri, name=name, **kw)
def loadobj(object_type, uri, name=None, relative_to=None,
global_conf=None):
context = loadcontext(
object_type, uri, name=name, relative_to=relative_to,
global_conf=global_conf)
return context.create()
第一次调用loadcontext传入的url,也就是paste_config, 值通过解析conf文件转为 config:/etc/keystone-paste.ini, name就是上层传入的name,下面我们以name为main做流程解析, url.split(‘:’) 后通过冒号前面的config找到实际调用函数.
def _loadconfig(object_type, uri, path, name, relative_to,
global_conf):
上面函数里面生成ConfigLoader类(就是我们常用解析ini文件的类的封装),并调用类中的get_context方法
get_context会通过传入的name和APP类的config_prefixes获取到section([composite:main])的内容并放入local_conf变量中
当section是pipeline或者local_conf中有"use"这个key的时候,有对应递归(看这里大致明白pipeline是怎么一级一级的调用filter了),
由于local_conf的key里有"use"(use = egg:Paste#urlmap),调用_context_from_use
_context_from_use 内部local_conf.pop弹出use对应values作为name(这次name为egg:Paste#urlmap)再调用get_context
__
这次又走到了loadcontext中实际函数为
def _loadegg(object_type, uri, spec, name, relative_to,
global_conf):
生成EggLoaderload类,
object_type为APP,name是urlmap,spec为Paste
调用这个类中get_context返回LoaderContext类
get_context中
entry_point, protocol, ep_name = self.find_egg_entry_point(object_type, name=name)
这里通过setuptool的对应工具找到Paste(spec)的egg配置中的paste.composite_factory即(通过APP(object_type)类的egg_protocols去匹配)
[paste.composite_factory]
urlmap = paste.urlmap.urlmap_factory
# 可以看到 urlmap映射到的代码位置为paste.urlmap.urlmap_factory
回到返回LoaderContext类
这部分代码里有个hack, LoaderContext的属性loader本来应该是EggLoaderload,
但是_context_from_use返回前把LoaderContext的loader覆盖为self,也就是ConfigLoader
_context_from_use在返回LoaderContext之前还处理了下LoaderContext中的protocol
后面的处理应该是为了pipeline做的
__
再回到前面的loadobj,这时候最后的create则是LoaderContext中的
def create(self):
return self.object_type.invoke(self)
当object_type为APP时, APP的invoke为
def invoke(self, context):
# invoke反射
if context.protocol in ('paste.composit_factory',
'paste.composite_factory'):
# fix_call看下面
return fix_call(context.object,
context.loader, context.global_conf,
**context.local_conf)
elif context.protocol == 'paste.app_factory':
return fix_call(context.object, context.global_conf, **context.local_conf)
else:
assert 0, "Protocol %r unknown" % context.protocol
def fix_call(callable, *args, **kw):
"""
Call ``callable(*args, **kw)`` fixing any type errors that come out.
"""
try:
val = callable(*args, **kw)
except TypeError:
exc_info = fix_type_error(None, callable, args, kw)
reraise(*exc_info)
return val
上面context.object就是就是之前传入的的entry_point,也就是urlmap_factory
def urlmap_factory(loader, global_conf, **local_conf):
if 'not_found_app' in local_conf:
not_found_app = local_conf.pop('not_found_app')
else:
not_found_app = global_conf.get('not_found_app')
if not_found_app:
not_found_app = loader.get_app(not_found_app, global_conf=global_conf)
urlmap = URLMap(not_found_app=not_found_app)
for path, app_name in local_conf.items():
path = parse_path_expression(path)
app = loader.get_app(app_name, global_conf=global_conf)
urlmap[path] = app
return urlmap
# URLMap类大致代码
class URLMap(paste.urlmap.URLMap):
...
# urlmap类是callable的
def __call__(self, environ, start_response):
host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower()
if ':' in host:
host, port = host.split(':', 1)
else:
if environ['wsgi.url_scheme'] == 'http':
port = '80'
else:
port = '443'
# 从environ中获取到path信息
path_info = environ['PATH_INFO']
path_info = self.normalize_url(path_info, False)[1]
# path映射到openstack的具体app line
mime_type, app, app_url = self._path_strategy(host, port, path_info)
到这里local_conf还剩下
/v2.0 = public_api
/v3 = api_v3
/ = public_version_api
通过ConfigLoader的get_app(注意前面红字部分),完成url的与调用app的map映射(字典) 到这里,path匹配到对应的处理方法完成 urlmap key对应的value作为wsgi的app, 接收wsgi传入的数据 app的具体初始化过程参考
后面就是通过绿色线程(eventlet.wsgi.server)处理监听端口过来的数据
然后会以key(url path) values(对应的映射方法)找到具体的openstack的app line (pipe, 也就是一串过滤器加最终的app)
最后wsig会以key(url path) values(对应的映射方法)找到具体的openstack的app line (pipe, 也就是一串过滤器加最终的app)
# tcp server, 处理socket来的数据
class Server(BaseHTTPServer.HTTPServer):
def process_request(self, sock_params):
sock, address = sock_params
# self.protocol 就是HttpProtocol
# 这里用了点技巧
# 先用用HttpProtocol.__new__生成类实例
# 修改部分属性后再调用init
proto = new(self.protocol)
if self.minimum_chunk_size is not None:
proto.minimum_chunk_size = self.minimum_chunk_size
proto.capitalize_response_headers = self.capitalize_response_headers
try:
# 把自己传到proto中
# BaseHTTPRequestHandler的第一个个参数request就是sock
proto.__init__(sock, address, self)
except socket.timeout:
# Expected exceptions are not exceptional
sock.close()
# similar to logging "accepted" in server()
self.log.debug('(%s) timed out %r' % (self.pid, address))
# http协议包处理,就是上面的proto.__init__(sock, address, self)
class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler):
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
# setup把socket的数据传入管道文件rfile
# 具体代码在BaseHTTPRequestHandler类中
self.setup()
try:
self.handle()
finally:
self.finish()
def setup(self):
# self.request就是socket对象
# 一般来说是eventlet替换过的绿色socket对象
conn = self.connection = self.request
if getattr(socket, 'TCP_QUICKACK', None):
try:
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, True)
except socket.error:
pass
try:
# 如果socket对象支持生成文件
self.rfile = conn.makefile('rb', self.rbufsize)
self.wfile = conn.makefile('wb', self.wbufsize)
except (AttributeError, NotImplementedError):
if hasattr(conn, 'send') and hasattr(conn, 'recv'):
# it's an SSL.Connection
# ssl socket对象没有makefile功能
# 调用soket原生的_fileobject方法
self.rfile = socket._fileobject(conn, "rb", self.rbufsize)
self.wfile = socket._fileobject(conn, "wb", self.wbufsize)
else:
# conn对象错误
raise NotImplementedError("wsgi.py doesn't support sockets "
"of type %s" % type(conn))
def handle(self):
self.close_connection = 1
self.handle_one_request()
# 循环接收数据
while not self.close_connection:
self.handle_one_request()
def handle_one_request(self):
# 从读管道rfile里读, 读取的第一行是url
self.raw_requestline = self.rfile.readline(self.server.url_length_limit)
# 这里是读出socket里的内容,生成environ
if not self.parse_request():
# 数据包不完整直接返回
return
...
# 前面已经从socket里读好数据并存放到当前实例的属性中
# 现在取出来
self.environ = self.get_environ()
# server.app,也就是self.application是一个URLMap类
# 也就是load_app所返回的所有对应openstack的app line组成的字典
self.application = self.server.app
try:
self.server.outstanding_requests += 1
try:
self.handle_one_response()
except socket.error as e:
# Broken pipe, connection reset by peer
if support.get_errno(e) not in BROKEN_SOCK:
raise
finally:
self.server.outstanding_requests -= 1
def handle_one_response(self):
...
# 这里是的application就是一个前面的URLMap类实例
# start_response用于回发数据
result = self.application(self.environ, start_response)
顺便,在nova-api中,配置文件是这样的
[app:metaapp]
paste.app_factory = nova.api.metadata.handler:MetadataRequestHandler.factory
匹配会走到
_context_from_explicit
这样就可以不绕egg找一圈直匹配到对应的类了
至于为什么keystone里用use = egg
[app:service_v3]
use = egg:keystone#service_v3
而nova-api里用比较直接的方式引过去,我反正是不打算纠结了,知道怎么映射过去的就差不多.
这里也可以看出openstack的代码风格也不怎么统一