360 OpenStack支持IP SAN存储实现

背景

为了更多的满足TOB场景下的需求,360虚拟化团队一直不断丰富完善openstack侧的功能,近期对接过信创存储腾凌存储等商业存储,所以梳理一下整个流程,下面进入正文从架构以及源码了解openstack如何支持SAN存储。

一、相关概念

提到 IP SAN 必然会想到磁盘阵列,磁盘阵列有三种架构分别为:DAS,NAS,SAN。而SAN里面主要又分为IP SAN和FC SAN。

FC-SAN(Fibre Channel Storage Area Network)是一种基于光纤通道技术的存储网络,它将存储设备和服务器连接在一起,形成一个高速、高性能的存储区域网络。FC-SAN的核心是光纤通道交换机,它实现了光纤通道协议,使得存储设备和服务器之间的数据传输更加可靠和高效。

IP-SAN(Internet Protocol Storage Area Network)是一种基于IP协议的存储网络,它将存储设备、连接设备和接口集成在高速网络中。IP-SAN使用IP网络将存储设备连接在一起,实现数据的可靠传输和共享。由于IP网络具有广泛的普及性和互操作性,IP-SAN具有较好的扩展性和灵活性。

DAS(Direct Attached Storage)是一种直接附加存储技术,它将存储设备通过电缆直接连接到服务器上。DAS的优点是简单、成本低,适用于小型网络和单机环境。但是,DAS的缺点也很明显,如存储容量受限、扩展性差、数据共享困难等。

NAS(Network Attached Storage)是一种网络附加存储技术,它将存储设备连接到现有的网络上,提供数据和文件服务。NAS实际上是一个专门优化了的文件服务器,具有独立的操作系统和文件系统。NAS的优点是易于部署和管理,可以实现数据的集中存储和共享。但是,NAS的缺点是性能受限于网络带宽和稳定性,不适合大规模数据存储和高性能计算。

二、OpenStack实现

2.1 cinder侧实现

我们知道在openstack中cinder负责存储volume 的生命周期管理,而cinder中cinder-volume负责转发控制面请求从而对存储执行 action,对于每一种存储介质,cinder-volume需要调用对应的driver才可以,这里以最近对接过的信创腾凌ip san 存储为例讲解实现。

我们首先需要配置一个cinder 的volume backend tldriver

ini 复制代码
[tldriver]volume_backend_name = tldrivervolume_driver = cinder.volume.drivers.tengling.tengling_driver.TenglingISCSIDrivertengling_sanip = {{ tengling_sanip }}tengling_username = {{ tengling_username }}tengling_password = {{ tengling_password }}tengling_storagepool = {{ tengling_storagepool }}tengling_max_clone_depth = {{ tengling_max_clone_depth }}tengling_flatten_volume_from_snapshot=True

我们以创建一个volume从源码刨析整个的流程,前面将新建的volume type指定capability 为backend tldriver,这样scheduler就能调度到我们的新存储上了。cinder scheduler 通过rpc 请求volume 调用 volumeManager 的create_volume函数

python 复制代码
@objects.Volume.set_workers    def create_volume(self, context, volume, request_spec=None,                      filter_properties=None, allow_reschedule=True):        ..........try:            # NOTE(flaper87): Driver initialization is            # verified by the task itself.            flow_engine = create_volume.get_flow(                context_elevated,                self,                self.db,                self.driver,                self.scheduler_rpcapi,                self.host,                volume,                allow_reschedule,                context,                request_spec,                filter_properties,                image_volume_cache=self.image_volume_cache,            )        except Exception:            msg = _("Create manager volume flow failed.")            LOG.exception(msg, resource={'type': 'volume', 'id': volume.id})            raise exception.CinderException(msg)

cinder volume创建volume时通过task flow执行了核心任务 CreateVolumeFromSpecTask,这里用户创建了一个系统盘,指定了image,所以执行了_create_from_image ,最终调用了_create_from_image_cache_or_download 方法

