1. 背景
介绍在Openstack G版以前,Nova的所有服务(包括nova-compute服务)都是直接访问数据库的,数据库访问接口在nova/db/api.py模块中实现,而该模块只是调用了IMPL的方法,即该模块只是一个代理,真正实现由IMPL实现,IMPL是一个可配置的动态加载驱动模块,通常使用Python sqlalchemy库实现,对应的代码为nova.db.sqlalchemy.api:
- _BACKEND_MAPPING = {'sqlalchemy': 'nova.db.sqlalchemy.api'}
该模块不仅实现了model的CRUD操作,还封装了一些高级API,比如:
- instance_get_all: 获取所有虚拟机实例。
- instance_update: 更新虚拟机熟悉。
- …
这种直接访问数据库的设计至少存在以下两个问题:
- 所有服务与数据模型耦合,当数据模型变更时,可能需要涉及所有代码的调整,并难以支持版本控制。
- 所有的主机都能访问数据库,大大增加了数据库的暴露风险。
为了实现Nova服务与数据库访问解耦,从G版本开始引入了nova-conductor服务,该服务的一个重要作用就是访问数据库,其它服务访问数据库时需要向nova-conductor发起RPC请求,由nova-conductor代理请求数据库。
以上方式基本解决了服务与数据库访问解耦,并且防止其它服务直接访问数据库,但仍然没有解决对象模型的版本控制。从I版本开始引入了对象模型的概念,所有的对象模型定义在nova/objects。在此之前访问数据库是直接调用数据库的model的,比如更新一个flavor一个字段,调用Flavor的update方法(由sqlalchemy)实现。引入对象模型后,相当于在服务与数据库之间又添加了一级对象层,各个服务直接和资源对象交互,资源对象再和数据库接口交互,数据库返回时也会相应的转化为对象模型中的对象。
对象模型的对象不仅封装了数据库访问,还支持了版本控制。每个对象都会维护一个版本号,发起RPC请求时必须指定对象的版本号。新版本的对象通常能够兼容旧版本对象,比如nova-conductor升级了使用对象模型版本为1.2,但nova-compute服务可能还没有升级完成,仍然使用的是1.1版本,此时请求返回时会把conductor的返回的对象转化为1.1版本兼容的对象。
目前Cinder服务还是直接访问数据库,目前已经在社区有对应的BP关于增加cinder-conductor服务Create conductor service for cinder like nova-conductor, 该BP于2013年6月提出,到当前最新版本N还尚未实现。
2. Nova配置
以上我们介绍了nova-conductor以及对象模型的背景,我们了解到所有服务访问数据库都必须通过RPC调用nova-conductor服务请求,但这并不是强制的,如果不考虑数据库访问安全,你仍然可以使用本地访问方式,nova-compute服务可以直接访问数据库而不发起nova-conductor RPC调用。我们看nova-compute服务的初始化,它位于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配置文件中可以配置是否直接访问数据库。以上indirection_api是Nova对象模型的一个字段,初始化为None。
如果设置use_local为true,则indirection_api为None,否则将初始化为conductor_rpcapi.ConductorAPI,从这里我们也可以看出调用conductor的入口。
我们可能会想到说在对象模型访问数据库时会有一堆if-else来判断是否使用use_local,事实上是否这样呢,我们接下来将分析源码,从而理解Openstack的设计理念。
3. 源码分析
3.1 nova-compute源码分析
本小节主要以删除虚拟机为例,分析nova-compute在删除虚拟机时如何操作数据库的。删除虚拟机的API入口为nova/compute/manager.py的_delete_instance方法,方法原型为:
- _delete_instance(self, context, instance, bdms, quotas)
该方法有4个参数,context是上下文信息,包含用户、租户等信息,instance就是我们上面提到的对象模型中Instance对象实例,bdms是blockDeviceMappingList对象实例,保存着block设备映射列表,quotas是nova.objects.quotas.Quotas对象实例,保存该租户的quota信息。
该方法涉及的数据库操作代码为:
- 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的几个字段,然后调用save()方法保存到数据库中,最后调用destroy方法删除该实例(注意,这里的删除并不一定是真的从数据库中删除记录,也有可能仅仅做个删除的标识)。
我们先找到以上的save()方法,它位于nova/object/instance.py模块中,方法原型为:
- @base.remotable
- save(self, expected_vm_state=None,
- expected_task_state=None, admin_state_reset=False)
save方法会记录需要更新的字段,并调用db接口保存到数据库中。关键是该方法的wrapper remotable,这个注解(python不叫注解,不过为了习惯这里就叫注解吧)非常重要,该方法在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
从代码看到,当indirection_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.2 RPC调用
前面我们分析到调用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调用方法,从代码上看,发起了RPC server端的object_action同步调用。此时nova-compute工作顺利转接到nova-conductor,并堵塞等待nova-conducor返回。
3.3 nova-conductor源码分析
nova-conductor RPC server端接收到RPC请求后调用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
该方法首先调用obj_clone()方法备份原来的对象,主要为了后续统计哪些字段更新了。然后调用了_object_dispatch方法:
- def _object_dispatch(self, target, method, args, kwargs):
- try:
- return getattr(target, method)(*args, **kwargs)
- except Exception:
- raise messaging.ExpectedException()
该方法利用反射机制通过方法名调用,这里我们的方法名为save方法,因此显然调用了target.save()方法,即最终还是调用的instance.save()方法,不过此时已经是在conductor端调用了.
又回到了nova/objects/instance.py的save方法,有人会说难道这不会无限递归RPC调用吗?显然不会,这是因为nova-conductor的indirection_api为None,在@remotable中肯定走else分支。
4. 思考一个问题
还记得在_delete_instance方法的数据库调用代码吗?这里再贴下代码:
- 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记录都要删了,直接调用destory方法不得了,前面一堆更新字段然后save方法是干什么的。这是因为Nova在处理删除记录时使用的是软删除策略,即不会真正得把记录彻底删除,而是在记录中有个deleted字段标记是否已经被删除。这样的好处是方便以后审计甚至数据恢复。
5. 总结
本文首先介绍了Openstack Nova组件数据库访问的发展历程,然后基于源码分析了当前Nova访问数据库的过程,最后解释了Nova使用软删除的原因。