OpenStack是目前主流的开源云计算平台,它通过解耦的架构分别提供了计算、快存储、镜像、网络、认证等服务,其中镜像服务是六大核心组件之一。Glance实现了OpenStack平台的Image service,为云平台提供镜像上传、下载和管理等功能,通过对Glance源码的深入解读,让我们更深层次得认识这个简单而又复杂的镜像服务。

Glance项目全部由Python编写,读者需要掌握Python语法和wsgi、evenlet、webob、paste等类库,生产环境的镜像一般存在分布式存储中因此还会涉及部分Ceph基础知识。

Glance服务都可以通过同名的命令行工具来使用,最常用的几个命令分别是上传镜像、列举镜像和删除镜像。

# Upload image glance image-upload --file ./cirros.img --progress   # List images glance image-list   # Delete image glance image-delete $id

系统架构

前面使用glance命令其实就是Glance的组件之一glanceclient,其他核心组件还包括glance-api和glance-registry,其架构图如下。

除了glance-api和glance-registry本身的进程外,镜像服务还依赖认证服务Keystone、数据库MySQL和底层存储Ceph,注意与其他OpenStack项目相比Glance相对简单并不依赖消息队列RabbitMQ。

   这个的glance-registry进行处理的?

这些进程组合起来就实现了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等存储系统。

源码解读

在本地能够部署和体验镜像服务后,我们可以深入源码,解读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)

这是比较典型的API实现流程,当然Glance还实现了诸如metadata、multi-location、quota等高级特性,这里就不一一叙述了。

总结

最后总结下,Glance是标准的OpenStack项目架构,通过wsgi启动HTTP服务,通过paste来定制使用的组件,使用数据库来保存关键数据,熟悉Glance源码实现后看其他OpenStack项目也将更加得心应手。




Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