OpenStack虛擬機掛載數據卷過程分析

1 關於OpenStack

OpenStack是一個IaaS開源項目,實現公有雲和私有雲的部署及管理,目前已經成為了最流行的一種開源雲解決方案。其中提供計算服務組件Nova、網路服務組件Neutron以及塊存儲服務組件Cinder是OpenStack的最為核心的組件。這裡我們重點關注Nova和Cinder組件,Neutron組件將在下一篇文章中詳細介紹。

1.1 計算服務Nova

Nova組件為OpenStack提供計算服務(Compute as Service),類似AWS的EC2服務。Nova管理的主要對象為雲主機(server),用戶可通過Nova API申請雲主機(server)資源。雲主機通常對應一個虛擬機,但不是絕對,也有可能是一個容器(docker driver)或者裸機(對接ironic driver)。

Nova創建一台雲主機的三個必要參數為:

  • image: 即雲主機啟動時的鏡像,這個鏡像source可能是從Glance中下載,也有可能是Cinder中的一個volume卷(boot from volume)。
  • flavor: flavor包含申請的資源數量,比如CPU核數、內存大小以及根磁碟大小、swap大小等。除了資源數量,flavor還包含一些特性配置,稱為extra specs,可以實現設置io限速、cpu拓撲等功能。
  • network: 雲主機的租戶網路。

創建一台雲主機的CLI為:

nova boot --image ${IMAGE_ID} --flavor m1.small --nic net-id=${NETWORK_ID} int32bit-test-1n

使用nova list可以查看租戶的所有雲主機列表。

1.2 塊存儲服務Cinder

Cinder組件為OpenStack提供塊存儲服務(Block Storage as Service),類似AWS的EBS服務。Cinder管理的主要對象為數據卷(volume),用戶通過Cinder API可以對volume執行創建、刪除、擴容、快照、備份等操作。

創建一個volume有兩個必要參數:

  • volume_type: volume_type關聯了後端存儲信息,比如存儲後端、QoS信息等。
  • size: 創建volume的大小。

創建一個20G的volume:

cinder create --volume-type ssd --name int32bit-test-volume 20n

Cinder目前最典型的應用場景就是為Nova雲主機提供雲硬碟功能,用戶可以把一個volume卷掛載到Nova的雲主機中,當作雲主機的一個虛擬塊設備使用。

掛載volume是在Nova端完成的:

nova volume-attach ${server_id} ${volume_id} n

Cinder除了能夠為Nova雲主機提供雲硬碟功能,還能為裸機、容器等提供數據卷功能。john griffith寫了一篇博客介紹如何使用Cinder為Docker提供volume功能:Cinder providing block storage for more than just Nova。

本文接下來將重點介紹OpenStack如何將volume掛載到虛擬機中,分析Nova和Cinder之間的交互過程。

2 存儲基礎

2.1 什麼是iSCSI

iSCSI是一種通過TCP/IP共享塊設備的協議,通過該協議,一台伺服器能夠把本地的塊設備共享給其它伺服器。換句話說,這種協議實現了通過internet向設備發送SCSI指令。

iSCSI server端稱為Target,client端稱為Initiator,一台伺服器可以同時運行多個Target,一個Target可以認為是一個物理存儲池,它可以包含多個backstores,backstore就是實際要共享出去的設備,實際應用主要有兩種類型:

  • block。即一個塊設備,可以是本地的一個硬碟,如/dev/sda,也可以是一個LVM卷。
  • fileio。把本地的一個文件當作一個塊設備,如一個raw格式的虛擬硬碟。

除了以上兩類,還有pscsi、ramdisk等。

backstore需要添加到指定的target中,target會把這些物理設備映射成邏輯設備,並分配一個id,稱為LUN(邏輯單元號)。

為了更好的理解iSCSI,我們下節將一步步手動實踐下如何使用iSCSI。

2.2 iSCSI實踐

首先我們準備一台iscsi server伺服器作為target,這裡以CentOS 7為例,安裝並啟動iscsi服務:

yum install targetcli -ynsystemctl enable targetnsystemctl start targetn

運行targetcli檢查是否安裝成功:

int32bit $ targetclintargetcli shell version 2.1.fb41nCopyright 2011-2013 by Datera, Inc and others.nFor help on commands, type help.nn/> lsno- / .................................... [...]n o- backstores ......................... [...]n | o- block ............. [Storage Objects: 0]n | o- fileio ............ [Storage Objects: 0]n | o- pscsi ............. [Storage Objects: 0]n | o- ramdisk ........... [Storage Objects: 0]n o- iscsi ....................... [Targets: 0]n o- loopback .................... [Targets: 0]n

如果正常的話會進入targetcli shell,在根目錄下運行ls命令可以查看所有的backstores和iscsi target。

具體的targetcli命令可以查看官方文檔,這裡需要說明的是,targetcli shell是有context session(上下文),簡單理解就是類似Linux的文件系統目錄,你處於哪個目錄位置,對應不同的功能,比如你在/backstores目錄則可以對backstores進行管理,你在/iscsi目錄,則可以管理所有的iscsi target。你可以使用pwd查看當前工作目錄,cd切換工作目錄,help查看當前工作環境的幫助信息,ls查看子目錄結構等,你可以使用tab鍵補全命令,和我們Linux shell操作非常相似,因此使用起來還是比較順手的。

