深入研究Openstack Nova組件對象模型以及數(shù)據(jù)庫訪問機(jī)制
1. 背景
介紹在Openstack G版以前,Nova的所有服務(wù)(包括nova-compute服務(wù))都是直接訪問數(shù)據(jù)庫的,數(shù)據(jù)庫訪問接口在nova/db/api.py模塊中實(shí)現(xiàn),而該模塊只是調(diào)用了IMPL的方法,即該模塊只是一個(gè)代理,真正實(shí)現(xiàn)由IMPL實(shí)現(xiàn),IMPL是一個(gè)可配置的動(dòng)態(tài)加載驅(qū)動(dòng)模塊,通常使用Python sqlalchemy庫實(shí)現(xiàn),對應(yīng)的代碼為nova.db.sqlalchemy.api:
- _BACKEND_MAPPING = {'sqlalchemy': 'nova.db.sqlalchemy.api'}
該模塊不僅實(shí)現(xiàn)了model的CRUD操作,還封裝了一些高級API,比如:
- instance_get_all: 獲取所有虛擬機(jī)實(shí)例。
- instance_update: 更新虛擬機(jī)熟悉。
- …
這種直接訪問數(shù)據(jù)庫的設(shè)計(jì)至少存在以下兩個(gè)問題:
- 所有服務(wù)與數(shù)據(jù)模型耦合,當(dāng)數(shù)據(jù)模型變更時(shí),可能需要涉及所有代碼的調(diào)整,并難以支持版本控制。
- 所有的主機(jī)都能訪問數(shù)據(jù)庫,大大增加了數(shù)據(jù)庫的暴露風(fēng)險(xiǎn)。
為了實(shí)現(xiàn)Nova服務(wù)與數(shù)據(jù)庫訪問解耦,從G版本開始引入了nova-conductor服務(wù),該服務(wù)的一個(gè)重要作用就是訪問數(shù)據(jù)庫,其它服務(wù)訪問數(shù)據(jù)庫時(shí)需要向nova-conductor發(fā)起RPC請求,由nova-conductor代理請求數(shù)據(jù)庫。
以上方式基本解決了服務(wù)與數(shù)據(jù)庫訪問解耦,并且防止其它服務(wù)直接訪問數(shù)據(jù)庫,但仍然沒有解決對象模型的版本控制。從I版本開始引入了對象模型的概念,所有的對象模型定義在nova/objects。在此之前訪問數(shù)據(jù)庫是直接調(diào)用數(shù)據(jù)庫的model的,比如更新一個(gè)flavor一個(gè)字段,調(diào)用Flavor的update方法(由sqlalchemy)實(shí)現(xiàn)。引入對象模型后,相當(dāng)于在服務(wù)與數(shù)據(jù)庫之間又添加了一級對象層,各個(gè)服務(wù)直接和資源對象交互,資源對象再和數(shù)據(jù)庫接口交互,數(shù)據(jù)庫返回時(shí)也會相應(yīng)的轉(zhuǎn)化為對象模型中的對象。
對象模型的對象不僅封裝了數(shù)據(jù)庫訪問,還支持了版本控制。每個(gè)對象都會維護(hù)一個(gè)版本號,發(fā)起RPC請求時(shí)必須指定對象的版本號。新版本的對象通常能夠兼容舊版本對象,比如nova-conductor升級了使用對象模型版本為1.2,但nova-compute服務(wù)可能還沒有升級完成,仍然使用的是1.1版本,此時(shí)請求返回時(shí)會把conductor的返回的對象轉(zhuǎn)化為1.1版本兼容的對象。
目前Cinder服務(wù)還是直接訪問數(shù)據(jù)庫,目前已經(jīng)在社區(qū)有對應(yīng)的BP關(guān)于增加cinder-conductor服務(wù)Create conductor service for cinder like nova-conductor, 該BP于2013年6月提出,到當(dāng)前最新版本N還尚未實(shí)現(xiàn)。
2. Nova配置
以上我們介紹了nova-conductor以及對象模型的背景,我們了解到所有服務(wù)訪問數(shù)據(jù)庫都必須通過RPC調(diào)用nova-conductor服務(wù)請求,但這并不是強(qiáng)制的,如果不考慮數(shù)據(jù)庫訪問安全,你仍然可以使用本地訪問方式,nova-compute服務(wù)可以直接訪問數(shù)據(jù)庫而不發(fā)起nova-conductor RPC調(diào)用。我們看nova-compute服務(wù)的初始化,它位于nova/cmd/compute.y:
- def main():
- # ...
- if not CONF.conductor.use_local:
- cmd_common.block_db_access('nova-compute')
- objects_base.NovaObject.indirection_api = \
- conductor_rpcapi.ConductorAPI()
- else:
- LOG.warning(_LW('Conductor local mode is deprecated and will '
- 'be removed in a subsequent release'))
- # ...
因此在/etc/nova.conf配置文件中可以配置是否直接訪問數(shù)據(jù)庫。以上indirection_api是Nova對象模型的一個(gè)字段,初始化為None。
如果設(shè)置use_local為true,則indirection_api為None,否則將初始化為conductor_rpcapi.ConductorAPI,從這里我們也可以看出調(diào)用conductor的入口。
我們可能會想到說在對象模型訪問數(shù)據(jù)庫時(shí)會有一堆if-else來判斷是否使用use_local,事實(shí)上是否這樣呢,我們接下來將分析源碼,從而理解Openstack的設(shè)計(jì)理念。
3. 源碼分析
3.1 nova-compute源碼分析
本小節(jié)主要以刪除虛擬機(jī)為例,分析nova-compute在刪除虛擬機(jī)時(shí)如何操作數(shù)據(jù)庫的。刪除虛擬機(jī)的API入口為nova/compute/manager.py的_delete_instance方法,方法原型為:
- _delete_instance(self, context, instance, bdms, quotas)
該方法有4個(gè)參數(shù),context是上下文信息,包含用戶、租戶等信息,instance就是我們上面提到的對象模型中Instance對象實(shí)例,bdms是blockDeviceMappingList對象實(shí)例,保存著block設(shè)備映射列表,quotas是nova.objects.quotas.Quotas對象實(shí)例,保存該租戶的quota信息。
該方法涉及的數(shù)據(jù)庫操作代碼為:
- instance.vm_state = vm_states.DELETED
- instance.task_state = None
- instance.power_state = power_state.NOSTATE
- instance.terminated_at = timeutils.utcnow()
- instance.save()
- system_meta = instance.system_metadata
- instance.destroy()
從代碼中可以看到,首先更新instance的幾個(gè)字段,然后調(diào)用save()方法保存到數(shù)據(jù)庫中,最后調(diào)用destroy方法刪除該實(shí)例(注意,這里的刪除并不一定是真的從數(shù)據(jù)庫中刪除記錄,也有可能僅僅做個(gè)刪除的標(biāo)識)。
我們先找到以上的save()方法,它位于nova/object/instance.py模塊中,方法原型為:
- @base.remotable
- save(self, expected_vm_state=None,
- expected_task_state=None, admin_state_reset=False)
save方法會記錄需要更新的字段,并調(diào)用db接口保存到數(shù)據(jù)庫中。關(guān)鍵是該方法的wrapper remotable,這個(gè)注解(python不叫注解,不過為了習(xí)慣這里就叫注解吧)非常重要,該方法在oslo中定義:
- def remotable(fn):
- """Decorator for remotable object methods."""
- @six.wraps(fn)
- def wrapper(self, *args, **kwargs):
- ctxt = self._context
- if ctxt is None:
- raise exception.OrphanedObjectError(method=fn.__name__,
- objtype=self.obj_name())
- if self.indirection_api:
- updates, result = self.indirection_api.object_action(
- ctxt, self, fn.__name__, args, kwargs)
- for key, value in six.iteritems(updates):
- if key in self.fields:
- field = self.fields[key]
- # NOTE(ndipanov): Since VersionedObjectSerializer will have
- # deserialized any object fields into objects already,
- # we do not try to deserialize them again here.
- if isinstance(value, VersionedObject):
- setattr(self, key, value)
- else:
- setattr(self, key,
- field.from_primitive(self, key, value))
- self.obj_reset_changes()
- self._changed_fields = set(updates.get('obj_what_changed', []))
- return result
- else:
- return fn(self, *args, **kwargs)
- wrapper.remotable = True
- wrapper.original_fn = fn
- return wrapper
從代碼看到,當(dāng)indirection_api不為None時(shí)會調(diào)用indirection_api的object_action方法,由前面我們知道這個(gè)值由配置項(xiàng)use_local決定,當(dāng)use_local為False時(shí)indirection_api為conductor_rpcapi.ConductorAPI。從這里了解到對象并不是通過一堆if-else來判斷是否使用use_local的,而是通過@remotable注解實(shí)現(xiàn)的,remotable封裝了if-else,當(dāng)使用local時(shí)直接調(diào)用原來對象實(shí)例的save方法,否則調(diào)用indirection_api的object_action方法。
注意: 除了@remotable注解,還定義了@remotable_classmethod注解,該注解功能和@remotable類似,僅僅相當(dāng)于又封裝了個(gè)@classmethod注解。
3.2 RPC調(diào)用
前面我們分析到調(diào)用conductor_rpcapi.ConductorAPI的object_action方法,該方法在nova/conductor/rpcapi.py中定義:
- def object_action(self, context, objinst, objmethod, args, kwargs):
- cctxt = self.client.prepare()
- return cctxt.call(context, 'object_action', objinst=objinst,
- objmethod=objmethod, args=args, kwargs=kwargs)
rpcapi.py封裝了client端的所有RPC調(diào)用方法,從代碼上看,發(fā)起了RPC server端的object_action同步調(diào)用。此時(shí)nova-compute工作順利轉(zhuǎn)接到nova-conductor,并堵塞等待nova-conducor返回。
3.3 nova-conductor源碼分析
nova-conductor RPC server端接收到RPC請求后調(diào)用manager.py的object_action方法(nova/conductor/manager.py):
- def object_action(self, context, objinst, objmethod, args, kwargs):
- """Perform an action on an object."""
- oldobj = objinst.obj_clone()
- result = self._object_dispatch(objinst, objmethod, args, kwargs)
- updates = dict()
- # NOTE(danms): Diff the object with the one passed to us and
- # generate a list of changes to forward back
- for name, field in objinst.fields.items():
- if not objinst.obj_attr_is_set(name):
- # Avoid demand-loading anything
- continue
- if (not oldobj.obj_attr_is_set(name) or
- getattr(oldobj, name) != getattr(objinst, name)):
- updates[name] = field.to_primitive(objinst, name,
- getattr(objinst, name))
- # This is safe since a field named this would conflict with the
- # method anyway
- updates['obj_what_changed'] = objinst.obj_what_changed()
- return updates, result
該方法首先調(diào)用obj_clone()方法備份原來的對象,主要為了后續(xù)統(tǒng)計(jì)哪些字段更新了。然后調(diào)用了_object_dispatch方法:
- def _object_dispatch(self, target, method, args, kwargs):
- try:
- return getattr(target, method)(*args, **kwargs)
- except Exception:
- raise messaging.ExpectedException()
該方法利用反射機(jī)制通過方法名調(diào)用,這里我們的方法名為save方法,因此顯然調(diào)用了target.save()方法,即最終還是調(diào)用的instance.save()方法,不過此時(shí)已經(jīng)是在conductor端調(diào)用了.
又回到了nova/objects/instance.py的save方法,有人會說難道這不會無限遞歸RPC調(diào)用嗎?顯然不會,這是因?yàn)閚ova-conductor的indirection_api為None,在@remotable中肯定走else分支。
4. 思考一個(gè)問題
還記得在_delete_instance方法的數(shù)據(jù)庫調(diào)用代碼嗎?這里再貼下代碼:
- instance.vm_state = vm_states.DELETED
- instance.task_state = None
- instance.power_state = power_state.NOSTATE
- instance.terminated_at = timeutils.utcnow()
- instance.save()
- system_meta = instance.system_metadata
- instance.destroy()
有人會說instance記錄都要?jiǎng)h了,直接調(diào)用destory方法不得了,前面一堆更新字段然后save方法是干什么的。這是因?yàn)镹ova在處理刪除記錄時(shí)使用的是軟刪除策略,即不會真正得把記錄徹底刪除,而是在記錄中有個(gè)deleted字段標(biāo)記是否已經(jīng)被刪除。這樣的好處是方便以后審計(jì)甚至數(shù)據(jù)恢復(fù)。
5. 總結(jié)
本文首先介紹了Openstack Nova組件數(shù)據(jù)庫訪問的發(fā)展歷程,然后基于源碼分析了當(dāng)前Nova訪問數(shù)據(jù)庫的過程,最后解釋了Nova使用軟刪除的原因。