python 复制代码
class CreateVolumeFromSpecTask(flow_utils.CinderTask):    ..........    def execute(self, context, volume, volume_spec):        ..........         elif create_type == 'image':            model_update = self._create_from_image(context,                                                   volume,                                                   **volume_spec)    ..........    def _create_from_image(self, context, volume,                           image_location, image_id, image_meta,                           image_service, **kwargs):        ..........        if not cloned:            model_update = self._create_from_image_cache_or_download(                context,                volume,                image_location,                image_id,                image_meta,                image_service)    def _create_from_image_cache_or_download(self, context, volume,                                             image_location, image_id,                                             image_meta, image_service,                                             update_cache=False):        ..........        try:            if not cloned:                try:                    with image_utils.TemporaryImages.fetch(                            image_service, context, image_id,                            backend_name) as tmp_image:                        if CONF.verify_glance_signatures != 'disabled':                            # Verify image signature via reading content from                            # temp image, and store the verification flag if                            # required.                            verified = \                                image_utils.verify_glance_image_signature(                                    context, image_service,                                    image_id, tmp_image)                            self.db.volume_glance_metadata_bulk_create(                                context, volume.id,                                {'signature_verified': verified})                        # Try to create the volume as the minimal size,                        # then we can extend once the image has been                        # downloaded.                        data = image_utils.qemu_img_info(tmp_image)
                        virtual_size = image_utils.check_virtual_size(                            data.virtual_size, volume.size, image_id)
                        if should_create_cache_entry:                            if virtual_size and virtual_size != original_size:                                    volume.size = virtual_size                                    volume.save()                        model_update = self._create_from_image_download(                            context,                            volume,                            image_location,                            image_meta,                            image_service                        )          finally:            # If we created the volume as the minimal size, extend it back to            # what was originally requested. If an exception has occurred or            # extending it back failed, we still need to put this back before            # letting it be raised further up the stack.            if volume.size != original_size:                try:                    self.driver.extend_volume(volume, original_size)                finally:                    volume.size = original_size                    volume.save()         ..........

在 _create_from_image_cache_or_download 中会将镜像下载到本地临时文件,再通过 qemu 获取info信息,最终调用了 _create_from_image_download。在 _create_from_image_download中调用的本backend的driver执行create_volume 操作,并调用 copy_image_to_volume将镜像数据写入到volume

python 复制代码
def _create_from_image_download(self, context, volume, image_location,                                    image_meta, image_service):        ..........        model_update = self.driver.create_volume(volume) or {}        self._cleanup_cg_in_volume(volume)        model_update['status'] = 'downloading'        try:            volume.update(model_update)            volume.save()        except exception.CinderException:            LOG.exception("Failed updating volume %(volume_id)s with "                          "%(updates)s",                          {'volume_id': volume.id,                           'updates': model_update})        try:            volume_utils.copy_image_to_volume(self.driver, context, volume,                                              image_meta, image_location,                                              image_service)        except exception.ImageTooBig:            with excutils.save_and_reraise_exception():                LOG.exception("Failed to copy image to volume "                              "%(volume_id)s due to insufficient space",                              {'volume_id': volume.id})        return model_update def copy_image_to_volume(driver, context, volume, image_meta, image_location,                         image_service):   ..........   try:        image_encryption_key = image_meta.get('cinder_encryption_key_id')
        if volume.encryption_key_id and image_encryption_key:            # If the image provided an encryption key, we have            # already cloned it to the volume's key in            # _get_encryption_key_id, so we can do a direct copy.            driver.copy_image_to_volume(                context, volume, image_service, image_id)        elif volume.encryption_key_id:            # Creating an encrypted volume from a normal, unencrypted,            # image.            driver.copy_image_to_encrypted_volume(                context, volume, image_service, image_id)        else:            driver.copy_image_to_volume(                context, volume, image_service, image_id)

这里调用了腾凌存储的 cinder.volume.drivers.tengling.tengling_driver.TenglingISCSIDriver.create_volume 创建云盘,并调用腾凌driver执行了copy_image_to_volume。这里TenglingISCSIDriver继承自了driver.ISCSIDriver ,所以调用了原生ISCSIDriver的

python 复制代码
def create_volume(self, volume):        """Create a volume."""        volume_type = self._get_volume_type(volume)        opts = self._get_volume_params(volume_type)        if (opts.get('hypermetro') == 'true'                and opts.get('replication_enabled') == 'true'):            err_msg = _("Hypermetro and Replication can not be "                        "used in the same volume_type.")            LOG.error(err_msg)            raise exception.VolumeBackendAPIException(data=err_msg)
        lun_params, lun_info, model_update = (            self._create_base_type_volume(opts, volume, volume_type))
        model_update = self._add_extend_type_to_volume(opts, lun_params,                                                       lun_info, model_update)        return model_update

原生ISCSIDriver 会通过os_brick模块远程iscsi挂载磁盘到本地