為了簡單起見,我們創建一個fileio類型的backstore,首先我們cd到/backstores/fileio目錄:

/> cd /backstores/fileion/backstores/fileio> create test_fileio /tmp/test_fileio.raw 2G write_back=falsenCreated fileio test_fileio with size 2147483648n

我們創建了一個名為test_fileio的fileio類型backstore,文件路徑為/tmp/test_fileio.raw,大小為2G,如果文件不存在會自動創建。

創建了backstore後,我們創建一個target,cd到/iscsi目錄:

/iscsi> create iqn.2017-09.me.int32bit:int32bitnCreated target iqn.2017-09.me.int32bit:int32bit.nCreated TPG 1.nDefault portal not created, TPGs within a target cannot share ip:port.n/iscsi>n

以上我們創建了一個名為int32bit的target,前面的iqn.2017-09.me.int32bit是iSCSI Qualified Name (IQN),具體含義參考wikipedia-ISCSI,這裡簡單理解為一個獨一無二的namespace就好。使用ls命令我們發現創建一個目錄iqn.2017-09.me.int32bit:int32bit(注意:實際上並不是目錄,我們暫且這麼理解)。

創建完target後,我們還需要把這個target export出去,即進入監聽狀態,我們稱為portal,創建portal也很簡單:

/iscsi> cd iqn.2017-09.me.int32bit:int32bit/tpg1/portals/n/iscsi/iqn.20.../tpg1/portals> create 10.0.0.4nUsing default IP port 3260nCreated network portal 10.0.0.4:3260.n

以上10.0.0.4是server的ip,不指定埠的話就會使用默認的埠3260。

target創建完畢,此時我們可以把我們之前創建的backstore加到這個target中:

/iscsi/iqn.20.../tpg1/portals> cd ../lunsn/iscsi/iqn.20...bit/tpg1/luns> create /backstores/fileio/test_fileionCreated LUN 0.n

此時我們的target包含有一個lun設備了:

/iscsi/iqn.20...bit/tpg1/luns> ls /iscsi/iqn.2017-09.me.int32bit:int32bit/no- iqn.2017-09.me.int32bit:int32bit ...................................................................................... [TPGs: 1]n o- tpg1 ................................................................................................... [no-gen-acls, no-auth]n o- acls .............................................................................................................. [ACLs: 0]n o- luns .............................................................................................................. [LUNs: 1]n | o- lun0 .......................................................................... [fileio/test_fileio (/tmp/test_fileio.raw)]n o- portals ........................................................................................................ [Portals: 0]n

接下來我們配置client端,即iSCSI Initiator:

yum install iscsi-initiator-utils -ynsystemctl enable iscsid iscsinsystemctl start iscsid iscsin

拿到本機的initiator name:

int32bit $ cat /etc/iscsi/initiatorname.iscsinInitiatorName=iqn.1994-05.com.redhat:e0db637c5cen

client需要連接server target,還需要ACL認證,我們在server端增加client的訪問許可權,在server端運行:

int32bit $ targetclintargetcli shell version 2.1.fb41nCopyright 2011-2013 by Datera, Inc and others.nFor help on commands, type help.nn/> cd /iscsi/iqn.2017-09.me.int32bit:int32bit/tpg1/aclsn/iscsi/iqn.20...bit/tpg1/acls> create iqn.1994-05.com.redhat:e0db637c5cenCreated Node ACL for iqn.1994-05.com.redhat:e0db637c5cenCreated mapped LUN 0.n

注意:以上我們沒有設置賬戶和密碼,client直接就能登錄。

一切準備就緒,接下來讓我們在client端連接我們的target吧。

首先我們使用iscsiadm命令自動發現本地可見的target列表:

int32bit $ iscsiadm --mode discovery --type sendtargets --portal 10.0.0.4 | grep int32bitn10.0.0.4:3260,1 iqn.2017-09.me.int32bit:int32bitn

發現target後,我們登錄驗證後才能使用:

int32bit $ iscsiadm -m node -T iqn.2017-09.me.int32bit:int32bit -lnLogging in to [iface: default, target: iqn.2017-09.me.int32bit:int32bit, portal: 10.0.0.4,3260] (multiple)nLogin to [iface: default, target: iqn.2017-09.me.int32bit:int32bit, portal: 10.0.0.4,3260] successful.n

我們可以查看所有已經登錄的target:

int32bit $ iscsiadm -m sessionntcp: [173] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-1e062767-f0bc-40fb-9a03-7b0df61b5671 (non-flash)ntcp: [198] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-060fe764-c17b-45da-af6d-868c1f5e19df (non-flash)ntcp: [199] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-757f6281-8c71-430e-9f7c-5df2e3008b46 (non-flash)ntcp: [203] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063 (non-flash)ntcp: [205] 10.0.0.4:3260,1 iqn.2017-09.me.int32bit:int32bit (non-flash) n

