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後端存儲,至少包含以下幾個優點:
- 所有的計算節點共享存儲,遷移時不需要拷貝塊設備,即使計算節點掛了,也能立即在另一個計算節點啟動虛擬機(evacuate)。
- 利用COW特性,創建虛擬機時,只需要基於鏡像clone即可,不需要下載整個鏡像,而clone操作基本是0開銷。
- 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的流程如下:
- 創建一個volume,相當於在cinder-volume節點指定的LVM volume group(vg)中創建一個LVM volume卷(lv)。
- 掛載volume由nova發起,nova-api會檢查volume狀態,然後通知cinder,cinder把volume狀態置為attaching。
- 剩餘大多數工作由nova-compute完成,它先拿到自己所在節點的iscsi name。
- nova-compute向cinder請求,cinder會創建對應的target,並把nova-compute節點加到acls中。
- nova-compute節點通過iscsiadm命令把volume映射到本地,這個過程稱為connect volume。
- nova-compute節點生成掛載的xml配置文件。
- nova-compute調用libvirt的attach-device介面把volume掛載到虛擬機。
掛載過程總結為以下流圖:
需要注意的是,以上分析都是基於老版本的attach API,社區從Ocata版本開始引入和開發新的volume attach API,整個流程可能需要重新設計,具體可參考add new attch apis,這個新的API設計將更好的實現多掛載(multi-attach)以及更好地解決cinder和nova狀態不一致問題。
參考文獻
- how to configure an iscsi target and initiator in linux.
- block device mapping.
- create centralized secure storage using iscsi target in linux.
- linux iscsi.
- Targetcli.
- volume attach code flow in cinder.
- cinder new attach apis
- 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各組件,對於源代碼的閱讀和學習,想得到大家的建議?