python 复制代码
class ISCSIDriver(VolumeDriver):    .........    def copy_image_to_volume(self, context, volume, image_service, image_id):        """Fetch image from image_service and write to unencrypted volume.
        This does not attach an encryptor layer when connecting to the volume.        """        self._copy_image_data_to_volume(            context, volume, image_service, image_id, encrypted=False)        .........    def _copy_image_data_to_volume(self, context, volume, image_service,                                   image_id, encrypted=False):        """Fetch the image from image_service and write it to the volume."""        LOG.debug('copy_image_to_volume %s.', volume['name'])
        use_multipath = self.configuration.use_multipath_for_image_xfer        enforce_multipath = self.configuration.enforce_multipath_for_image_xfer        properties = utils.brick_get_connector_properties(use_multipath,                                                          enforce_multipath)        attach_info, volume = self._attach_volume(context, volume, properties) # 这里会挂载远程disk        try:            if encrypted:                encryption = self.db.volume_encryption_metadata_get(context,                                                                    volume.id)                utils.brick_attach_volume_encryptor(context,                                                    attach_info,                                                    encryption)            try:                image_utils.fetch_to_raw(                    context,                    image_service,                    image_id,                    attach_info['device']['path'],                    self.configuration.volume_dd_blocksize,                    size=volume['size']) #这里写入镜像数据             except exception.ImageTooBig:                with excutils.save_and_reraise_exception():                    LOG.exception("Copying image %(image_id)s "                                  "to volume failed due to "                                  "insufficient available space.",                                  {'image_id': image_id})
            finally:                if encrypted:                    utils.brick_detach_volume_encryptor(attach_info,                                                        encryption)        finally:            self._detach_volume(context, attach_info, volume, properties,                                force=True)

fetch_to_volume_format 中会fetch镜像文件到本地临时目录,最后通过执行qemu-img convert 命令将镜像临时文件数据写入到本地iscsi远程磁盘中,至此虚机的系统盘数据写入完成。

python 复制代码
def fetch_to_volume_format(context, image_service,                           image_id, dest, volume_format, blocksize,                           volume_subformat=None, user_id=None,                           project_id=None, size=None, run_as_root=True):    .........    convert_image(tmp, dest, volume_format,                      out_subformat=volume_subformat,                      src_format=disk_format,                      run_as_root=run_as_root)
def _convert_image(prefix, source, dest, out_format,                   out_subformat=None, src_format=None,                   run_as_root=True, cipher_spec=None, passphrase_file=None):    cmd = _get_qemu_convert_cmd(source, dest,                                 out_format=out_format,                                src_format=src_format,                                out_subformat=out_subformat,                                cache_mode=cache_mode,                                prefix=prefix,                                cipher_spec=cipher_spec,                                passphrase_file=passphrase_file)  #拼接 qemu-img convert 命令

最后cinder侧更新数据库,至此cinder侧工作完成了,概括性的流程如下:

2.2 nova侧实现

nova侧在给虚机挂载磁盘时,nova-compute收到请求后attach volume的请求后调用nova.compute.manager.ComputeManager.attach_volume

python 复制代码
def _attach_volume(self, context, instance, bdm):        context = context.elevated()        LOG.info('Attaching volume %(volume_id)s to %(mountpoint)s',                 {'volume_id': bdm.volume_id,                  'mountpoint': bdm['mount_device']},                 instance=instance)        compute_utils.notify_about_volume_attach_detach(            context, instance, self.host,            action=fields.NotificationAction.VOLUME_ATTACH,            phase=fields.NotificationPhase.START,            volume_id=bdm.volume_id)        try:            bdm.attach(context, instance, self.volume_api, self.driver,                       do_driver_attach=True)            ................

nova.virt.block_device.DriverVolumeBlockDevice.attach中会 调用 _legacy_volume_attach 进行挂载磁盘

python 复制代码
@update_db    def attach(self, context, instance, volume_api, virt_driver,               do_driver_attach=False, **kwargs):        ..................         # Check to see if we need to lock based on the shared_targets value.        # Default to False if the volume does not expose that value to maintain        # legacy behavior.        if volume.get('shared_targets', False):            # Lock the attach call using the provided service_uuid.            @utils.synchronized(volume['service_uuid'])            def _do_locked_attach(*args, **_kwargs):                self._do_attach(*args, **_kwargs)
            _do_locked_attach(context, instance, volume, volume_api,                              virt_driver, do_driver_attach)        else:            # We don't need to (or don't know if we need to) lock.            self._do_attach(context, instance, volume, volume_api,                            virt_driver, do_driver_attach)
    def _do_attach(self, context, instance, volume, volume_api, virt_driver,                   do_driver_attach):        """Private method that actually does the attach.
        This is separate from the attach() method so the caller can optionally        lock this call.        """        context = context.elevated()        connector = virt_driver.get_volume_connector(instance)        if not self['attachment_id']:            self._legacy_volume_attach(context, volume, connector, instance,                                       volume_api, virt_driver,                                       do_driver_attach)        else:            self._volume_attach(context, volume, connector, instance,                                volume_api, virt_driver,                                self['attachment_id'],                                do_driver_attach)