此時target已經自動映射到本地塊設備,我們可以使用lsblk查看:

int32bit $ lsblk --scsinNAME HCTL TYPE VENDOR MODEL REV TRANn... # 省略部分輸出nsdh 183:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsinsdi 208:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsinsdj 209:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsinsdk 213:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsinsdm 215:0:0:0 disk LIO-ORG test_fileio 4.0 iscsin

可見映射本地設備為/dev/shm。接下來就可以當作本地硬碟一樣使用了。

以上我們是通過target伺服器的一個本地文件以塊形式共享的,通常這只是用來測試,生產環境下一般都通過商業存儲提供真實的塊設備來共享。OpenStack Cinder如果使用的LVM driver,則是通過LVM卷共享的,這其實不難實現,只需要把LVM對應LV PATH加到block backstore即可,本文後面會重點介紹這種情況。

2.3 cinder-rtstool工具簡介

前面我們使用的targetcli是Datera公司開發的,不僅提供了這個CLI工具,Datera還提供了一個Python庫-rtslib,該項目地址為rtslib。可能由於某些原因,社區fork自rtslib項目,並單獨維護了一個分支,命名為「free branch」,即rtslib-fb項目,目前這兩個分支可能不兼容,因此確保targetcli、rtslib以及configshell是在同一個版本分支,要麼全是fb,要麼全是non-fb。

OpenStack社區基於rtstool封裝了一個CLI工具,即我們要介紹的cinder-rtstool工具。該工具使用起來非常簡單,我們查看它的help信息:

$ cinder-rtstool --helpnUsage:ncinder-rtstool create [device] [name] [userid] [password] [iser_enabled] <initiator_iqn,iqn2,iqn3,...> [-a<IP1,IP2,...>] [-pPORT]ncinder-rtstool add-initiator [target_iqn] [userid] [password] [initiator_iqn]ncinder-rtstool delete-initiator [target_iqn] [initiator_iqn]ncinder-rtstool get-targetsncinder-rtstool delete [iqn]ncinder-rtstool verifyncinder-rtstool save [path_to_file]n

該工具主要運行在target端,即cinder-volume所在節點,其中create命令用於快速創建一個target,並把設備加到該target中,當然也包括創建對應的portal。add-initiator對應就是創建acls,get-targets列出當前伺服器的創建的所有target。其它命令不過多介紹,基本都能大概猜出什麼功能。

2.4 ceph rbd介紹

Ceph是開源分散式存儲系統,具有高擴展性、高性能、高可靠性等優點,同時提供塊存儲服務(rbd)、對象存儲服務(rgw)以及文件系統存儲服務(cephfs)。目前也是OpenStack的主流後端存儲,為OpenStack提供統一共享存儲服務。使用ceph作為OpenStack後端存儲,至少包含以下幾個優點:

  1. 所有的計算節點共享存儲,遷移時不需要拷貝塊設備,即使計算節點掛了,也能立即在另一個計算節點啟動虛擬機(evacuate)。
  2. 利用COW特性,創建虛擬機時,只需要基於鏡像clone即可,不需要下載整個鏡像,而clone操作基本是0開銷。
  3. ceph rbd支持thin provisioning,即按需分配空間,有點類似Linux文件系統的sparse稀疏文件。你開始創建一個20GB的虛擬硬碟時,實際上不佔用真正的物理存儲空間,只有當寫入數據時,才逐一分配空間,從而實現了磁碟的overload。

ceph的更多知識可以參考官方文檔,這裡我們僅僅簡單介紹下rbd。

前面我們介紹的iSCSI有個target的概念,存儲設備必須加到指定的target中,映射為lun。rbd中也有一個pool的概念,rbd創建的虛擬塊設備實例我們稱為image,所有的image必須包含在一個pool中。這裡我們暫且不討論pool的作用,簡單理解是一個namespace即可。

我們可以通過rbd命令創建一個rbd image:

$ rbd -p test2 create --size 1024 int32bit-test-rbd --new-formatn$ rbd -p test2 lsnint32bit-test-rbdncentos7.rawn$ rbd -p test2 info int32bit-test-rbdnrbd image int32bit-test-rbd:n size 1024 MB in 256 objectsn order 22 (4096 kB objects)n block_name_prefix: rbd_data.9beee82ae8944an format: 2n features: layeringn flags:n

以上我們通過create子命令創建了一個name為int32bit-test-rbd,大小為1G的 image,其中-p的參數值test2就是pool名稱。通過ls命令可以查看所有的image列表,info命令查看image的詳細信息。

iSCSI創建lun設備後,Initiator端通過login把設備映射到本地。rbd image則是通過map操作映射到本地的,在client端安裝ceph client包並配置好證書後,只需要通過rbd map即可映射到本地中:

$ rbd -p test2 map int32bit-test-rbdn/dev/rbd0n

此時我們把創建的image映射到了/dev/rbd0中,作為本地的一個塊設備,現在可以對該設備像本地磁碟一樣使用。

2.5 如何把塊設備掛載到虛擬機

