PS:分析中如有错误,请各位看官指正。
两篇的介绍后,这篇来深入到代码里看看。
Glance组件的代码托管在GitHub上,包括两个项目:glance、glance_store(store adapter),有兴趣可以clone下来深入研究。
前面提到一个服务glance-api,下面就以它为例,先来看下服务的启动流程:
# filename: glance/cmd/api.py
def main():
try:
# config.parse_args()中会确认要加载的配置文件的绝对路径
# 并且将配置文件中的配置存放到config.CONF中供后续调用
config.parse_args()
# 设置eventlet的hub,配置文件中没有该配置选项,代码中默认值为poll
wsgi.set_eventlet_hub()
log.setup('glance')
# 初始化后端存储,注册与后端存储(filesystem、eqlx、ceph等)相关的控制模块,
# 并校验默认存储方式。
glance_store.register_opts(config.CONF)
glance_store.create_stores(config.CONF)
glance_store.verify_default_store()
# 确认是否启用OpenStack分析功能
if cfg.CONF.profiler.enabled:
_notifier = osprofiler.notifier.create("Messaging",
notifier.messaging, {},
notifier.get_transport(),
"glance", "api",
cfg.CONF.bind_host)
osprofiler.notifier.set(_notifier)
else:
osprofiler.web.disable()
# 通过wsgi.Server()创建一个http服务器(使用了eventlet绿色线程(协程)),
# 调用start()方法启动服务器。
# server.wait()让server等待接收http rest查询。
server = wsgi.Server()
server.start(config.load_paste_app('glance-api'), default_port=9292)
server.wait()
具体看下server这一部分。 首先通过wsgi.Server实例化了一个server,然后通过server.start()启动server。其中通过config.load_paste_app(‘glance-api’)获得一个要加载的app。
先看下config.load_paste_app()函数相关代码,该函数最终返回一个app作为server.start()函数的application参数:
# filename glance/common/config.py
def load_paste_app(app_name, flavor=None, conf_file=None):
# 根据配置文件(glance-api.conf)的配置,确定最终的app_name,
# 例如配置文件中flavor=keystone,则app_name=glance-api-keystone
app_name += _get_deployment_flavor(flavor)
if not conf_file:
# 获取paste的配置文件的绝对路径
conf_file = _get_deployment_config_file()
try:
logger = logging.getLogger(__name__)
logger.debug("Loading %(app_name)s from %(conf_file)s",
{'conf_file': conf_file, 'app_name': app_name})
# 调用paste.deploy中的loadapp()函数加载paste配置文件中的app_name对应的app
# (例如glance-api-keystone)
app = deploy.loadapp("config:%s" % conf_file, name=app_name)
return app
def _get_deployment_flavor(flavor=None):
if not flavor:
# 读取配置文件(glance-api.conf)中[paste_deploy]配置块中flavor参数的配置。
# from oslo.config import cfg ==> CONF = cfg.CONF
# 这就属于oslo.config项目的内容了,有时间单开一篇介绍细节部分
flavor = CONF.paste_deploy.flavor
# 如果配置文件中配置了flavor,则返回'-参数内容',
# 例如配置文件中flavor=keystone,则返回'-keystone'
return '' if not flavor else ('-' + flavor)
def _get_deployment_config_file():
# 读取配置文件(glance-api.conf)中[paste_deploy]配置块中config_file参数的配置。
path = CONF.paste_deploy.config_file
if not path:
# 如果配置文件中未配置config_file参数,则根据program名字生成path
path = _get_paste_config_path()
if not path:
msg = _("Unable to locate paste config file for %s.") % CONF.prog
raise RuntimeError(msg)
return os.path.abspath(path)
这部分涉及到paste.deploy的相关内容,可以参考以下内容:
向外继续来看下server.start(self, application, default_port)
# filename: glance/common/wsgi.py
def start(self, application, default_port):
"""
Run a WSGI server with the given application.
:param application: The application to be run in the WSGI server
:param default_port: Port to bind to if none is specified in conf
"""
self.application = application
self.sock = get_socket(default_port)
if CONF.workers == 0:
# Useful for profiling, test, debug etc.
self.pool = self.create_pool()
self.pool.spawn_n(self._single_run, self.application, self.sock)
return
else:
self.logger.info(_("Starting %d workers") % CONF.workers)
signal.signal(signal.SIGTERM, kill_children)
signal.signal(signal.SIGINT, kill_children)
signal.signal(signal.SIGHUP, hup)
while len(self.children) < CONF.workers:
self.run_child()
def run_child(self):
pid = os.fork()
if pid == 0:
signal.signal(signal.SIGHUP, signal.SIG_DFL)
signal.signal(signal.SIGTERM, signal.SIG_DFL)
# ignore the interrupt signal to avoid a race whereby
# a child worker receives the signal before the parent
# and is respawned unnecessarily as a result
signal.signal(signal.SIGINT, signal.SIG_IGN)
self.run_server()
self.logger.info(_('Child %d exiting normally') % os.getpid())
# self.pool.waitall() has been called by run_server, so
# its safe to exit here
sys.exit(0)
else:
self.logger.info(_('Started child %s') % pid)
self.children.append(pid)
def run_server(self):
"""Run a WSGI server."""
if cfg.CONF.pydev_worker_debug_host:
utils.setup_remote_pydev_debug(cfg.CONF.pydev_worker_debug_host,
cfg.CONF.pydev_worker_debug_port)
eventlet.wsgi.HttpProtocol.default_request_version = "HTTP/1.0"
#创建一个eventlet协程池
self.pool = self.create_pool()
try:
#将application放到eventlet协程池里运行,start动作完成。
eventlet.wsgi.server(self.sock,
self.application,
log=logging.WritableLogger(self.logger),
custom_pool=self.pool,
debug=False)
except socket.error as err:
if err[0] != errno.EINVAL:
raise
self.pool.waitall()
def create_pool(self):
return eventlet.GreenPool(size=self.threads)
将application放到协程池里运行。这部分里面涉及到了eventlet里面的协程(GreenThread绿色线程),可以参考以下内容:
前面这部分涉及到了两种配置文件的读取:
- glance-api.conf:用户配置文件
- glance-api-paste.ini:WSGI 配置文件,
用户配置文件(glance-api.conf)的解析是通过glance.common.config.parse_args()实现的,具体来看下
# filename: glance/common/config.py
# 从oslo.config中导入cfg
from oslo.config import cfg
# 定义名为“paste_deploy”的参数组,其中包含两个选项:flavor、config_file
# 这两个参数对应了glance-api.conf配置文件里的以下部分的配置:
# [paste_deploy]
# flavor=
# config_file =
paste_deploy_opts = [
cfg.StrOpt('flavor',
help=_('Partial name of a pipeline in your paste configuration '
'file with the service name removed. For example, if '
'your paste section name is '
'[pipeline:glance-api-keystone] use the value '
'"keystone"')),
cfg.StrOpt('config_file',
help=_('Name of the paste configuration file.')),
]
CONF = cfg.CONF
# 注册多个选项
CONF.register_opts(paste_deploy_opts, group='paste_deploy')
# glance/cmd/api.py 中的config.parse_args() 部分调用该方法找到配置文件,
# 并将配置文件的内容加载到CONF中,需要获取某个配置项时,
# 通过CONF.groupname.option调用即可,
# 可以参考_get_deployment_flavor()中的例子
def parse_args(args=None, usage=None, default_config_files=None):
CONF(args=args,
project='glance',
version=version.cached_version_string(),
usage=usage,
default_config_files=default_config_files)
def _get_deployment_flavor(flavor=None):
if not flavor:
# 当需要获取配置文件中的flavor配置项时,
# 调用CONF.paste_deploy.flavor即可
flavor = CONF.paste_deploy.flavor
return '' if not flavor else ('-' + flavor)
以上这段代码截选自glance/common/config.py,对于组件配置文件(XXX.conf)的解析,glance并没有自己实现配置文件解析功能,而是引入了oslo.conf模块,该模块被所有OpenStack组件引用用于实现对配置文件的解析。
想要自己尝试写个例子,可以研究下这篇blog
WSGI 配置文件(glance-api-paste.ini)的解析是通过paste模块的deploy.loadapp()方法实现的,关于paste的deploy.loadapp()具体如何实现文件的解析不在这里展开,如果不熟悉paste模块可以看下这篇blog:
下面来分析下glance-api-paste.ini文件的部分内容
# Use this pipeline for keystone auth
[pipeline:glance-api-keystone]
pipeline = versionnegotiation osprofiler authtoken context rootapp
[composite:rootapp]
paste.composite_factory = glance.api:root_app_factory
/: apiversions
/v1: apiv1app
/v2: apiv2app
[app:apiversions]
paste.app_factory = glance.api.versions:create_resource
[app:apiv1app]
paste.app_factory = glance.api.v1.router:API.factory
[app:apiv2app]
paste.app_factory = glance.api.v2.router:API.factory
# 检查请求中的版本信息,如果请求的不是当前所支持的版本,则返回当前支持的版本信息
[filter:versionnegotiation]
paste.filter_factory = glance.api.middleware.version_negotiation:VersionNegotiationFilter.factory
# 用来enables tracing for an application。似乎是在 enabled = yes 的情况下,记录 request 的一些信息。具体功能需要后续研究。
[filter:osprofiler]
paste.filter_factory = osprofiler.web:WsgiMiddleware.factory
hmac_keys = SECRET_KEY
enabled = yes
# 该 filter 会通过校验request所带的 token 来校验用户身份。当request来的时候,它根据 token 来判断这个请求是否合法。校验失败则直接返回 401 错误,校验通过则提取 token 中的信息,放入到env中,供后续的其它app使用。
[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
delay_auth_decision = true
# 将认证信息保存到context中,供后续app调用。
[filter:context]
paste.filter_factory = glance.api.middleware.context:ContextMiddleware.factory
简单解释下上面这个文件中的几个术语:
-
app:实际处理REST API请求的python类。
-
filter:过滤器,为app提供一层封装,在app处理请求之前会先调用filter的对象。
-
pipeline:所对应的对象是对filter和app的的封装,它将多个filter和某个app绑在一起,在app处理请求之前要先通过pipline指定的在app之前的filter的处理。
-
composite:用来完成将将一个请求调度定向(dispatched)到多个(多种)应用上. 比如应用有 v1, v2 的版本, 就可以用 composite 来做调度. composite 其实像是 app, 但是实际上是由 多个应用组成.
在前面对glance/common/config.py的分析中已经介绍过load_paste_app(app_name, flavor=None, conf_file=None)函数,该函数会调用deploy.loadapp(“config:%s” % conf_file, name=app_name)解析conf_file文件,加载app_name对应的pipeline。
举个例子来说,如果我们在配置文件glance-api.conf文件中配置了flavor=keystone,此时app_name=glance-api-keystone,那么deploy.loadapp()会到glance-api-paste.ini文件中查找值为glance-api-keystone的pipeline或app,从上面的配置文件中,我们可以找到一个名为glance-api-keystone的pipeline,此时deploy.loadapp()会依次加载versionnegotiation、osprofiler、authtoken、context过滤器及最后的rootapp。
glance-api服务加载完filter和app后,即可响应REST API请求。
OK,下面了解下glance-api是如何处理REST API请求的。 前面我们看到glance-api服务加载了versionnegotiation、osprofiler、authtoken、context过滤器及一个rootapp,这里有个请求处理顺序的问题,当server接收到一个请求时,rootapp会依次调用前面的filter(过滤器)对请求进行处理,在处理过程中,如果某个filter的判断条件不满足,直接将filter反馈信息响应给客户端,不再将请求交给下个模块进行处理。具体的流程如下图所示:
当请求经过前面各个filter的处理后,最终来到rootapp,rootapp会根据请求信息将请求调度到对应的app,比如请求的是v1版本,rootapp会调度apiv1app这个app来处理请求。
apiv1app和apiv2app两个app的工厂方法的 API 类都继承自 wsgi.Router, Router 类利用了 python-routes 模块做请求信息URL的选择处理, 以v1版本为例,看下具体流程图:
OK,源码框架大致是这样的。具体一些的功能的实现源码分析后续再写。