OpenStack是目前主流的开源云计算平台,它通过解耦的架构分别提供了计算、快存储、镜像、网络、认证等服务,其中镜像服务是六大核心组件之一。Glance实现了OpenStack平台的Image service,为云平台提供镜像上传、下载和管理等功能,通过对Glance源码的深入解读,让我们更深层次得认识这个简单而又复杂的镜像服务。
Glance项目全部由Python编写,读者需要掌握Python语法和wsgi、evenlet、webob、paste等类库,生产环境的镜像一般存在分布式存储中因此还会涉及部分Ceph基础知识。
深入学习一个项目前,我们要了解这个项目的作用和使用方法。Glance提供了镜像上传和下载功能,也就是说管理员可以通过Glance来上传镜像,而普通用户可以通过Glance下载镜像进而启动虚拟机,还有用户的虚拟机快照也是保存在Glance中的。有人可能要问Glance是否实现了自己的存储引擎,实际上Glance只提供API和管理功能,并通过灵活的插件机制可以将镜像保存在本地硬盘、Ceph或者商业存储中。
Glance服务都可以通过同名的命令行工具来使用,最常用的几个命令分别是上传镜像、列举镜像和删除镜像。
# Upload image glance image-upload --file ./cirros.img --progress # List images glance image-list # Delete image glance image-delete $id |
通过这些命令就可以访问Glance的所有功能了,当然还有更多高级特性有待我们分析源码时再介绍。
前面使用glance命令其实就是Glance的组件之一glanceclient,其他核心组件还包括glance-api和glance-registry,其架构图如下。
除了glance-api和glance-registry本身的进程外,镜像服务还依赖认证服务Keystone、数据库MySQL和底层存储Ceph,注意与其他OpenStack项目相比Glance相对简单并不依赖消息队列RabbitMQ。
这些进程组合起来就实现了OpenStack的镜像服务,首先用户通过命令行工具或RESTful API发送请求,中间会经过Keystone实现的多租户认证,授权后会访问glance-api,这个服务会处理所有的API请求然后转发给glance-registry,这个服务实现了policy、quota等特性并把用户数据保存在数据库中,如果用户上传或下载镜像则会根据管理员的配置调用不同的后端存储driver,例如访问本地文件系统或者访问Ceph集群。
Glance架构相对简单,glance-api和glance-registry职责和定位也很清晰,通过driver的抽象目前已经支持LVM、Swift、S3、Ceph、Sheepdog和SAN等存储系统。
要体验和深入研究Glance,需要在本地环境部署服务,Glance可以通过RPM或者APT来安装,安装后就可以通过glance-api和glance-registry来启动服务,详细安装文档参考OpenStack Installation Guide。如果系统支持Systemd可以通过systemctl来启动,或者直接使用脚本文件。
systemctl start openstack-glance-api systemctl start openstack-glance-registry |
安装和部署Glance很简单,但在实际的生产环境中我们需要高可用的部署架构,我们的推荐架构是在三个API节点上分别部署glance-api和glance-registry,使用Systemd启动来保证进程持续运行,通过HAproxy和VIP实现负载均衡,而HAproxy和VIP都通过pacemaker管理以实现高可用。由于glance-api和glance-registry都是无状态的,因此在HA架构中可以实现多活同时提供服务。
在本地能够部署和体验镜像服务后,我们可以深入源码,解读OpenStack服务的实现机理。对于源码不理解的地方,我们可以通过pdb单步调试,进一步熟悉项目源码。
OpenStack项目最重要的入口时间是setup.cfg,通过glance-api脚本启动会调用glance.cmd.api库的main()函数,我们对代码进行适当的简化。
def main(): try : server = wsgi.Server(initialize_glance_store = True ) server.start(config.load_paste_app( 'glance-api' ), default_port = 9292 ) server.wait() except KNOWN_EXCEPTIONS as e: fail(e) if __name__ = = '__main__' : main() |
可以发现main()函数会启动一个wsgi服务器,这个服务器会加载glance-api-paste.conf文件的glance-api服务,并且服务器监听的端口是9292。
为了进一步了解这个服务是如何启动的,我们查看glance-api-paste.conf,并且找到相应的pipeline。
[pipeline:glance-api] pipeline = healthcheck versionnegotiation osprofiler unauthenticated-context rootapp [composite:rootapp] paste.composite_factory = glance.api:root_app_factory /: apiversions /v1: apiv1app /v2: apiv2app /v3: apiv3app |
如果想深入理解pipeline的作用,建议阅读paste库的官方文档,这里我们只需要glance-api最后会调用rootapp,也就是glance.api类里面的root_app_factory()函数。
因此我们进入/glance/api/__init__.py文件,找到root_app_factory()函数,它会根据管理员配置启动v1、v2或者v3的API服务。
def root_app_factory(loader, global_conf, * * local_conf): if not CONF.enable_v1_api and '/v1' in local_conf: del local_conf[ '/v1' ] if not CONF.enable_v2_api and '/v2' in local_conf: del local_conf[ '/v2' ] if not CONF.enable_v3_api and '/v3' in local_conf: del local_conf[ '/v3' ] return paste.urlmap.urlmap_factory(loader, global_conf, * * local_conf) |
如果只关注最常用的v2接口,我们在/glance/api/v2/router.py文件中找到API类,里面所有API请求对应进行处理的controller和action。
例如用户使用GET请求/images路径时,会列举所有用户镜像,这是使用的是images这个controller,并调用起index()函数。
images_resource = images.create_resource(custom_image_properties) mapper.connect( '/images' , controller = images_resource, action = 'index' , conditions = { 'method' : [ 'GET' ]}) mapper.connect( '/images' , controller = images_resource, action = 'create' , conditions = { 'method' : [ 'POST' ]}) mapper.connect( '/images' , controller = reject_method_resource, action = 'reject' , allowed_methods = 'GET, POST' , conditions = { 'method' : [ 'PUT' , 'DELETE' , 'PATCH' , 'HEAD' ]}) |
我们对index()进行简化,这里其实就是通过image_repo这个对象获取镜像信息,而这其实是数据库的一个对象,本质上就是查询数据库返回镜像列表信息。
def index( self , req, marker = None , limit = None , sort_key = None , sort_dir = None , filters = None , member_status = 'accepted' ): result = {} image_repo = self .gateway.get_repo(req.context) try : images = image_repo. list (marker = marker, limit = limit, sort_key = sort_key, sort_dir = sort_dir, filters = filters, member_status = member_status) if len (images) ! = 0 and len (images) = = limit: result[ 'next_marker' ] = images[ - 1 ].image_id except exception.NotAuthenticated as e: raise webob.exc.HTTPUnauthorized(explanation = e.msg) result[ 'images' ] = images return result def get_repo( self , context): image_repo = glance.db.ImageRepo(context, self .db_api) store_image_repo = glance.location.ImageRepoProxy( image_repo, context, self .store_api, self .store_utils) quota_image_repo = glance.quota.ImageRepoProxy( store_image_repo, context, self .db_api, self .store_utils) policy_image_repo = policy.ImageRepoProxy( quota_image_repo, context, self .policy) notifier_image_repo = glance.notifier.ImageRepoProxy( policy_image_repo, context, self .notifier) if property_utils.is_property_protection_enabled(): property_rules = property_utils.PropertyRules( self .policy) pir = property_protections.ProtectedImageRepoProxy( notifier_image_repo, context, property_rules) authorized_image_repo = authorization.ImageRepoProxy( pir, context) else : authorized_image_repo = authorization.ImageRepoProxy( notifier_image_repo, context) return authorized_image_repo |
了解过image list这个API的实现后,我们可以看看较为复杂的镜像上传实现,这涉及到对后端存储的调用。
上传镜像可以分为创建新镜像和上传文件两步,创建镜像通过文档可以找到请求的地址也是/images,方法变成POST,然后直接调用create()函数。
def create( self , request): body = self ._get_request_body(request) image = {} properties = body tags = properties.pop( 'tags' , []) return dict (image = image, extra_properties = properties, tags = tags) |
创建新镜像后可以获取image id,然后通过POST请求/images/{image_id}/file来上传文件,这时调用另一个controller的upload()函数。
image_data_resource = image_data.create_resource() mapper.connect( '/images/{image_id}/file' , controller = image_data_resource, action = 'download' , conditions = { 'method' : [ 'GET' ]}) mapper.connect( '/images/{image_id}/file' , controller = image_data_resource, action = 'upload' , conditions = { 'method' : [ 'PUT' ]}) mapper.connect( '/images/{image_id}/file' , controller = reject_method_resource, action = 'reject' , allowed_methods = 'GET, PUT' , conditions = { 'method' : [ 'POST' , 'DELETE' , 'PATCH' , 'HEAD' ]}) |
upload过程首先需要通过数据获取镜像信息,然后将镜像状态改为“saving”,接下来就是调用save()函数和set_data()函数。
def upload( self , req, image_id, data, size): image_repo = self .gateway.get_repo(req.context) image = None try : image = image_repo.get(image_id) image.status = 'saving' try : image_repo.save(image) image.set_data(data, size) image_repo.save(image, from_state = 'saving' ) |
其中save()函数主要是更新数据库内容,而set_data()会经过domain、quota、auth、location、proxy、notifier多个组件的依次调用,最终保存到存储后端还是要看/glance/location.py的实现。
def set_data( self , data, size = None ): location, size, checksum, loc_meta = self .store_api.add_to_backend(CONF, self .image.image_id, utils.LimitingReader(utils.CooperativeReader(data), CONF.image_size_cap), size, context = self .context) self .image.locations = [{ 'url' : location, 'metadata' : loc_meta, 'status' : 'active' }] self .image.size = size self .image.checksum = checksum self .image.status = 'active' def add_to_backend( self , conf, image_id, data, size, scheme = None , context = None ): self .data[image_id] = (data, size) checksum = 'Z' return (image_id, size, checksum, self .store_metadata) |
我们简化了set_data()的实现,发现这里只是调用store_api的add_to_backend()函数,后面就设置镜像状态为“active”了,前面会通过配置文件获取一个存储后端相关的scheme,以我们使用Ceph作为存储后端为例。
在/glance/common/location_strategy/__init__.py中获取不同后端的模块名,如rbd,然后调用add()函数实现写入Ceph存储集群中。
def _load_strategies(): """Load all strategy modules.""" modules = {} namespace = "glance.common.image_location_strategy.modules" ex = stevedore.extension.ExtensionManager(namespace) for module_name in ex.names(): try : mgr = stevedore.driver.DriverManager( namespace = namespace, name = module_name, invoke_on_load = False ) # Obtain module name strategy_name = str (mgr.driver.get_strategy_name()) if strategy_name in modules: msg = (_( '%(strategy)s is registered as a module twice. ' '%(module)s is not being used.' ) % { 'strategy' : strategy_name, 'module' : module_name}) LOG.warn(msg) else : # Initialize strategy module mgr.driver.init() modules[strategy_name] = mgr.driver except Exception as e: LOG.error(_LE( "Failed to load location strategy module " "%(module)s: %(e)s" ) % { 'module' : module_name, 'e' : e}) return modules |
这是比较典型的API实现流程,当然Glance还实现了诸如metadata、multi-location、quota等高级特性,这里就不一一叙述了。
最后总结下,Glance是标准的OpenStack项目架构,通过wsgi启动HTTP服务,通过paste来定制使用的组件,使用数据库来保存关键数据,熟悉Glance源码实现后看其他OpenStack项目也将更加得心应手。
陈迪豪,UnitedStack有云存储组PTL,目前专注于Docker、OpenStack社区。Docker监控管理工具Seagull项目作者,开源电子书《理解Linux进程》作者。