如何把一個塊設備提供給虛擬機使用,qemu-kvm只需要通過--drive參數指定即可。如果使用libvirt,以CLI virsh為例,可以通過attach-device子命令掛載設備給虛擬機使用,該命令包含兩個必要參數,一個是domain,即虛擬機id,另一個是xml文件,文件包含設備的地址信息。

$ virsh help attach-devicen NAMEn attach-device - attach device from an XML filenn SYNOPSISn attach-device <domain> <file> [--persistent] [--config] [--live] [--current]nn DESCRIPTIONn Attach device from an XML <file>.nn OPTIONSn [--domain] <string> domain name, id or uuidn [--file] <string> XML filen --persistent make live change persistentn --config affect next bootn --live affect running domainn --current affect current domainn

iSCSI設備需要先把lun設備映射到宿主機本地,然後當做本地設備掛載即可。一個簡單的demo xml為:

<disk type=block device=disk>n <driver name=qemu type=raw cache=none io=native/>n <source dev=/dev/disk/by-path/ip-10.0.0.2:3260-iscsi-iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063-lun-0/>n <target dev=vdb bus=virtio/>n <serial>2ed1b04c-b34f-437d-9aa3-3feeb683d063</serial>n <address type=pci domain=0x0000 bus=0x00 slot=0x06 function=0x0/>n</disk>n

可見source就是lun設備映射到本地的路徑。

值得一提的是,libvirt支持直接掛載rbd image(宿主機需要包含rbd內核模塊),通過rbd協議訪問image,而不需要先map到宿主機本地,一個demo xml文件為:

<disk type=network device=disk>n <driver name=qemu type=raw cache=writeback/>n <auth username=admin>n <secret type=ceph uuid=bdf77f5d-bf0b-1053-5f56-cd76b32520dc/>n </auth>n <source protocol=rbd name=nova-pool/962b8560-95c3-4d2d-a77d-e91c44536759_disk>n <host name=10.0.0.2 port=6789/>n <host name=10.0.0.3 port=6789/>n <host name=10.0.0.4 port=6789/>n </source>n <target dev=vda bus=virtio/>n <address type=pci domain=0x0000 bus=0x00 slot=0x05 function=0x0/>n</disk>n

所以我們Cinder如果使用LVM driver,則需要先把LV加到iSCSI target中,然後映射到計算節點的宿主機,而如果使用rbd driver,不需要映射到計算節點,直接掛載即可。

以上介紹了存儲的一些基礎知識,有了這些知識,再去理解OpenStack nova和cinder就非常簡單了,接下來我們開始進入我們的正式主題,分析OpenStack虛擬機掛載數據卷的流程。

3 OpenStack虛擬機掛載volume源碼分析

這裡我們先以Ciner使用LVM driver為例,iSCSI驅動使用lioadm,backend配置如下:

[lvm] niscsi_helper=lioadm nvolume_driver=cinder.volume.drivers.lvm.LVMVolumeDrivernvolume_backend_name=lvm volume_group = cinder-volumes n

OpenStack源碼閱讀方法可以參考如何閱讀OpenStack源碼,這裡不過多介紹。這裡需要說明的是,Nova中有一個資料庫表專門用戶存儲數據卷和虛擬機的映射關係的,這個表名為block_device_mapping,其欄位如下:

MariaDB [nova]> desc block_device_mapping;n+-----------------------+--------------+------+-----+---------+----------------+n| Field | Type | Null | Key | Default | Extra |n+-----------------------+--------------+------+-----+---------+----------------+n| created_at | datetime | YES | | NULL | |n| updated_at | datetime | YES | | NULL | |n| deleted_at | datetime | YES | | NULL | |n| id | int(11) | NO | PRI | NULL | auto_increment |n| device_name | varchar(255) | YES | | NULL | |n| delete_on_termination | tinyint(1) | YES | | NULL | |n| snapshot_id | varchar(36) | YES | MUL | NULL | |n| volume_id | varchar(36) | YES | MUL | NULL | |n| volume_size | int(11) | YES | | NULL | |n| no_device | tinyint(1) | YES | | NULL | |n| connection_info | mediumtext | YES | | NULL | |n| instance_uuid | varchar(36) | YES | MUL | NULL | |n| deleted | int(11) | YES | | NULL | |n| source_type | varchar(255) | YES | | NULL | |n| destination_type | varchar(255) | YES | | NULL | |n| guest_format | varchar(255) | YES | | NULL | |n| device_type | varchar(255) | YES | | NULL | |n| disk_bus | varchar(255) | YES | | NULL | |n| boot_index | int(11) | YES | | NULL | |n| image_id | varchar(36) | YES | | NULL | |n+-----------------------+--------------+------+-----+---------+----------------+n

Cinder中也有一個單獨的表volume_attachment用來記錄掛載情況:

