当前位置: 首页 > 科技观察

深入学习OpenstackNova组件对象模型和数据库访问机制

时间:2023-03-18 21:50:08 科技观察

1。背景介绍在OpenstackG版本之前,所有的nova服务(包括nova-compute服务)都是直接访问数据库的,数据库访问接口在nova/db/api.py模块中,这个模块只是调用了IMPL的方法,即,模块只是一个代理,真正的实现是通过IMPL来实现的。IMPL是一个可配置的动态加载驱动模块,通常使用Python的sqlalchemy库实现。对应的代码为nova.db.sqlalchemy.api:_BACKEND_MAPPING={'sqlalchemy':'nova.db.sqlalchemy.api'}这个模块不仅实现了模型的CRUD操作,还封装了一些高级的API,比如as:instance_get_all:获取所有虚拟机实例。instance_update:更新虚拟机熟悉度。...这种直接访问数据库的设计至少存在以下两个问题:所有服务都与数据模型耦合,当数据模型发生变化时,可能需要调整涉及所有代码,难以支持版本控制.所有主机都可以访问数据库,大大增加了数据库的暴露风险。为了实现Nova服务与数据库访问的解耦,从G版本开始引入了nova-conductor服务。该服务的一个重要功能是访问数据库。当其他服务访问数据库时,需要向nova-conductor发起RPC请求,以nova-conductor表示请求数据库。上面的方法基本解决了服务和数据库访问的解耦,避免了其他服务直接访问数据库,但是仍然没有解决对象模型的版本控制。从第一版开始就引入了对象模型的概念,所有的对象模型都定义在nova/objects中。在访问数据库之前,直接调用了数据库的model,比如更新一个flavor的字段,调用flavor的update方法(sqlalchemy实现)。引入对象模型后,相当于在服务和数据库之间增加了一个对象层。每个服务直接与资源对象交互,资源对象与数据库接口交互。当数据库返回时,会相应地转换成对象模型。目的。对象模型的对象不仅封装了数据库访问,还支持版本控制。每个对象维护一个版本号,发起RPC请求时必须指定对象的版本号。新版本的对象通常与旧版本的对象兼容。比如nova-conductor升级为使用对象模型1.2版本,但是nova-compute服务可能还没有升级,仍然使用1.1版本。当请求返回时,conductor将返回的对象转换为1.1版兼容对象。目前Cinder服务还是直接访问数据库。社区已经有对应的BP关于添加cinder-conductor服务createconductorserviceforcinderlikenova-conductor。这个BP是2013年6月提出的,最新的N版本还没有实现。2.Nova配置上面我们介绍了nova-conductor的背景和对象模型。我们了解到所有访问数据库的服务都必须通过RPC调用nova-conductor服务请求,但这不是强制的。如果不考虑数据库访问安全,你仍然可以使用本地访问方式,nova-compute服务可以直接访问数据库,不需要发起nova-conductorRPC调用。让我们看一下nova-compute服务的初始化,它位于nova/cmd/compute.y中:defmain():#...ifnotCONF.conductor.use_local:cmd_common.block_db_access('nova-compute')objects_base。novaObject.indirection_api=\conductor_rpcapi.ConductorAPI()else:LOG.warning(_LW('Conductorlocalmodeisdeprecatedandwill''beremovedinasubsequentrelease'))#...所以在/etc/nova.conf配置文件中可以配置是否直接访问数据库。上面indirection_api是Nova对象模型的一个字段,初始化为None。如果use_local设置为true,indirection_api为None,否则会初始化为conductor_rpcapi.ConductorAPI,从这里我们也可以看出调用conductor的入口。我们可能会想到,对象模型访问数据库的时候,会有一堆if-else来判断是否使用use_local。其实是这样吗?接下来我们通过源码分析,了解Openstack的设计理念。3.源码分析3.1nova-compute源码分析本节主要以删除虚拟机为例,分析nova-compute在删除虚拟机时是如何操作数据库的。删除虚拟机的API入口是nova/compute/manager.py的_delete_instance方法。方法原型为:_delete_instance(self,context,instance,bdms,quotas)这个方法有4个参数,context是上下文信息,包括user,Tenant等信息,instance就是我们说的对象模型中的Instance对象实例上面的bdms是blockDeviceMappingList对象实例,保存了块设备映射列表,quotas是nova.objects.quotas.Quotas对象实例,保存了租户的配额信息。该方法涉及的数据库操作代码为:instance.vm_state=vm_states.DELETEDin??stance.task_state=Noneinstance.power_state=power_state.NOSTATEinstance.terminated_at=timeutils.utcnow()instance.save()system_meta=instance.system_metadatainstance.destroy()来自代码中可以看到,首先更新实例的几个字段,然后调用save()方法保存到数据库中,最后调用destroy方法删除实例(注意这里的删除并不一定代表从数据库中删除记录,也有可能只是删除标记)。我们首先找到上面的save()方法,它位于nova/object/instance.py模块中。方法原型为:@base.remotablesave(self,expected_vm_state=None,expected_task_state=None,admin_state_reset=False)save方法将记录需要更新的字段通过调用db接口保存到数据库中。关键是这个方法的wrapperremotable。这个注解(python不叫注解,这里叫注解是为了习惯)很重要。这个方法在oslo中定义:defremotable(fn):"""Decoratorforremotableobjectmethods."""@six.wraps(fn)defwrapper(self,*args,**kwargs):ctxt=self._contextifctxtisNone:raiseexception.OrphanedObjectError(method=fn.__name__,objtype=self.obj_name())ifself.indirection_api:updates,result=self.indirection_api.object_action(ctxt,self,fn.__name__,args,kwargs)forkey,valueinsix.iteritems(updates):ifkeyinself。fields:field=self.fields[key]#NOTE(ndipanov):SinceVersionedObjectSerializerwillhave#deserializedanyobjectfieldsintoobjectsalreadytoga.ifisinstance(value,VersionedObject):setattr(self,key,value)其他:setattr(self,key,field.from_primitive(self,key),value))self.obj_reset_changes()self._changed_fields=set(updates.get('obj_what_changed',[]))returnresultelse:returnfn(self,*args,**kwargs)wrapper.remotable=Truewrapper.original_fn=从代码中看到的fnreturnwrapper,间接时当ion_api不为None时,会调用indirection_api的object_action方法。我们知道这个值是由配置项use_local决定的。当use_local为False时,indirection_api为conductor_rpcapi。ConductorAPI理解对象并不会通过一堆if-else来判断是否使用use_local,而是通过@remotable注解,remotable封装了if-else,使用local时直接调用原对象实例的save方法,否则调用indirection_api的object_action方法。注意:除了@remotable注解外,还定义了@remotable_classmethod注解。注解功能类似于@remotable,但相当于封装了另一个@classmethod注解。3.2RPC调用之前我们分析了调用conductor_rpcapi.ConductorAPI的object_action方法,定义在nova/conductor/rpcapi.py中:defobject_action(self,context,objinst,objmethod,args,kwargs):cctxt=self.client.prepare()returncctxt.call(context,'object_action',objinst=objinst,objmethod=objmethod,args=args,kwargs=kwargs)rpcapi.py封装了客户端所有的RPC调用方法。从代码来看,它发起RPC服务器端的object_action是同步调用的。此时nova-compute工作成功转移到nova-conductor,阻塞等待nova-conductor返回。3.3nova-conductor源码分析nova-conductorRPCserver在收到RPC请求后调用manager.py(nova/conductor/manager.py)的object_action方法:defobject_action(self,context,objinst,objmethod,args,kwargs):“”“对对象执行操作。”“”oldobj=objinst.obj_clone()result=self._object_dispatch(objinst,objmethod,args,kwargs)updates=dict()#NOTE(danms):Difftheobjectwiththeonepassedtousand#generatealistofchangestoforwardbackforname,fieldsinobjinst():ifnotobjinst。obj_attr_is_set(name):#Avoiddemand-loadinganythingcontinueif(notoldobj.obj_attr_is_set(name)orgetattr(oldobj,name)!=getattr(objinst,name)):updates[name]=field.to_primitive(objinst,name,getattr(objinst,name)))#Thisissafesinceafieldnamedthiswouldconflictwiththe#methodanywayupdates['obj_what_changed']=objinst.obj_what_changed()returnupdates,result该方法首先调用obj_clone()方法备份原对象,主要是后续统计哪些字段更新了。然后调用_object_dispatch方法:def_object_dispatch(self,target,method,args,kwargs):try:returngetattr(target,method)(*args,**kwargs)exceptException:raisemessaging.ExpectedException()该方法使用了反射通过方法Name调用的机制,这里我们的方法调用的是save方法,所以明明是调用了target.save()方法,也就是最终调用了instance.save()方法,但是此时已经调用了在导体侧。回到nova/objects/instance.py的save方法,有人会说这不会是无限递归的RPC调用吧?显然不是,这是因为nova-conductor的indirection_api为None,@remotable中必须走else分支。4、想一个问题,你还记得_delete_instance方法中的数据库调用代码吗?在此处粘贴代码:instance.vm_state=vm_states.DELETEDin??stance.task_state=Noneinstance.power_state=power_state.NOSTATEinstance.terminated_at=timeutils.utcnow()实例。save()system_meta=instance.system_metadatainstance.destroy()有人会说实例记录会被删除,直接调用destroy方法很可怕。前面的一堆字段更新后save方法干什么。这是因为Nova在处理删除记录时采用了软删除策略,即实际上并没有完全删除记录,而是在记录中有一个deleted字段来标记是否被删除。这样做的好处是方便以后审计甚至数据恢复。5.小结本文首先介绍了OpenstackNova组件数据库访问的发展历史,然后根据源码分析了目前Nova访问数据库的流程,最后解释了为什么Nova使用软删除。