操做系統得到外部存儲空間通常有如下兩種方式:html
Block Storage:經過某種協議(e.g.SAS、SCSI、SAN、iSCSI)從後端存儲 Assigned、Attached 塊設備(Volume),而後分區格式化、建立文件系統並 mount 到操做系統,而後就能夠在此文件系統之上存儲數據,或者也能夠直接使用裸硬盤來存儲數據(e.g. 數據庫系統)node
FileSystem Storage:經過 NFS、CIFS 等協議,mount 遠程文件系統到本地操做系統。NAS、NFS 服務器,以及各類分佈式文件系統提供的都是這種存儲。python
Object Storage:對象存儲是以對象形式存儲數據的存儲系統,最大的優點就是可讓用戶更加靈活的處理海量數據。操做系統客戶端能夠經過對象存儲提供的存儲網關接口(通常是 HTTP/S)來上傳或下載存儲數據。ios
而本篇的主角就是 Cinder,OpenStack Block Storage as a Services(塊存儲即服務)。web
Cinder is the OpenStack Block Storage service for providing volumes to Nova virtual machines, Ironic bare metal hosts, containers and more. Some of the goals of Cinder are to be/have:算法
- Component based architecture: Quickly add new behaviors
- Highly available: Scale to very serious workloads
- Fault-Tolerant: Isolated processes avoid cascading failures
- Recoverable: Failures should be easy to diagnose, debug, and rectify
- Open Standards: Be a reference implementation for a community-driven api
— — 官方文檔:https://docs.openstack.org/cinder/latest/
須要注意的是,正如 Nova 自己不提供 Hypervisor 技術通常,Cinder 自身也不提供存儲技術,而是做爲一個抽象的中間管理層,北向提供穩定而統一的 Block Storage 資源模型、南向經過 Plug-ins&Drivers 模型對接多樣化的後端存儲設備(e.g. LVM、CEPH、NetApp、Datastore etc.)。因此 Cinder 的精華從不在於存儲技術,而是在於對 Block Storage as a Service 需求(建立、刪除、快照、掛載、分離、備份卷)的抽象與理解。sql
a WSGI app that authenticates and routes requests throughout the Block Storage service. It supports the OpenStack APIs only, although there is a translation that can be done through Compute’s EC2 interface, which calls in to the Block Storage client.數據庫
對外提供穩定而統一的北向 RESTful API,cinder-api service 服務進程一般運行在控制節點,支持多 Workers 進程(經過配置項 osapi_volume_workers 設定)。接收到的合法請求會經由 MQ 傳遞到 cinder-volume 執行。Cinder API 現存 v2(DEPRECATED)、v3(CURRENT) 兩個版本,能夠經過配置文件來啓用。swift
Cinder API 官方文檔:https://developer.openstack.org/api-ref/block-storage/後端
NOTE:從 API 的官方文檔,或從 /opt/stack/cinder/cinder/db/sqlalchemy/models.py 均可以從側面對 Cinder 的資源模型定義有所瞭解。
schedules and routes requests to the appropriate volume service. Depending upon your configuration, this may be simple round-robin scheduling to the running volume services, or it can be more sophisticated through the use of the Filter Scheduler. The Filter Scheduler is the default and enables filters on things like Capacity, Availability Zone, Volume Types, and Capabilities as well as custom filters.
若是說 cinder-api 接收的是關於 「建立」 的請求(e.g. Create Volume),那麼該請求就會經過 MQ 轉發到 cinder-scheduler service 服務進程,cinder-scheduler 與 nova-scheduler 通常,顧名思義是調度的層面。經過 Filters 選擇最 「合適」 的 Storage Provider Node 來對請求資源(e.g. Volume)進行建立。不一樣的 Filters 具備不一樣的過濾(調度)算法,所謂的 「合適」 就是達到客戶預期的結果,用戶還能夠自定義 Filter Class 來實現符合自身需求的過濾器,讓調度更加靈活。與 nova-scheduler 通常,cinder-scheduler 一樣須要維護調度對象(存儲節點)「實時」 狀態,cinder-volume service 會按期的向 cinder-scheduler service 上報存儲節點狀態(注:這其實是經過後端存儲設備的驅動程序上報了該設備的狀態)。
現支持的 Filters 清單以下:
storage_availability_zone=az1
來指定該存儲節點的 Zone。配合 AvailabilityZoneFilter,用戶建立 Volume 時選擇指望的 AZ,就能夠實現將 Volume 建立到指定的 AZ 中了。 默認 Zone 爲 nova。經過 nova-scheduler service 的配置文件 cinder.conf 指定你須要使用的 Filters 列表,e.g.
[DEFAULT] ... scheduler_driver =cinder.scheduler.filter_scheduler.FilterScheduler scheduler_default_filters =AvailabilityZoneFilter,CapacityFilter,CapabilitiesFilter
現支持的 Weights 算法以下:
allocated_capacity_weight_multiplier
。capacity_weight_multiplier
。固然了,在實際使用中也存在不須要或者說不但願進行調度的狀況,仍是正如 Nova 通常,建立虛擬機時能夠經過 --availability-zone AZ:Host:Node
來強制指定計算節點,Cinder 也有這般手段。
manages Block Storage devices, specifically the back-end devices themselves.
cinder-volume service 是 Cinder 的核心服務進程,運行該服務進程的節點均可以被稱之爲存儲節點。cinder-volume 經過抽象出統一的 Back-end Storage Driver 層,讓不一樣存儲廠商得以經過提供各自的驅動程序來對接本身的後端存儲設備,實現即插即用(經過配置文件指定),多個這樣的存儲節點共同構成了一個龐大而複雜多樣的存儲資源池系統。
OpenStack 做爲開放的 Infrastracture as a Service 雲操做系統,支持業界各類優秀的技術,這些技術多是開源免費的,也多是商業收費的。 這種開放的架構使得 OpenStack 保持技術上的先進性,具備很強的競爭力,同時又不會形成廠商鎖定(Lock-in)。
以 Cinder 爲例,存儲節點支持多種 Volume Provider,包括 LVM、NFS、Ceph、GlusterFS 以及 EMC、IBM 等商業存儲系統。 cinder-volume 爲這些 Volume Provider 抽象了統一的 Driver 接口,Volume Provider 只須要實現這些接口,就能夠以 Driver 的形式即插即(volume_driver 配置項)用到 OpenStack 中。下面是 Cinder Driver 的架構示意圖:
在 cinder-volume 的配置文件 /etc/cinder/cinder.conf 中設置該存儲節點使用的 Volume Provider Driver 類型:
# 啓用 LVM Provider(默認) volume_driver=cinder.volume.drivers.lvm.LVMVolumeDriver
Driver 和 Plugin 一般不會分家,Driver 是由各存儲廠商提供的,那麼 Plugin(插槽)就應該有 Cinder 的提供。
根據 FileSystem Storage 和 Block Storage 兩個不一樣類型的外部存儲系統,Cinder Plugins 也提供了 FileSystem based 和 Block based 兩種不一樣類型 Plugin。除此以外,Cinder Plugins 還提供了 iSCSC、FC、NFS 等經常使用的數據傳輸協議 Plugin 框架,上傳邏輯得以根據實際狀況來使用(Attached/Dettached)存儲資源。
provides a means to back up a Block Storage volume to OpenStack Object Storage (swift).
提供 Volume 的備份功能,支持將 Volume 備份到對象存儲中(e.g. Swift、Ceph、IBM TSM、NFS),也支持從備份 Restore 成爲 Volume。
Cinder Backup 架構實現:
Volume Provider 不屬於 Cinder 架構的組件,其含義是真實的物理數據存儲設備,爲 Volume 提供物理存儲空間,故又稱 Backend Storage。Cinder 支持多種 Volume Provider,每種 Volume Provider 都經過本身的 Driver 與 cinder-volume 通訊。之因此單獨列出 Volume Provider,是爲了強調 Cinder 自己並不具有存儲技術,真實的存儲資源,由後端 Volume Provider 提供。
Cinder 與 Volume Provider 之間的關係:
建立 Volume 是 Cinder 的頭一號功能,天然花樣也就多了。
# /opt/stack/cinder/cinder/volume/flows/manager/create_volume.py # 根據不一樣的類型有不一樣的建立執行細節 create_type = volume_spec.pop('type', None) LOG.info("Volume %(volume_id)s: being created as %(create_type)s " "with specification: %(volume_spec)s", {'volume_spec': volume_spec, 'volume_id': volume_id, 'create_type': create_type}) if create_type == 'raw': model_update = self._create_raw_volume( context, volume, **volume_spec) elif create_type == 'snap': model_update = self._create_from_snapshot(context, volume, **volume_spec) elif create_type == 'source_vol': model_update = self._create_from_source_volume( context, volume, **volume_spec) elif create_type == 'image': model_update = self._create_from_image(context, volume, **volume_spec) elif create_type == 'backup': model_update, need_update_volume = self._create_from_backup( context, volume, **volume_spec) volume_spec.update({'need_update_volume': need_update_volume}) else: raise exception.VolumeTypeNotFound(volume_type_id=create_type)
這裏咱們先不考慮這些 「花樣」 底層細節的區別,主要關注 create_volume 在 Cinder 全部服務部件之間的流轉過程。
cinder-api service 接收 Post /v3/{project_id}/volumes
請求,通過了一系列的 Body 數據校驗、數據類型轉換(UUID => Object Model)和操做受權校驗(context.authorize)以後,啓動 volume_create_api flow,此 Flow 中包含了下列 Tasks:
自此 volume_create_api flow 完成了 pending -> running -> success 的整個流程,cinder-api 階段結束。
須要注意的是,正如上文提到過的 create volume 也存在不須要進入 cinder-scheduler 的狀況:
# /opt/stack/cinder/cinder/volume/api.py # sched_rpcapi 可能爲 None sched_rpcapi = (self.scheduler_rpcapi if ( not cgsnapshot and not source_cg and not group_snapshot and not source_group) else None) volume_rpcapi = (self.volume_rpcapi if ( not cgsnapshot and not source_cg and not group_snapshot and not source_group) else None) flow_engine = create_volume.get_flow(self.db, self.image_service, availability_zones, create_what, sched_rpcapi, volume_rpcapi)
cinder-scheduler service 接收到了 create volume request 以後也會啓動一個 volume_create_scheduler flow,此 Flow 包含了一下 Tasks:
最終在 cinder-scheduler service 選出一個最佳的存儲節點(cinder-volume),並繼續講 create volume request 經過 RPC 丟到選中的 cinder-volume service 接收。
一樣的,cinder-volume service 仍是啓用了 TaskFlow:volume_create_manager,該 Flow 具備如下 Tasks:
其中最主要的咱們關注 CreateVolumeFromSpecTask,該 Task 調用了後端存儲設備的 Driver 真正執行 Volume 建立的任務。
# /opt/stack/cinder/cinder/volume/flows/manager/create_volume.py def _create_raw_volume(self, context, volume, **kwargs): try: # 後端存儲設備的驅動程序 ret = self.driver.create_volume(volume) except Exception as ex: with excutils.save_and_reraise_exception(): self.message.create( context, message_field.Action.CREATE_VOLUME_FROM_BACKEND, resource_uuid=volume.id, detail=message_field.Detail.DRIVER_FAILED_CREATE, exception=ex) finally: self._cleanup_cg_in_volume(volume) return ret
這裏以 LVM Backend 爲例,該 Task 的本質就是調用操做系統指令 lvcreate
從 Backend 對應的 VG 中劃分出 LV(Volume)。
# /opt/stack/cinder/cinder/brick/local_dev/lvm.py def create_volume(self, name, size_str, lv_type='default', mirror_count=0): """Creates a logical volume on the object's VG. :param name: Name to use when creating Logical Volume :param size_str: Size to use when creating Logical Volume :param lv_type: Type of Volume (default or thin) :param mirror_count: Use LVM mirroring with specified count """ if lv_type == 'thin': pool_path = '%s/%s' % (self.vg_name, self.vg_thin_pool) cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '-T', '-V', size_str, '-n', name, pool_path] else: cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '-n', name, self.vg_name, '-L', size_str] if mirror_count > 0: cmd.extend(['--type=mirror', '-m', mirror_count, '--nosync', '--mirrorlog', 'mirrored']) terras = int(size_str[:-1]) / 1024.0 if terras >= 1.5: rsize = int(2 ** math.ceil(math.log(terras) / math.log(2))) # NOTE(vish): Next power of two for region size. See: # http://red.ht/U2BPOD cmd.extend(['-R', str(rsize)]) try: # 執行組裝好的 CLI self._execute(*cmd, root_helper=self._root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception('Error creating Volume') LOG.error('Cmd :%s', err.cmd) LOG.error('StdOut :%s', err.stdout) LOG.error('StdErr :%s', err.stderr) LOG.error('Current state: %s', self.get_all_volume_groups(self._root_helper)) raise
NOTE: 須要注意的是,雖然 LVM Driver 是經過 CLI 的方式來對 VG 進行操做的,但每種不一樣的後端存儲設備都有本身的實現方式,例如:經過 HTTP/HTTPS、TCP Socket 來進行 Driver 與存儲設備的鏈接。
經過上述流程的分析,或者你會疑惑什麼是 Flow?什麼是 Task?這是由於 Cinder 在 create volume 的流程中採用了 TaskFlow 通用技術庫,其帶來的好處就是可以有效的保證了實際存儲資源與代碼邏輯記錄的一致性,說白了就是避免了程序賬數據的出現。這不能算作一個小問題,由於存儲卷管理的流程每每是長線、長時間的操做,自動化流程脆弱,若是沒有一個有效的 「流程原子性」 機制,將致使程序的穩定性沒法獲得保證。
更多的 TaskFlow 資料請瀏覽:《Openstack 實現技術分解 (4) 通用技術 — TaskFlow》
能夠在 cinder-volume service 的配置文件 cinder.conf 中 使用 scheduler_max_attempts
來配置 Volume 建立失敗重試的次數,默認值爲 3,值爲 1 則表示不啓用失敗重試機制。
# Maximum number of attempts to schedule an volume (integer value) scheduler_max_attempts=3
cinder-sheduler 和 cinder-volume 之間會傳遞當前的失敗重試次數。當 Volume 建立失敗,cinder-volume 會經過 RPC 通知 cinder-scheduler 從新進行調度。
# /opt/stack/cinder/cinder/volume/manager.py # 肯定須要從新調度 if rescheduled: # NOTE(geguileo): Volume was rescheduled so we need to update # volume stats because the volume wasn't created here. # Volume.host is None now, so we pass the original host value. self._update_allocated_capacity(volume, decrement=True, host=original_host)
cinder-scheduler 檢查當前重試次數若沒有超出最大重試次數,則會從新進入調度環節,選擇當前最優存儲節點從新建立 Volume。不然,就觸發 No valid host was found 異常。Nova 一樣具備 RetryFilter 機制,這是爲了防止 「實時資源缺失(調度時參考的資源存量與實際資源存量有差距)」 問題的出現,進一步保障了操做的成功率。
仍是從 REST API 看起:DELETE /v3/{project_id}/volumes/{volume_id}
。刪除的請求比較簡單,指定要刪除的 UUID of Volume。在版本較新的 API 中還支持 cascade 和 force 兩個可選的 request query parameter。
在通過 cinder-api service 一系列的刪除 「預準備(主要是看這個 Volume 刪了適合不適合)」 操做以後,delete volume request 依舊會進入 cinder-volume service。
# /opt/stack/cinder/cinder/volume/manager.py """Deletes and unexports volume. 1. Delete a volume(normal case) Delete a volume and update quotas. 2. Delete a migration volume If deleting the volume in a migration, we want to skip quotas but we need database updates for the volume. 3. Delete a temp volume for backup If deleting the temp volume for backup, we want to skip quotas but we need database updates for the volume. """
最後的最後實際上仍是交由後端存儲設備的驅動程序來完成 「真·卷」 的刪除。固然了,在整個刪除的過程當中還須要處理多種多樣的複雜場景的問題,這裏就不一一列舉了,代碼會清晰的告訴你。
@utils.retry(putils.ProcessExecutionError) def delete(self, name): """Delete logical volume or snapshot. :param name: Name of LV to delete """ def run_udevadm_settle(): cinder.privsep.lvm.udevadm_settle() # LV removal seems to be a race with other writers or udev in # some cases (see LP #1270192), so we enable retry deactivation LVM_CONFIG = 'activation { retry_deactivation = 1} ' try: # 執行 CLI 完成 LV 的刪除 self._execute( 'lvremove', '--config', LVM_CONFIG, '-f', '%s/%s' % (self.vg_name, name), root_helper=self._root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.debug('Error reported running lvremove: CMD: %(command)s, ' 'RESPONSE: %(response)s', {'command': err.cmd, 'response': err.stderr}) LOG.debug('Attempting udev settle and retry of lvremove...') run_udevadm_settle() # The previous failing lvremove -f might leave behind # suspended devices; when lvmetad is not available, any # further lvm command will block forever. # Therefore we need to skip suspended devices on retry. LVM_CONFIG += 'devices { ignore_suspended_devices = 1}' self._execute( 'lvremove', '--config', LVM_CONFIG, '-f', '%s/%s' % (self.vg_name, name), root_helper=self._root_helper, run_as_root=True) LOG.debug('Successfully deleted volume: %s after ' 'udev settle.', name)
Data Module:
# /opt/stack/cinder/cinder/db/sqlalchemy/models.py class Volume(BASE, CinderBase): """Represents a block storage device that can be attached to a vm.""" __tablename__ = 'volumes' __table_args__ = (Index('volumes_service_uuid_idx', 'deleted', 'service_uuid'), CinderBase.__table_args__) id = Column(String(36), primary_key=True) _name_id = Column(String(36)) # Don't access/modify this directly! @property def name_id(self): return self.id if not self._name_id else self._name_id @name_id.setter def name_id(self, value): self._name_id = value @property def name(self): return CONF.volume_name_template % self.name_id ec2_id = Column(Integer) user_id = Column(String(255)) project_id = Column(String(255)) snapshot_id = Column(String(36)) cluster_name = Column(String(255), nullable=True) host = Column(String(255)) # , ForeignKey('hosts.id')) size = Column(Integer) availability_zone = Column(String(255)) # TODO(vish): foreign key? status = Column(String(255)) # TODO(vish): enum? attach_status = Column(String(255)) # TODO(vish): enum migration_status = Column(String(255)) scheduled_at = Column(DateTime) launched_at = Column(DateTime) terminated_at = Column(DateTime) display_name = Column(String(255)) display_description = Column(String(255)) provider_location = Column(String(255)) provider_auth = Column(String(255)) provider_geometry = Column(String(255)) provider_id = Column(String(255)) volume_type_id = Column(String(36)) source_volid = Column(String(36)) encryption_key_id = Column(String(36)) consistencygroup_id = Column(String(36), index=True) group_id = Column(String(36), index=True) bootable = Column(Boolean, default=False) multiattach = Column(Boolean, default=False) replication_status = Column(String(255)) replication_extended_status = Column(String(255)) replication_driver_data = Column(String(255)) previous_status = Column(String(255)) consistencygroup = relationship( ConsistencyGroup, backref="volumes", foreign_keys=consistencygroup_id, primaryjoin='Volume.consistencygroup_id == ConsistencyGroup.id') group = relationship( Group, backref="volumes", foreign_keys=group_id, primaryjoin='Volume.group_id == Group.id') service_uuid = Column(String(36), index=True) service = relationship(Service, backref="volumes", foreign_keys=service_uuid, primaryjoin='Volume.service_uuid == Service.uuid') shared_targets = Column(Boolean, default=True) # make an FK of service?
數據庫屬性:
MariaDB [cinder]> desc volumes; +-----------------------------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-----------------------------+--------------+------+-----+---------+-------+ | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | | deleted_at | datetime | YES | | NULL | | | deleted | tinyint(1) | YES | | NULL | | | id | varchar(36) | NO | PRI | NULL | | | ec2_id | varchar(255) | YES | | NULL | | | user_id | varchar(255) | YES | | NULL | | | project_id | varchar(255) | YES | | NULL | | | host | varchar(255) | YES | | NULL | | | size | int(11) | YES | | NULL | | | availability_zone | varchar(255) | YES | | NULL | | | status | varchar(255) | YES | | NULL | | | attach_status | varchar(255) | YES | | NULL | | | scheduled_at | datetime | YES | | NULL | | | launched_at | datetime | YES | | NULL | | | terminated_at | datetime | YES | | NULL | | | display_name | varchar(255) | YES | | NULL | | | display_description | varchar(255) | YES | | NULL | | | provider_location | varchar(256) | YES | | NULL | | | provider_auth | varchar(256) | YES | | NULL | | | snapshot_id | varchar(36) | YES | | NULL | | | volume_type_id | varchar(36) | YES | | NULL | | | source_volid | varchar(36) | YES | | NULL | | | bootable | tinyint(1) | YES | | NULL | | | provider_geometry | varchar(255) | YES | | NULL | | | _name_id | varchar(36) | YES | | NULL | | | encryption_key_id | varchar(36) | YES | | NULL | | | migration_status | varchar(255) | YES | | NULL | | | replication_status | varchar(255) | YES | | NULL | | | replication_extended_status | varchar(255) | YES | | NULL | | | replication_driver_data | varchar(255) | YES | | NULL | | | consistencygroup_id | varchar(36) | YES | MUL | NULL | | | provider_id | varchar(255) | YES | | NULL | | | multiattach | tinyint(1) | YES | | NULL | | | previous_status | varchar(255) | YES | | NULL | | | cluster_name | varchar(255) | YES | | NULL | | | group_id | varchar(36) | YES | MUL | NULL | | | service_uuid | varchar(36) | YES | MUL | NULL | | | shared_targets | tinyint(1) | YES | | NULL | | +-----------------------------+--------------+------+-----+---------+-------+ 39 rows in set (0.00 sec)
從 Havana 開始 Cinder 支持經過啓用多個不一樣類型的存儲後端(Backend),併爲每個 Backend 啓動一個 cinder-volume service 服務進程。對於用戶而言,只須要經過對配置文件的修改便可實現。
e.g. 同時啓用 LVM 和 GlusterFS 後端存儲
# /etc/cinder/cinder.conf [DEFAULT] ... enabled_backends =lvmdriver,glusterfs [lvmdriver] volume_driver = cinder.volume.drivers.lvm.LVMISCSIDriver volume_group = cinder-volumes volume_backend_name =LVM_iSCSI [glusterfs] volume_driver =cinder.volume.drivers.glusterfs.GlusterfsDriver glusterfs_shares_config =/etc/cinder/glusterfs_shares glusterfs_mount_point_base =/var/lib/nova glusterfs_disk_util = df glusterfs_qcow2_volumes = true glusterfs_sparsed_volumes = true volume_backend_name = GlusterFS
volume_backend_name
:會被 Volume Type 功能用到,經過 --property volume_backend_name
屬性來設置。openstack volume service list
查看。NOTE:開發調試階段不建議開啓多後端存儲或者建議使用 remote_pdb 進行調試。
Tranfer Volume 將 Volume 的擁有權從一個 Tenant 用戶轉移到另外一個 Tenant 用戶。該功能的本質就是 修改了 volume 的 tenant id 屬性(os-vol-tenant-attr:tenant_id)而已。
cinder transfer-create
建立 tranfer 時候會產生 transfer id
和 authkey
。cinder transfer-accept
接受 transfer 時將 1 中的 transfer id
和 auth_key
填入便可。Migrate Volume 即將 Volume 從一個 Backend 遷移至另外一個 Backend。
if 若是 Volume 沒有 Attach:
else Volume 已經被 Attach 到虛擬機:
NOTE:migration-policy
有兩個選項 never 和 on-demand,若是設置了 never,是沒法實現 Volume 遷移的。
Mutli-Attach - Support for attaching a single Cinder volume to multile VM instances.
OpenStack’s Mutli-Attach Feature 在早前的版本的定義是:容許一個 RO volume 經 iSCSI/FC 被 Attached 到多個 VM;直到 Queens 版本以後,Mutil-Attach 的定義被更新爲:「The ability to attach a volume to multiple hosts/servers simultaneously is a use case desired for active/active or active/standby scenarios.」,即 Volume 支持以 RO/RW 的方式被掛載到多個 VM。
官方文檔:Volume multi-attach: Enable attaching a volume to multiple servers
Multi-Attach 最直接的價值就是支撐核心業務數據盤的高可用冗餘特性,好比說:一個 Volume 同時掛載在兩個 VM 上,當 ACTIVE VM 失聯時,能夠由另外的 PASSIVE VM 繼續訪問(RO、R/W)這個卷。
Multi-Attach RO:以 RO 模式將 Volume Attach 到另外一個主機/服務器。問題在於,要求使用者(e.g. KVM)知道如何在 Volume 上設置只讀模式和強制執行只讀模式。
Multi-Attach RW:以 RW 模式將 Volume Attach 到另外一個主機/服務器。這種狀況下,多個 Attachment(Volume 與 Instance 的隱射關係)之間沒有區別,它們都被視爲獨立項目,而且都是讀寫卷。
NOTE:目前,全部 Volume 均可以以 RW 模式 Attach,包括 Boot From Volume 模式。
代碼實現原理:
使用:
# 建立 Multi-Attach Volume Type cinder type-create multiattach cinder type-key multiattach set multiattach="<is> True" # Create Volume cinder create 10 --name multiattach-volume --volume-type <volume_type_uuid> # multiattach-volume nova volume-attach test01 <volume_uuid> nova volume-attach test02 <volume_uuid>
對於版本比較舊的 Cinder(< Q)可使用下述方式進行 Multi-Attach:
[root@overcloud-controller-0 ~]# cinder create --allow-multiattach --name vol_shared 1 +--------------------------------+--------------------------------------+ | Property | Value | +--------------------------------+--------------------------------------+ | attachments | [] | | availability_zone | nova | | bootable | false | | consistencygroup_id | None | | created_at | 2019-03-04T02:44:51.000000 | | description | None | | encrypted | False | | id | 240a94b6-6f8c-4797-9379-eebeda984c95 | | metadata | {} | | migration_status | None | | multiattach | True | | name | vol_shared | | os-vol-host-attr:host | None | | os-vol-mig-status-attr:migstat | None | | os-vol-mig-status-attr:name_id | None | | os-vol-tenant-attr:tenant_id | f745745cebce4a609f074b0121ae3a53 | | replication_status | disabled | | size | 1 | | snapshot_id | None | | source_volid | None | | status | creating | | updated_at | None | | user_id | 11b816e454384d038472c7c89d2544f4 | | volume_type | None | +--------------------------------+--------------------------------------+ [root@overcloud-controller-0 ~]# openstack volume show vol_shared +--------------------------------+------------------------------------------------+ | Field | Value | +--------------------------------+------------------------------------------------+ | attachments | [] | | availability_zone | nova | | bootable | false | | consistencygroup_id | None | | created_at | 2019-03-04T02:44:51.000000 | | description | None | | encrypted | False | | id | 240a94b6-6f8c-4797-9379-eebeda984c95 | | migration_status | None | | multiattach | True | | name | vol_shared | | os-vol-host-attr:host | hostgroup@tripleo_iscsi#tripleo_iscsi_fanguiju | | os-vol-mig-status-attr:migstat | None | | os-vol-mig-status-attr:name_id | None | | os-vol-tenant-attr:tenant_id | f745745cebce4a609f074b0121ae3a53 | | properties | | | replication_status | disabled | | size | 1 | | snapshot_id | None | | source_volid | None | | status | available | | type | None | | updated_at | 2019-03-04T02:44:52.000000 | | user_id | 11b816e454384d038472c7c89d2544f4 | +--------------------------------+------------------------------------------------+ [root@overcloud-controller-0 ~]# openstack server add volume VM1 vol_shared [root@overcloud-controller-0 ~]# openstack server add volume VM2 vol_shared [root@overcloud-controller-0 ~]# openstack volume show 87213d56-8bb3-494e-9477-5cea3f0fed83 +--------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Field | Value | +--------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | attachments | [{u'server_id': u'6cd7f5e2-9782-446c-b9fe-c9423341ac23', u'attachment_id': u'136ce8d6-e82e-4db6-8742-6c4d0d4fa410', u'attached_at': u'2019-03-04T07:34:58.000000', u'host_name': None, u'volume_id': | | | u'87213d56-8bb3-494e-9477-5cea3f0fed83', u'device': u'/dev/vdb', u'id': u'87213d56-8bb3-494e-9477-5cea3f0fed83'}, {u'server_id': u'e9131a29-c32e-4f8f-a1ac-7e8d09bb09d3', u'attachment_id': u'502cc9b7-cc3e-4796-858b-940ba6423788', | | | u'attached_at': u'2019-03-04T07:30:12.000000', u'host_name': None, u'volume_id': u'87213d56-8bb3-494e-9477-5cea3f0fed83', u'device': u'/dev/vdb', u'id': u'87213d56-8bb3-494e-9477-5cea3f0fed83'}, {u'server_id': u'120d49e5-8942-4fec- | | | a54c-6e2ab5ab4bf2', u'attachment_id': u'd0fd3744-6135-485f-a358-c5e333658a32', u'attached_at': u'2019-03-04T07:36:25.000000', u'host_name': None, u'volume_id': u'87213d56-8bb3-494e-9477-5cea3f0fed83', u'device': u'/dev/vdb', u'id': | | | u'87213d56-8bb3-494e-9477-5cea3f0fed83'}] | | availability_zone | nova | | bootable | false | | consistencygroup_id | None | | created_at | 2019-03-04T07:20:40.000000 | | description | None | | encrypted | False | | id | 87213d56-8bb3-494e-9477-5cea3f0fed83 | | migration_status | None | | multiattach | True | | name | vol_shared | | os-vol-host-attr:host | hostgroup@tripleo_iscsi#tripleo_iscsi_fanguiju | | os-vol-mig-status-attr:migstat | None | | os-vol-mig-status-attr:name_id | None | | os-vol-tenant-attr:tenant_id | f745745cebce4a609f074b0121ae3a53 | | properties | attached_mode='rw', readonly='False' | | replication_status | disabled | | size | 1 | | snapshot_id | None | | source_volid | None | | status | in-use | | type | None | | updated_at | 2019-03-04T07:36:26.000000 | | user_id | 11b816e454384d038472c7c89d2544f4 | +--------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
Volume Type 常被用來知足用戶對 Volume 的一些自定義需求,例如:選擇存儲後端,開啓 Thin/Thick Provisioning(精簡置備與厚置備),Deduplication(去重)或 Compression(壓縮)等高級功能。
EXAMPLE 1:假設有兩個 Cinder 存儲池,後端名(backend_name)分別是 ssd_pool 與 hdd_pool。如何實現建立 Volume 時指定後端的存儲池?
# 1. 分別建立兩個 volume type:ssd-type 與 hdd-type cinder type create "ssd-type" cinder type create "hdd-type" # 2. 爲這兩個 volume type 指定後端名 cinder type-key "ssd-type" set volume_backend_name=ssd_pool cinder type-key "hdd-type" set volume_backend_name=hdd_pool # 3. 指定 volume type 建立 volume cinder create --name --volume-type ssd-type cinder create --name --volume-type hdd-type
如此 Cinder 就會根據 volume type 所具備的 extra spec key(e.g. volume_backend_name:ssd_pool),將 Volume 建立到 backend_name 所對應的 Cinder Backend 上(Backend Section 會定義 backend_name 配置項)。
EXAMPLE 2:開啓 Thin Provisioning(精簡置備),首先須要注意的是,Thin Provisioning 須要後端存儲設備支持。若是使用 Ceph 做爲存儲後端,則不須要考慮開啓此功能,由於 Ceph 的 rbd device 默認就是 Thin Provisioning 且沒法選擇。
# 1. 建立一個 volume type:thin-type cinder type create "thin-type" # 2. 爲 thin-type 設定 extra-spec: cinder type-key "thin-type" set provisioning:type=thin cinder type-key "thin-type" set thin_provisioning_support="True" # 3. 使用 thin-type 建立 volume cinder create --name --volume-type thin-type
EXAMPLE:若是切換 Volume 的 Thin/Thick provisioning?使用 Cinder Retype 功能,Retype 經常使用來改變 Volume 的 volume type,從而達到切換或開關某項功能(特性)的做用。
# retype Changes the volume type for a volume. usage: cinder retype [--migration-policy <never|on-demand>] <volume> <volume-type>
Volume Type 能夠用來選擇後端存儲,那 retype 就能夠用來改變 Volume 的後端存儲池,因此 retype 能夠被用來實現 Volume 的遷移功能。e.g. 用戶有一個屬於 ssd_pool 的 Volume,想把其遷移至 hdd_pool 裏,能夠用 retype 命令實現:
cinder retype --migration-policy on-demand hdd-type
在《[Cinder] 存儲 Qos》一文中已經說到,這裏再也不贅述。簡單演示一個示例:
# 1. 建立兩個 QoS 規格與 volume type: cinder qos-create qos1 write_iops_sec=500 cinder qos-create qos2 write_iops_sec=50000 cinder type create "qos1-type" cinder type create "qos2-type" # 2. 將 QoS 規格與 volume type 關聯: cinder qos-associate # 3. 建立一個 volume,並設置其 QoS 規格爲 qos1 cinder create --name --volume-type qos1-type # 4. 從新設置 volume 的 QoS 規格爲 qos2 cinder retype qos2-type
設置或更改 Volume 的 QoS 須要注意兩點:
Replication(複製)、Snapshot(快照)、Backup(備份)是存儲領域最爲常見的數據保護三劍客。Cinder 從 Juno 版開始支持 Replication。
在衡量數據保護技術優劣時,一般使用 RPO(Recovery Point Objective,復原點目標,當服務恢復後得來的數據所對應的時間點)與 RTO(Recovery Time Objective,復原時間目標,企業可允許服務中斷的時間長度)兩個指標。優秀的數據保護技術可使得 RPO=0 且 RTO 趨近於 0。Replication 與 Snapshot 相比,它不依賴於數據源,能夠作到異地容災;而與 Backup 相比,它輕量,能夠恢復到任意時間點數據源。Replication 技術因其可靠的數據保護做用,經常被用來構建高可用數據中心。
簡單來講,
Replication 經過在兩個 Volume(卷)之間建立一個 Session(會話),並進行數據同步。這兩個 Volume 能夠處於同一臺存儲陣列中,也能夠處於兩個異地的存儲陣列中。但同時只有一個 Volume 會提供生產服務,當提供生產服務的 Primary Volume 發生故障時,當即切換至備用的 Secondary Volume,從而保證業務的連續性。Replication 根據數據同步的方式分爲 Sync(同步)與 Async(異步)兩種模式。Sync 模式下,數據在寫入 Primary Volume 與 Secondary Volume 後纔可用;Async 模式反之,響應時間更短。所以,只有 Sync Replication 能夠提供 RPO=0 的保障,但 Volume 性能卻見不得是最高的。
Cinder Replication 操做對象:
Failover:是一個具體的 Backend,咱們成爲 Replication Backend(以區別於承載業務的 Cinder Backend)。當用於生產的某個 Cinder Backend 發生故障時,其中全部的 Volume 將不可訪問,從而致使業務中斷。這時能夠考慮其進行 Cinder Replication 的 failover-host 操做以恢復被中斷的業務。在進行 failover-host 操做時須要指定的 backend-id 就是 Replication 目的端(Replication Backend),當 failover-host 操做完成後,Cinder Backend 上全部的 Volume 將由目的端上的 Volume 替代,提供生產服務。固然了,前提是運管人員爲 Cinder Backend 上全部的 Volume 都啓用了 Replication 功能,若是 Cinder Backend 上的某些 Volume 沒有開啓 Replication 功能,那麼這些 Volume 將不會 Failover 到目的端。
Replication 目的端:Cinder 支持一對多的 Replication,即一個 Cinder Backend 能夠 Replicate 到多個目的端,但並不是全部後端存儲設備都支持一對多的 Relication。Replication 目的端的信息須要在 cinder.conf 裏配置,其中 backend_id 是必須配置的,該參數用於進行 failover-host 操做時指定目的端。Replication 目的端通常是災備數據中心的存儲陣列,用來作主業務數據中心的冗餘設備。
# Multi opt of dictionaries to represent the aggregate mapping between source # and destination back ends when using whole back end replication. For every # source aggregate associated with a cinder pool (NetApp FlexVol), you would # need to specify the destination aggregate on the replication target device. A # replication target device is configured with the configuration option # replication_device. Specify this option as many times as you have replication # devices. Each entry takes the standard dict config form: # netapp_replication_aggregate_map = # backend_id:<name_of_replication_device_section>,src_aggr_name1:dest_aggr_name1,src_aggr_name2:dest_aggr_name2,...
Failback:當 Cinder Backend 源端的故障排除後,能夠對該 Cinder Backend 執行 Failback 操做,即將生產業務從 Replication 目的端從新接管回阿里。這樣 Cinder Backend 源端的 Volume 就能夠繼續提供服務了。並且在故障處理期間,向 Replication 目的端的 Volumes 寫入的數據也會同步回源端。
Sync/Async:目前 Cinder 並不支持顯示的設置 Replication 的同步模式,這是由於 Replication 極度依賴實際的後端存儲設備功能集以及驅動程序的實現。通常而言,各存儲廠商的具體實現中,會使用 volume type 中的 extra spec key 來設置 Volume 的 Replication Sync/Async 模式。
Step 1. 建立開啓 Replication 的 Volume
cinder type-create replication-type cinder type-key replication-type set replication_enabled=True
cinder create --volume-type replication_type --name vol001 100
Step 2. 發生故障時對指定的 Cinder Backend 執行 Failover 操做
cinder failover-host --backend-id storage-node2@replication2
Step 3. 待 Cinder Backend 源端故障清理完畢,執行 Failback 操做
cinder failover-host --backend-id <cinder_backend> <replication_backend> # e.g. cinder failover-host --backend-id default storage-node2@replication2
在瞭解 Cinder 的分佈式鎖以前,先了解 Cinder 的本地鎖。顯然 Volume 資源自己也是一種共享資源,也須要處理併發訪問衝突的問題,好比:刪除一個 Volume 時,另外一個線程正在基於該 Volume 建立快照;又或者同時有兩個線程都在執行 Volume 掛載操做。cinder-volume 也是使用鎖機制來實現 Volume 資源的併發訪問的,Volume 的刪除、掛載、卸載等操做都會對 Volume 上鎖。在 Newton 版本之前,cinder-volume 的鎖實現是基於本地文件實現的,使用了 Linux 的 flock 工具進行鎖的管理。Cinder 執行加鎖操做默認會從配置指定的 lockpath 目錄下建立一個命名爲 cinder-volume_uuid-{action}
的空文件,並對該文件使用 flock 加鎖。flock 只能做用於同一個操做系統的文件鎖。即便使用共享存儲,另外一個操做系統也不能經過 flock 工具判斷該空文件是否有鎖。可見 Cinder 使用的是本地鎖。
本地鎖的侷限,只可以保證同一個 cinder-volume 下的共享資源(e.g. Volume)的數據一致性,也就只能使用 A/P 主備的模式進行高可用,不能管理分佈式 cinder-volume 下共享資源,致使了 cinder-volume 不支持多實例高可用的問題。因此爲了不 cinder-volume 服務宕機,就須要引入自動恢復機制來進行管理。好比: Pacemaker,Pacemaker 輪詢判斷 cinder-volume 的存活狀態,一旦發現掛了,Pacemaker 會嘗試重啓服務。若是 Pacemaker 依然重啓失敗,則嘗試在另外一臺主機啓動該服務,實現故障的自動恢復。
顯然,這種方式是初級而原始的。你或許會想到引入分佈式鎖,好比 Zookeeper、Etcd 等服務,但這須要用戶本身部署和維護一套 DLM,無疑增長了運維的成本,而且也不是全部的存儲後端都須要分佈式鎖。Cinder 社區爲了知足不一樣用戶、不一樣場景的需求,並無強制用戶部署固定的 DLM,而是採起了很是靈活的可插除方式,就是 Tooz 庫。
引入了 Tooz 庫以後,當用戶不須要分佈式鎖時,只須要指定後端爲本地文件便可,此時不須要部署任何 DLM,和引入分佈式鎖以前的方式保持一致,基本不須要執行大的變動。當用戶須要 cinder-volume 支持 AA 時,能夠選擇部署一種 DLM,好比 Zookeeper 服務。Cinder 對 Tooz 又封裝了一個單獨的 coordination 模塊,其源碼位於 cinder/coordination.py,當代碼須要使用同步鎖時,只須要在函數名前面加上 @coordination.synchronized
裝飾器便可,方便易用,而且很是統一。
社區爲了解決項目中的分佈式問題,開發了一個很是靈活的通用框架,項目名爲 Tooz,它是一個 Python 庫,提供了標準的 coordination API,其主要目標是解決分佈式系統的通用問題,好比節點管理、主節點選舉以及分佈式鎖等。簡而言之,Tooz 實現了很是易用的分佈式鎖接口。
Tooz 封裝了一套鎖管理的庫或者框架,只須要簡單調用 lock、trylock、unlock 便可完成實現,不用關心底層細節,也不用瞭解後端到底使用的是 Zookeeper、Redis 仍是 Etcd。使用 Tooz 很是方便,只須要三步:
coordinator = coordination.get_coordinator('zake://', b'host-1’) coordinator.start() #Create a lock lock = coordinator.get_lock("foobar」) with lock: … print("Do something that is distributed」) coordinator.stop()
從 Cinder 架構圖能夠看出,Nova 和 Cinder 是交互最密切的兩個項目。Cinder 的功能集涵蓋了 Nova 虛擬機的整個生命週期。
Cinder 與 Nova 的協同方式:
下面以 LVMVolumeDriver + iSCSI target_driver 的組合來講明 Cinder 是如何經過 iSCSI 協議將 LVM LV(Volume) 掛載到計算節點中供虛擬機看成數據盤或系統盤(Boot from volume)使用的。
LVM backend 的配置:
[lvmdriver-1] image_volume_cache_enabled = True volume_clear = zero lvm_type = auto target_helper = lioadm volume_group = stack-volumes-lvmdriver-1 volume_driver = cinder.volume.drivers.lvm.LVMVolumeDriver volume_backend_name = lvmdriver-1
從上述配置能夠看出一個 VG 就對應一個 LVM Backend,一個 VG(Backend)有能夠劃分爲多個 LV(Volume),Volume 經過 iSCSI 的方式掛載到計算節點做爲一個塊設備(/dev/sdx),而後這個塊設備會被 qemu-kvm 提供給虛擬機使用。
iSCSI 協議和工具很是經常使用,這裏再也不贅述,只是簡單說明 iSCSI 掛載卷的過程:
Initiator: iqn.1993-08.org.debian:01:8d794081cd6a alias: compute1
iscsiadm -m session
通常的,在劃分網絡的時候應該考慮是否須要將 OpenStack Management Network 與 OpenStack Storage Network 分開,避免網絡阻塞(e.g. Download/Upload 大數據)。此時能夠經過 cinder-volume service 的配置文件 cinder.conf 中的 iscsi_ip_address
、iscsi_port
來指定 iSCSI Session 所使用的網卡,從而作到管理網絡(ControlPath)和數據網絡(DataPath)分離的效果。
固然,除了 LVMISCSIDriver 以外,Cinder 還支持很是豐富的後端存儲類型(e.g. IBM SVC、DS8K、XIV)以及數據傳輸方式(e.g. FC、iSCSI)。