MariaDB [cinder]> desc volume_attachment;n+---------------+--------------+------+-----+---------+-------+n| Field | Type | Null | Key | Default | Extra |n+---------------+--------------+------+-----+---------+-------+n| created_at | datetime | YES | | NULL | |n| updated_at | datetime | YES | | NULL | |n| deleted_at | datetime | YES | | NULL | |n| deleted | tinyint(1) | YES | | NULL | |n| id | varchar(36) | NO | PRI | NULL | |n| volume_id | varchar(36) | NO | MUL | NULL | |n| attached_host | varchar(255) | YES | | NULL | |n| instance_uuid | varchar(36) | YES | | NULL | |n| mountpoint | varchar(255) | YES | | NULL | |n| attach_time | datetime | YES | | NULL | |n| detach_time | datetime | YES | | NULL | |n| attach_mode | varchar(36) | YES | | NULL | |n| attach_status | varchar(255) | YES | | NULL | |n+---------------+--------------+------+-----+---------+-------+n13 rows in set (0.00 sec) n

接下來我們從nova-api開始一步步跟蹤其過程。

S1 nova-api

nova-api掛載volume入口為nova/api/openstack/compute/volumes.py,controller為VolumeAttachmentController,create就是虛擬機掛載volume的方法。

該方法首先檢查該volume是不是已經掛載到這個虛擬機了:

bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(n context, instance.uuid)nfor bdm in bdms:n if bdm.volume_id == volume_id:n _msg = _("Volume %(volume_id)s have been attaced to "n "instance %(server_id)s.") % {n volume_id: volume_id,n server_id: server_id}n raise exc.HTTPConflict(explanation=_msg)n

然後調用nova/compute/api.py的attach_volume方法,該方法的工作內容為:

(1) create_volume_bdm()

即在block_device_mapping表中創建對應的記錄,由於API節點無法拿到目標虛擬機掛載後的設備名,比如/dev/vdb,只有計算節點才知道自己虛擬機映射到哪個設備。因此bdm不是在API節點創建的,而是通過RPC請求到虛擬機所在的計算節點創建,請求方法為reserve_block_device_name,該方法首先調用libvirt分配一個設備名,比如/dev/vdb,然後創建對應的bdm實例。

(2) check_attach_and_reserve_volume()

這裡包含check_attach和reserve_volume兩個過程,check_attach就是檢查這個volume能不能掛載,比如status必須為avaliable,或者支持多掛載情況下狀態為in-use或者avaliable。該方法位置為nova/volume/cinder.py的check_attach方法。而reserve_volume是由Cinder完成的,nova-api會調用cinder API。該方法其實不做什麼工作,僅僅是把volume的status置為attaching。該方法流程:nova-api -> cinder-api -> reserver_volume,該方法位於cinder/volume/api.py:

@wrap_check_policyndef reserve_volume(self, context, volume):n expected = {multiattach: volume.multiattach,n status: ((available, in-use) if volume.multiattachn else available)}nn result = volume.conditional_update({status: attaching}, expected)nn if not result:n expected_status = utils.build_or_str(expected[status])n msg = _(Volume status must be %s to reserve.) % expected_statusn LOG.error(msg)n raise exception.InvalidVolume(reason=msg)nn LOG.info(_LI("Reserve volume completed successfully."),n resource=volume)n

(3) RPC計算節點的attach_volume()

此時nova-api會向目標計算節點發起RPC請求,由於rpcapi.py的attach_volume方法調用的是cast方法,因此該RPC是非同步調用。由此,nova-api的工作結束,剩下的工作由虛擬機所在的計算節點完成。

S2 nova-compute

nova-compute接收到RPC請求,callback函數入口為nova/compute/manager.py的attach_volume方法,該方法會根據之前創建的bdm實例參數轉化為driver_block_device,然後調用該類的attach方法,這就已經到了具體的硬體層,它會根據volume的類型實例化不同的具體類,這裡我們的類型是volume,因此對應為DriverVolumeBlockDevice,位於nova/virt/block_device.py。

我們看其attach方法,該方法是虛擬機掛載卷的最重要方法,也是實現的核心。該方法分好幾個階段,我們一個一個階段看。

(1) get_volume_connector()

該方法首先調用的是virt_driver.get_volume_connector(instance),其中virt_driver這裡就是libvirt,該方法位於nova/virt/libvirt/driver.py,其實就是調用os-brick的get_connector_properties:

def get_volume_connector(self, instance):n root_helper = utils.get_root_helper()n return connector.get_connector_properties(n root_helper, CONF.my_block_storage_ip,n CONF.libvirt.iscsi_use_multipath,n enforce_multipath=True,n host=CONF.host)n

os-brick是從Cinder項目分離出來的,專門用於管理各種存儲系統卷的庫,代碼倉庫為os-brick。其中get_connector_properties方法位於os_brick/initiator/connector.py:

def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath,n host=None):n iscsi = ISCSIConnector(root_helper=root_helper)n fc = linuxfc.LinuxFibreChannel(root_helper=root_helper)nn props = {}n props[ip] = my_ipn props[host] = host if host else socket.gethostname()n initiator = iscsi.get_initiator()n if initiator:n props[initiator] = initiatorn wwpns = fc.get_fc_wwpns()n if wwpns:n props[wwpns] = wwpnsn wwnns = fc.get_fc_wwnns()n if wwnns:n props[wwnns] = wwnnsn props[multipath] = (multipath andn _check_multipathd_running(root_helper,n enforce_multipath))n props[platform] = platform.machine()n props[os_type] = sys.platformn return propsn