legacyvolume_attach 会 调用cinder 的 initialize_connection 获取volume挂载的connection info,这里因为是iscsi 云盘,cinder会返回iSCSI的 connection info。

python 复制代码
def _legacy_volume_attach(self, context, volume, connector, instance,                              volume_api, virt_driver,                              do_driver_attach=False):        volume_id = volume['id']
        connection_info = volume_api.initialize_connection(context,                                                           volume_id,                                                           connector)       .................. 
        # If do_driver_attach is False, we will attach a volume to an instance        # at boot time. So actual attach is done by instance creation code.        if do_driver_attach:            encryption = encryptors.get_encryption_metadata(                context, volume_api, volume_id, connection_info)
            try:                virt_driver.attach_volume(                        context, connection_info, instance,                        self['mount_device'], disk_bus=self['disk_bus'],                        device_type=self['device_type'], encryption=encryption)            except Exception:                with excutils.save_and_reraise_exception():                    LOG.exception("Driver failed to attach volume "                                  "%(volume_id)s at %(mountpoint)s",                                  {'volume_id': volume_id,                                   'mountpoint': self['mount_device']},                                  instance=instance)                    volume_api.terminate_connection(context, volume_id,                                                    connector)        ..................         if volume['attach_status'] == "detached":            # NOTE(mriedem): save our current state so connection_info is in            # the database before the volume status goes to 'in-use' because            # after that we can detach and connection_info is required for            # detach.            self.save()            try:                volume_api.attach(context, volume_id, instance.uuid,                                  self['mount_device'], mode=mode)

virt_driver.attach_volume 会调用libvirt 挂载磁盘 ,self._connect_volume 会依据connection info判断为iSCSI driver 会Calling os-brick to attach iSCSI Volume ,最终libvirt 在线添加了disk到虚机里面,至此虚机侧挂载完成!

python 复制代码
def attach_volume(self, context, connection_info, instance, mountpoint,                      disk_bus=None, device_type=None, encryption=None):        guest = self._host.get_guest(instance)
        disk_dev = mountpoint.rpartition("/")[2]        bdm = {            'device_name': disk_dev,            'disk_bus': disk_bus,            'device_type': device_type}
        .................. 
        self._connect_volume(context, connection_info, instance,                             encryption=encryption)  
        ..................  	try:            state = guest.get_power_state(self._host)            live = state in (power_state.RUNNING, power_state.PAUSED)
            guest.attach_device(conf, persistent=True, live=live)

三、总结

IP SAN 由于基于传统IP网络,所以优势是成本低、易维护且比较灵活,但同时也限制了他只适合应用于对于性能要求并不是太高的场景,性能上比较不如FS SAN以及nvmf等,因此IP SAN 适合于中小型应用场景,所以需要针对不同的使用场景下进行选择不同的存储协议。

本文借以兼容腾凌存储梳理了openstack侧虚机挂载使用商业 IP SAN存储的整个流程,对于理解包括支持其他类型的SAN/NVMF等存储有一定的参考意义。


更多技术干货,

请关注"360智汇云开发者"👇

360智汇云官网:https://zyun.360.cn(复制在浏览器中打开)

更多云产品欢迎试用体验~

相关推荐
老六ip加速器15 分钟前
国内ip地址怎么改?详细教程
网络·tcp/ip·智能路由器
欧先生^_^1 小时前
OSPF网络协议
网络·网络协议·智能路由器
光而不耀@lgy1 小时前
C++初登门槛
linux·开发语言·网络·c++·后端
合新通信 | 让光不负所托2 小时前
【合新通信】浸没式液冷光模块与冷媒兼容性测试技术报告
大数据·网络·光纤通信
Yeats_Liao2 小时前
Go 语言 TCP 端口扫描器实现与 Goroutine 池原理
开发语言·tcp/ip·golang
浩浩测试一下3 小时前
计算机网络中的DHCP是什么呀? 详情解答
android·网络·计算机网络·安全·web安全·网络安全·安全架构
Luck小吕5 小时前
两天两夜!这个 GB28181 的坑让我差点卸载 VSCode
后端·网络协议
三思而后行,慎承诺6 小时前
tcp 和http 网络知识
网络·tcp/ip·http
JavaEdge.6 小时前
LangChain4j HTTP 客户端定制:解锁 LLM API 交互的更多可能性
网络·网络协议·http