該方法最重要的工作就是返回該計算節點的信息(如ip、操作系統類型等)以及initiator name(參考第2節內容)。

(2) volume_api.initialize_connection()

終於輪到Cinder真正干點活了!該方法會調用Cinder API的initialize_connection方法,該方法又會RPC請求給volume所在的cinder-volume服務節點。我們略去cinder-api,直接到cinder-volume。

S3 cinder-volume

代碼位置為cinder/volume/manager.py,該方法也是分階段的。

(1) driver.validate_connector()

該方法不同的driver不一樣,對於LVM + iSCSI來說,就是檢查有沒有initiator欄位,即nova-compute節點的initiator信息,代碼位於cinder/volume/targets/iscsi.py:

def validate_connector(self, connector):n # NOTE(jdg): api passes in connector which is initiator infon if initiator not in connector:n err_msg = (_LE(The volume driver requires the iSCSI initiator n name in the connector.))n LOG.error(err_msg)n raise exception.InvalidConnectorException(missing=initiator)n return Truen

注意以上代碼跳轉過程:drivers/lvm.py -> targets/lio.py -> targets/iscsi.py。即我們的lvm driver會調用target相應的方法,這裡我們用的是lio,因此調到lio.py,而lio又繼承自iscsi,因此跳到iscsi.py。下面分析將省去這些細節直接跳轉。

(2) driver.create_export()

該方法位於cinder/volume/targets/iscsi.py:

def create_export(self, context, volume, volume_path):n # iscsi_name: iqn.2010-10.org.openstack:volume-00000001n iscsi_name = "%s%s" % (self.configuration.iscsi_target_prefix,n volume[name])n iscsi_target, lun = self._get_target_and_lun(context, volume)n chap_auth = self._get_target_chap_auth(context, iscsi_name)n if not chap_auth:n chap_auth = (vutils.generate_username(),n vutils.generate_password())nn # Get portals ips and portn portals_config = self._get_portals_config()n tid = self.create_iscsi_target(iscsi_name,n iscsi_target,n lun,n volume_path,n chap_auth,n **portals_config)n data = {}n data[location] = self._iscsi_location(n self.configuration.iscsi_ip_address, tid, iscsi_name, lun,n self.configuration.iscsi_secondary_ip_addresses)n LOG.debug(Set provider_location to: %s, data[location])n data[auth] = self._iscsi_authentication(n CHAP, *chap_auth)n return datan

該方法最重要的操作是調用了create_iscsi_target方法,該方法其實就是調用了cinder-rtstool的create方法:

command_args = [cinder-rtstool,n create,n path,n name,n chap_auth_userid,n chap_auth_password,n self.iscsi_protocol == iser] + optional_argsnself._execute(*command_args, run_as_root=True)n

即create_export方法的主要工作就是調用cinder-rtstool工具創建target,並把設備添加到target中。

在cinder-volume節點可以通過targetcli查看所有export的target:

/iscsi> ls /iscsi/ 1no- iscsi .............................................................................................................. [Targets: 5]n o- iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063 ............................................... [TPGs: 1]n o- iqn.2010-10.org.openstack:volume-70347e2a-cdfc-4575-a891-3973ec264ec0 ............................................... [TPGs: 1]n o- iqn.2010-10.org.openstack:volume-980eaf85-9d63-4e1e-9e47-75f1a14ecc40 ............................................... [TPGs: 1]n o- iqn.2010-10.org.openstack:volume-db6aa94d-64cc-4996-805e-f768346d8082 ............................................... [TPGs: 1]n

(3) driver.initialize_connection()

這是最後一步。該方法位於cinder/volume/targets/lio.py:

def initialize_connection(self, volume, connector):n volume_iqn = volume[provider_location].split( )[1]n (auth_method, auth_user, auth_pass) = n volume[provider_auth].split( , 3)n # Add initiator iqns to target ACLn try:n self._execute(cinder-rtstool, add-initiator,n volume_iqn,n auth_user,n auth_pass,n connector[initiator],n run_as_root=True)n except putils.ProcessExecutionError:n LOG.exception(_LE("Failed to add initiator iqn %s to target"),n connector[initiator])n raise exception.ISCSITargetAttachFailed(n volume_id=volume[id])n self._persist_configuration(volume[id])n return super(LioAdm, self).initialize_connection(volume, connector) n

該方法的重要工作就是調用cinder-rtstool的add-initiator子命令,即把計算節點的initiator增加到剛剛創建的target acls中。

targetcli輸出結果如下:

/iscsi> ls /iscsi/iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063/tpg1/acls/no- acls .................................................................................................................. [ACLs: 1]n o- iqn.1994-05.com.redhat:e0db637c5ce ............................................................... [1-way auth, Mapped LUNs: 1]n o- mapped_lun0 ......................... [lun0 block/iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063 (rw)] n

因此Cinder的主要工作就是創建volume的iSCSI target以及acls。cinder-volume工作結束,我們返回到nova-compute。

S4 nova-compute

回到nova-compute的第(2)步,調用volume_api.initialize_connection()後,執行第(3)步。

(3) virt_driver.attach_volume()

此時到達libvirt層,代碼位於nova/virt/libvirt/driver.py,該方法分為如下幾個步驟。

1. _connect_volume()

該方法會調用nova/virt/libvirt/volume/iscsi.py的connect_volume()方法,該方法其實是直接調用os-brick的connect_volume()方法,該方法位於os_brick/initiator/connector.py中ISCSIConnector類中的connect_volume方法,該方法會調用前面介紹的iscsiadm命令的discovory以及login子命令,即把lun設備映射到本地設備。

可以使用iscsiadm查看已經connect(login)的所有volume:

$ iscsiadm -m sessionntcp: [203] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063 (non-flash)ntcp: [206] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-980eaf85-9d63-4e1e-9e47-75f1a14ecc40 (non-flash)ntcp: [207] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-70347e2a-cdfc-4575-a891-3973ec264ec0 (non-flash)ntcp: [208] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-db6aa94d-64cc-4996-805e-f768346d8082 (non-flash) n

使用lsblk查看映射路徑:

$ lsblk --scsinNAME HCTL TYPE VENDOR MODEL REV TRANn... # 略去部分輸出nsdh 216:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsinsdi 217:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsinsdj 218:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsinsdk 213:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsin

也可以在Linux的/dev/disk中by-path找到:

$ ls -l /dev/disk/by-path/ntotal 0nlrwxrwxrwx 1 root root 9 Sep 6 17:21 ip-10.0.0.4:3260-iscsi-iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063-lun-0 -> ../../sdknlrwxrwxrwx 1 root root 9 Sep 8 17:34 ip-10.0.0.4:3260-iscsi-iqn.2010-10.org.openstack:volume-70347e2a-cdfc-4575-a891-3973ec264ec0-lun-0 -> ../../sdinlrwxrwxrwx 1 root root 9 Sep 8 17:29 ip-10.0.0.4:3260-iscsi-iqn.2010-10.org.openstack:volume-980eaf85-9d63-4e1e-9e47-75f1a14ecc40-lun-0 -> ../../sdhnlrwxrwxrwx 1 root root 9 Sep 8 17:35 ip-10.0.0.4:3260-iscsi-iqn.2010-10.org.openstack:volume-db6aa94d-64cc-4996-805e-f768346d8082-lun-0 -> ../../sdjn

2. _get_volume_config()

獲取volume的信息,其實也就是我們生成xml需要的信息,最重要的就是拿到映射後的本地設備的路徑,如/dev/disk/by-path/ip-10.0.0.2:3260-iscsi-iqn.2010-10.org.openstack:volume-060fe764-c17b-45da-af6d-868c1f5e19df-lun-0,返回的conf最終會轉化成xml格式。該代碼位於nova/virt/libvirt/volume/iscsi.py:

def get_config(self, connection_info, disk_info):n """Returns xml for libvirt."""n conf = super(LibvirtISCSIVolumeDriver,n self).get_config(connection_info, disk_info)n conf.source_type = "block"n conf.source_path = connection_info[data][device_path]n conf.driver_io = "native"n return confn

3. guest.attach_device()

終於到了最後一步,該步驟其實就類似於調用virsh attach-device命令把設備掛載到虛擬機中,該代碼位於nova/virt/libvirt/guest.py:

def attach_device(self, conf, persistent=False, live=False):n """Attaches device to the guest.nn :param conf: A LibvirtConfigObject of the device to attachn :param persistent: A bool to indicate whether the change isn persistent or notn :param live: A bool to indicate whether it affect the guestn in running staten """n flags = persistent and libvirt.VIR_DOMAIN_AFFECT_CONFIG or 0n flags |= live and libvirt.VIR_DOMAIN_AFFECT_LIVE or 0n self._domain.attachDeviceFlags(conf.to_xml(), flags=flags)n

libvirt的工作完成,此時volume已經掛載到虛擬機中了。

(4) volume_api.attach()

回到nova/virt/block_device.py,最後調用了volume_api.attach()方法,該方法向Cinder發起API請求。此時cinder-api通過RPC請求到cinder-volume,代碼位於cinder/volume/manager.py,該方法沒有做什麼工作,其實就是更新資料庫,把volume狀態改為in-use,並創建對應的attach記錄。

到此,OpenStack整個掛載流程終於結束了,我們是從Nova的視角分析,如果從Cinder的視角分析,其實Cinder的工作並不多,總結有如下三點:

  • reserve_volume: 把volume的狀態改為attaching,阻止其它節點執行掛載操作。
  • initialize_connection: 創建target、lun、acls等。
  • attach_volume: 把volume狀態改為in-use,掛載成功。

4 OpenStack虛擬機掛載rbd分析

前面我們分析了LVM + lio的volume掛載流程,如果掛載rbd會有什麼不同呢。這裡我們不再詳細介紹其細節過程,直接從cinder-volume的initialize_connection入手。我們前面已經分析cinder-volume的initialize_connection步驟:

  • driver.validate_connector()
  • driver.create_export()
  • driver.initialize_connection()

這些步驟對應ceph rbd就特別簡單。因為rbd不需要像iSCSI那樣創建target、創建portal,因此rbd driver的create_export()方法為空:

def create_export(self, context, volume, connector):n """Exports the volume."""n pass n

initialize_connection()方法也很簡單,直接返回rbd image信息,如pool、image name、mon地址以及ceph配置信息等。

def initialize_connection(self, volume, connector):n hosts, ports = self._get_mon_addrs()n data = {n driver_volume_type: rbd,n data: {n name: %s/%s % (self.configuration.rbd_pool,n volume.name),n hosts: hosts,n ports: ports,n auth_enabled: (self.configuration.rbd_user is not None),n auth_username: self.configuration.rbd_user,n secret_type: ceph,n secret_uuid: self.configuration.rbd_secret_uuid,n volume_id: volume.id,n rbd_ceph_conf: self.configuration.rbd_ceph_conf,n }n }n LOG.debug(connection data: %s, data)n

而前面介紹過了,rbd不需要映射虛擬設備到宿主機,因此connect_volume方法也是為空。

剩下的工作其實就是nova-compute節點libvirt調用get_config()方法獲取ceph的mon地址、rbd image信息、認證信息等,並轉為成xml格式,最後調用guest.attach_device()即完成了volume的掛載。

因此,相對iSCSI,rbd掛載過程簡單得多。

4 總結

總結下整個過程,仍以LVM+LIO為例,從創建volume到掛載volume的流程如下:

  1. 創建一個volume,相當於在cinder-volume節點指定的LVM volume group(vg)中創建一個LVM volume卷(lv)。
  2. 掛載volume由nova發起,nova-api會檢查volume狀態,然後通知cinder,cinder把volume狀態置為attaching。
  3. 剩餘大多數工作由nova-compute完成,它先拿到自己所在節點的iscsi name。
  4. nova-compute向cinder請求,cinder會創建對應的target,並把nova-compute節點加到acls中。
  5. nova-compute節點通過iscsiadm命令把volume映射到本地,這個過程稱為connect volume。
  6. nova-compute節點生成掛載的xml配置文件。
  7. nova-compute調用libvirt的attach-device介面把volume掛載到虛擬機。

掛載過程總結為以下流圖:

需要注意的是,以上分析都是基於老版本的attach API,社區從Ocata版本開始引入和開發新的volume attach API,整個流程可能需要重新設計,具體可參考add new attch apis,這個新的API設計將更好的實現多掛載(multi-attach)以及更好地解決cinder和nova狀態不一致問題。

參考文獻

  1. how to configure an iscsi target and initiator in linux.
  2. block device mapping.
  3. create centralized secure storage using iscsi target in linux.
  4. linux iscsi.
  5. Targetcli.
  6. volume attach code flow in cinder.
  7. cinder new attach apis
  8. add new attach apis

附:OpenStack attach volume flow

以上的流程圖可能看不太清楚,可以直接在Draw sequence diagrams online in seconds查看原始圖,以下是flow源碼:

title OpenStack attach volume flownnparticipant clientnparticipant nova-apinparticipant cindernparticipant nova-computenparticipant libvirtnnclient -> nova-api: volume_attachnactivate clientnactivate nova-apinnote over nova-api: check if volume has been attachednnova-api->nova-compute: reserve_block_device_namenactivate nova-computennnova-compute->libvirt: get device name for instancenactivate libvirtnlibvirt->nova-compute: return /dev/vdbndeactivate libvirtnnnote over nova-compute: create bdmnnova-compute->nova-api: return new bdmndeactivate nova-computennote over nova-api: check attachnnova-api->cinder: reserve_volumenactivate cindernnote over cinder: set volume status to attachingncinder->nova-api: donendeactivate cindernnnova-api->nova-compute: attach_volumendeactivate nova-apindeactivate clientnactivate nova-computennote over nova-compute: convert bdm to block device drivernnote over nova-compute: get_volume_connectornnnova-compute->cinder: initialize_connectionnactivate cindernnote over cinder: require driver initializednnote over cinder: validate connectornnote over cinder: create exportnnote over cinder: do driver initialize connectionncinder->nova-compute: return connection infondeactivate cindernnnova-compute->libvirt: attach_volumenactivate libvirtnnote over libvirt: connect volumennote over libvirt: get volume conf and convert to xmlnnote over libvirt: attach devicenlibvirt->nova-compute: donendeactivate libvirtnnnova-compute->cinder:attach_volumenactivate cindernnote over cinder: set volume status to in-usennote over cinder: create attachmentncinder->nova-compute: return attachmentndeactivate cindernnnote over nova-compute: ENDndeactivate nova-computen

推薦閱讀:

準備學習 OpenStack,眾位大神有哪些教材推薦和經驗傳授?
解讀Mirantis最新的Neutron性能測試
為何現在流行OpenStack和Docker結合?
最近在學習 OpenStack,已經了解了其作用、架構。想進一步學習研究OpenStack各組件,對於源代碼的閱讀和學習,想得到大家的建議?

TAG:OpenStack | 源码阅读 | 云计算 |