中国领先的IT技术网站
|
|
创建专栏

OpenStack虚拟机挂载数据卷过程分析

目前已经成为了最流行的一种开源云解决方案。其中提供计算服务组件Nova、网络服务组件Neutron以及块存储服务组件Cinder是OpenStack的最为核心的组件。这里我们重点关注Nova和Cinder组件,Neutron组件将在下一篇文章中详细介绍。

作者:付广平|2017-09-14 10:11

Tech Neo技术沙龙 | 11月25号,九州云/ZStack与您一起探讨云时代网络边界管理实践


1 关于OpenStackOpenStack

是一个IaaS开源项目,实现公有云和私有云的部署及管理,目前已经成为了最流行的一种开源云解决方案。其中提供计算服务组件Nova、网络服务组件Neutron以及块存储服务组件Cinder是OpenStack的最为核心的组件。这里我们重点关注Nova和Cinder组件,Neutron组件将在下一篇文章中详细介绍。

1.1 计算服务NovaNova

组件为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为:

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

使用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:

  1. cinder create --volume-type ssd --name int32bit-test-volume 20 

Cinder目前最典型的应用场景就是为Nova云主机提供云硬盘功能,用户可以把一个volume卷挂载到Nova的云主机中,当作云主机的一个虚拟块设备使用。

挂载volume是在Nova端完成的:

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

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服务:

  1. yum install targetcli -y 
  2. systemctl enable target 
  3. systemctl start target 

运行targetcli检查是否安装成功:

  1. int32bit $ targetcli 
  2. targetcli shell version 2.1.fb41 
  3. Copyright 2011-2013 by Datera, Inc and others. 
  4. For help on commands, type 'help'
  5.  
  6. /> ls 
  7. o- / .................................... [...] 
  8.   o- backstores ......................... [...] 
  9.   | o- block ............. [Storage Objects: 0] 
  10.   | o- fileio ............ [Storage Objects: 0] 
  11.   | o- pscsi ............. [Storage Objects: 0] 
  12.   | o- ramdisk ........... [Storage Objects: 0] 
  13.   o- iscsi ....................... [Targets: 0] 
  14.   o- loopback .................... [Targets: 0] 

如果正常的话会进入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目录:

  1. /> cd /backstores/fileio 
  2. /backstores/fileio> create test_fileio /tmp/test_fileio.raw 2G write_back=false 
  3. Created fileio test_fileio with size 2147483648 

我们创建了一个名为test_fileio的fileio类型backstore,文件路径为/tmp/test_fileio.raw,大小为2G,如果文件不存在会自动创建。

创建了backstore后,我们创建一个target,cd到/iscsi目录:

  1. /iscsi> create iqn.2017-09.me.int32bit:int32bit 
  2. Created target iqn.2017-09.me.int32bit:int32bit. 
  3. Created TPG 1. 
  4. Default portal not created, TPGs within a target cannot share ip:port. 
  5. /iscsi> 

以上我们创建了一个名为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也很简单:

  1. /iscsi> cd iqn.2017-09.me.int32bit:int32bit/tpg1/portals/ 
  2. /iscsi/iqn.20.../tpg1/portals> create 10.0.0.4 
  3. Using default IP port 3260 
  4. Created network portal 10.0.0.4:3260. 

以上10.0.0.4是server的ip,不指定端口的话就会使用默认的端口3260。

target创建完毕,此时我们可以把我们之前创建的backstore加到这个target中:

  1. /iscsi/iqn.20.../tpg1/portals> cd ../luns 
  2. /iscsi/iqn.20...bit/tpg1/luns> create /backstores/fileio/test_fileio 
  3. Created LUN 0. 

此时我们的target包含有一个lun设备了:

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

接下来我们配置client端,即iSCSI Initiator:

  1. yum install iscsi-initiator-utils -y 
  2. systemctl enable iscsid iscsi 
  3. systemctl start iscsid iscsi 

拿到本机的initiator name:

  1. int32bit $ cat /etc/iscsi/initiatorname.iscsi 
  2. InitiatorName=iqn.1994-05.com.redhat:e0db637c5ce 

client需要连接server target,还需要ACL认证,我们在server端增加client的访问权限,在server端运行:

  1. int32bit $ targetcli 
  2. targetcli shell version 2.1.fb41 
  3. Copyright 2011-2013 by Datera, Inc and others. 
  4. For help on commands, type 'help'
  5.  
  6. /> cd /iscsi/iqn.2017-09.me.int32bit:int32bit/tpg1/acls 
  7. /iscsi/iqn.20...bit/tpg1/acls> create iqn.1994-05.com.redhat:e0db637c5ce 
  8. Created Node ACL for iqn.1994-05.com.redhat:e0db637c5ce 
  9. Created mapped LUN 0. 

注意:以上我们没有设置账户和密码,client直接就能登录。

一切准备就绪,接下来让我们在client端连接我们的target吧。

首先我们使用iscsiadm命令自动发现本地可见的target列表:

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

发现target后,我们登录验证后才能使用:

  1. int32bit $ iscsiadm -m node -T iqn.2017-09.me.int32bit:int32bit -l 
  2. Logging in to [iface: default, target: iqn.2017-09.me.int32bit:int32bit, portal: 10.0.0.4,3260] (multiple) 
  3. Login to [iface: default, target: iqn.2017-09.me.int32bit:int32bit, portal: 10.0.0.4,3260] successful. 

我们可以查看所有已经登录的target:

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

此时target已经自动映射到本地块设备,我们可以使用lsblk查看:

  1. int32bit $ lsblk --scsi 
  2. NAME HCTL       TYPE VENDOR   MODEL             REV TRAN 
  3. sda  0:0:2:0    disk ATA      INTEL SSDSC2BX40 DL2B 
  4. sdb  0:0:3:0    disk ATA      INTEL SSDSC2BX40 DL2B 
  5. sdc  0:0:4:0    disk ATA      INTEL SSDSC2BX40 DL2B 
  6. sdd  0:0:5:0    disk ATA      INTEL SSDSC2BX40 DL2B 
  7. sde  0:0:6:0    disk ATA      INTEL SSDSC2BX40 DL2B 
  8. sdf  0:0:7:0    disk ATA      INTEL SSDSC2BX40 DL2B 
  9. sdg  0:2:0:0    disk DELL     PERC H330 Mini   4.26 
  10. sdh  183:0:0:0  disk LIO-ORG  IBLOCK           4.0  iscsi 
  11. sdi  208:0:0:0  disk LIO-ORG  IBLOCK           4.0  iscsi 
  12. sdj  209:0:0:0  disk LIO-ORG  IBLOCK           4.0  iscsi 
  13. sdk  213:0:0:0  disk LIO-ORG  IBLOCK           4.0  iscsi 
  14. sdm  215:0:0:0  disk LIO-ORG  test_fileio      4.0  iscsi 

可见映射本地设备为/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信息:

  1. $ cinder-rtstool --help 
  2. Usage: 
  3. cinder-rtstool create [device] [name] [userid] [password] [iser_enabled] <initiator_iqn,iqn2,iqn3,...> [-a<IP1,IP2,...>] [-pPORT] 
  4. cinder-rtstool add-initiator [target_iqn] [userid] [password] [initiator_iqn] 
  5. cinder-rtstool delete-initiator [target_iqn] [initiator_iqn] 
  6. cinder-rtstool get-targets 
  7. cinder-rtstool delete [iqn] 
  8. cinder-rtstool verify 
  9. cinder-rtstool save [path_to_file] 

该工具主要运行在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:

  1. $ rbd -p test2 create --size 1024 int32bit-test-rbd --new-format 
  2. $ rbd -p test2 ls 
  3. int32bit-test-rbd 
  4. centos7.raw 
  5. $ rbd -p test2 info int32bit-test-rbd 
  6. rbd image 'int32bit-test-rbd'
  7.         size 1024 MB in 256 objects 
  8.         order 22 (4096 kB objects) 
  9.         block_name_prefix: rbd_data.9beee82ae8944a 
  10.         format: 2 
  11.         features: layering 
  12.         flags: 

以上我们通过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即可映射到本地中:

  1. $ rbd -p test2 map int32bit-test-rbd 
  2. /dev/rbd0 

此时我们把创建的image映射到了/dev/rbd0中,作为本地的一个块设备,现在可以对该设备像本地磁盘一样使用。

2.5 如何把块设备挂载到虚拟机

如何把一个块设备提供给虚拟机使用,qemu-kvm只需要通过--drive参数指定即可。如果使用libvirt,以CLI virsh为例,可以通过attach-device子命令挂载设备给虚拟机使用,该命令包含两个必要参数,一个是domain,即虚拟机id,另一个是xml文件,文件包含设备的地址信息。

  1. $ virsh  help attach-device 
  2.   NAME 
  3.     attach-device - attach device from an XML file 
  4.  
  5.   SYNOPSIS 
  6.     attach-device <domain> <file> [--persistent] [--config] [--live] [--current] 
  7.  
  8.   DESCRIPTION 
  9.     Attach device from an XML <file>. 
  10.  
  11.   OPTIONS 
  12.     [--domain] <string>  domain name, id or uuid 
  13.     [--file] <string>  XML file 
  14.     --persistent     make live change persistent 
  15.     --config         affect next boot 
  16.     --live           affect running domain 
  17.     --current        affect current domain 

iSCSI设备需要先把lun设备映射到宿主机本地,然后当做本地设备挂载即可。一个简单的demo xml为:

  1. <disk type='block' device='disk'
  2.       <driver name='qemu' type='raw' cache='none' io='native'/> 
  3.       <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'/> 
  4.       <target dev='vdb' bus='virtio'/> 
  5.       <serial>2ed1b04c-b34f-437d-9aa3-3feeb683d063</serial> 
  6.       <address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/> 
  7. </disk> 

可见source就是lun设备映射到本地的路径。

值得一提的是,libvirt支持直接挂载rbd image(宿主机需要包含rbd内核模块),通过rbd协议访问image,而不需要先map到宿主机本地,一个demo xml文件为:

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

所以我们Cinder如果使用LVM driver,则需要先把LV加到iSCSI target中,然后映射到计算节点的宿主机,而如果使用rbd driver,不需要映射到计算节点,直接挂载即可。

以上介绍了存储的一些基础知识,有了这些知识,再去理解OpenStack nova和cinder就非常简单了,接下来我们开始进入我们的正式主题,分析OpenStack虚拟机挂载数据卷的流程。

3 OpenStack虚拟机挂载volume源码分析

这里我们先以Ciner使用LVM driver为例,iSCSI驱动使用lioadm,backend配置如下:

  1. [lvm] 
  2. iscsi_helper=lioadm 
  3. volume_driver=cinder.volume.drivers.lvm.LVMVolumeDriver 
  4. volume_backend_name=lvm 
  5. volume_group = cinder-volumes 

OpenStack源码阅读方法可以参考如何阅读OpenStack源码,这里不过多介绍。这里需要说明的是,Nova中有一个数据库表专门用户存储数据卷和虚拟机的映射关系的,这个表名为block_device_mapping,其字段如下:

  1. MariaDB [nova]> desc block_device_mapping; 
  2. +-----------------------+--------------+------+-----+---------+----------------+ 
  3. | Field                 | Type         | Null | Key | Default | Extra          | 
  4. +-----------------------+--------------+------+-----+---------+----------------+ 
  5. | created_at            | datetime     | YES  |     | NULL    |                | 
  6. | updated_at            | datetime     | YES  |     | NULL    |                | 
  7. | deleted_at            | datetime     | YES  |     | NULL    |                | 
  8. | id                    | int(11)      | NO   | PRI | NULL    | auto_increment | 
  9. | device_name           | varchar(255) | YES  |     | NULL    |                | 
  10. | delete_on_termination | tinyint(1)   | YES  |     | NULL    |                | 
  11. | snapshot_id           | varchar(36)  | YES  | MUL | NULL    |                | 
  12. | volume_id             | varchar(36)  | YES  | MUL | NULL    |                | 
  13. | volume_size           | int(11)      | YES  |     | NULL    |                | 
  14. | no_device             | tinyint(1)   | YES  |     | NULL    |                | 
  15. | connection_info       | mediumtext   | YES  |     | NULL    |                | 
  16. | instance_uuid         | varchar(36)  | YES  | MUL | NULL    |                | 
  17. | deleted               | int(11)      | YES  |     | NULL    |                | 
  18. | source_type           | varchar(255) | YES  |     | NULL    |                | 
  19. | destination_type      | varchar(255) | YES  |     | NULL    |                | 
  20. | guest_format          | varchar(255) | YES  |     | NULL    |                | 
  21. | device_type           | varchar(255) | YES  |     | NULL    |                | 
  22. | disk_bus              | varchar(255) | YES  |     | NULL    |                | 
  23. | boot_index            | int(11)      | YES  |     | NULL    |                | 
  24. | image_id              | varchar(36)  | YES  |     | NULL    |                | 
  25. +-----------------------+--------------+------+-----+---------+----------------+ 

Cinder中也有一个单独的表volume_attachment用来记录挂载情况:

  1. MariaDB [cinder]> desc volume_attachment; 
  2. +---------------+--------------+------+-----+---------+-------+ 
  3. | Field         | Type         | Null | Key | Default | Extra | 
  4. +---------------+--------------+------+-----+---------+-------+ 
  5. | created_at    | datetime     | YES  |     | NULL    |       | 
  6. | updated_at    | datetime     | YES  |     | NULL    |       | 
  7. | deleted_at    | datetime     | YES  |     | NULL    |       | 
  8. | deleted       | tinyint(1)   | YES  |     | NULL    |       | 
  9. | id            | varchar(36)  | NO   | PRI | NULL    |       | 
  10. | volume_id     | varchar(36)  | NO   | MUL | NULL    |       | 
  11. | attached_host | varchar(255) | YES  |     | NULL    |       | 
  12. | instance_uuid | varchar(36)  | YES  |     | NULL    |       | 
  13. | mountpoint    | varchar(255) | YES  |     | NULL    |       | 
  14. | attach_time   | datetime     | YES  |     | NULL    |       | 
  15. | detach_time   | datetime     | YES  |     | NULL    |       | 
  16. | attach_mode   | varchar(36)  | YES  |     | NULL    |       | 
  17. | attach_status | varchar(255) | YES  |     | NULL    |       | 
  18. +---------------+--------------+------+-----+---------+-------+ 
  19. 13 rows in set (0.00 sec) 

接下来我们从nova-api开始一步步跟踪其过程。

S1 nova-apinova-api

挂载volume入口为nova/api/openstack/compute/volumes.py,controller为VolumeAttachmentController,create就是虚拟机挂载volume的方法。

该方法首先检查该volume是不是已经挂载到这个虚拟机了:

  1. bdms = objects.BlockDeviceMappingList.get_by_instance_uuid( 
  2.                 context, instance.uuid) 
  3. for bdm in bdms: 
  4.     if bdm.volume_id == volume_id: 
  5.         _msg = _("Volume %(volume_id)s have been attaced to " 
  6.                  "instance %(server_id)s.") % { 
  7.                  'volume_id': volume_id, 
  8.                  'server_id': server_id} 
  9.         raise exc.HTTPConflict(explanation=_msg) 

然后调用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:

  1. @wrap_check_policy 
  2. def reserve_volume(self, context, volume): 
  3.     expected = {'multiattach': volume.multiattach, 
  4.                 'status': (('available''in-use') if volume.multiattach 
  5.                            else 'available')} 
  6.  
  7.     result = volume.conditional_update({'status''attaching'}, expected) 
  8.  
  9.     if not result: 
  10.         expected_status = utils.build_or_str(expected['status']) 
  11.         msg = _('Volume status must be %s to reserve.') % expected_status 
  12.         LOG.error(msg) 
  13.         raise exception.InvalidVolume(reason=msg) 
  14.  
  15.     LOG.info(_LI("Reserve volume completed successfully."), 
  16.              resource=volume) 

(3) RPC计算节点的attach_volume()

此时nova-api会向目标计算节点发起RPC请求,由于rpcapi.py的attach_volume方法调用的是cast方法,因此该RPC是异步调用。由此,nova-api的工作结束,剩下的工作由虚拟机所在的计算节点完成。

S2 nova-computenova-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:

  1. def get_volume_connector(self, instance): 
  2.    root_helper = utils.get_root_helper() 
  3.    return connector.get_connector_properties( 
  4.        root_helper, CONF.my_block_storage_ip, 
  5.        CONF.libvirt.iscsi_use_multipath, 
  6.        enforce_multipath=True
  7.        host=CONF.host) 

os-brick是从Cinder项目分离出来的,专门用于管理各种存储系统卷的库,代码仓库为os-brick。其中get_connector_properties方法位于os_brick/initiator/connector.py:

  1. def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath, 
  2.                              host=None): 
  3.     iscsi = ISCSIConnector(root_helper=root_helper) 
  4.     fc = linuxfc.LinuxFibreChannel(root_helper=root_helper) 
  5.  
  6.     props = {} 
  7.     props['ip'] = my_ip 
  8.     props['host'] = host if host else socket.gethostname() 
  9.     initiator = iscsi.get_initiator() 
  10.     if initiator: 
  11.         props['initiator'] = initiator 
  12.     wwpns = fc.get_fc_wwpns() 
  13.     if wwpns: 
  14.         props['wwpns'] = wwpns 
  15.     wwnns = fc.get_fc_wwnns() 
  16.     if wwnns: 
  17.         props['wwnns'] = wwnns 
  18.     props['multipath'] = (multipath and 
  19.                           _check_multipathd_running(root_helper, 
  20.                                                     enforce_multipath)) 
  21.     props['platform'] = platform.machine() 
  22.     props['os_type'] = sys.platform 
  23.     return props 

该方法最重要的工作就是返回该计算节点的信息(如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:

  1. def validate_connector(self, connector): 
  2.    # NOTE(jdg): api passes in connector which is initiator info 
  3.    if 'initiator' not in connector: 
  4.        err_msg = (_LE('The volume driver requires the iSCSI initiator ' 
  5.                       'name in the connector.')) 
  6.        LOG.error(err_msg) 
  7.        raise exception.InvalidConnectorException(missing='initiator'
  8.    return True 

注意以上代码跳转过程: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:

  1. def create_export(self, context, volume, volume_path): 
  2.     # 'iscsi_name''iqn.2010-10.org.openstack:volume-00000001' 
  3.     iscsi_name = "%s%s" % (self.configuration.iscsi_target_prefix, 
  4.                            volume['name']) 
  5.     iscsi_target, lun = self._get_target_and_lun(context, volume) 
  6.     chap_auth = self._get_target_chap_auth(context, iscsi_name) 
  7.     if not chap_auth: 
  8.         chap_auth = (vutils.generate_username(), 
  9.                      vutils.generate_password()) 
  10.  
  11.     # Get portals ips and port 
  12.     portals_config = self._get_portals_config() 
  13.     tid = self.create_iscsi_target(iscsi_name, 
  14.                                    iscsi_target, 
  15.                                    lun, 
  16.                                    volume_path, 
  17.                                    chap_auth, 
  18.                                    **portals_config) 
  19.     data = {} 
  20.     data['location'] = self._iscsi_location( 
  21.         self.configuration.iscsi_ip_address, tid, iscsi_name, lun, 
  22.         self.configuration.iscsi_secondary_ip_addresses) 
  23.     LOG.debug('Set provider_location to: %s', data['location']) 
  24.     data['auth'] = self._iscsi_authentication( 
  25.         'CHAP', *chap_auth) 
  26.     return data 

该方法最重要的操作是调用了create_iscsi_target方法,该方法其实就是调用了cinder-rtstool的create方法:

  1. command_args = ['cinder-rtstool'
  2.             'create'
  3.             path, 
  4.             name
  5.             chap_auth_userid, 
  6.             chap_auth_password, 
  7.             self.iscsi_protocol == 'iser'] + optional_args 
  8. self._execute(*command_args, run_as_root=True

即create_export方法的主要工作就是调用cinder-rtstool工具创建target,并把设备添加到target中。

在cinder-volume节点可以通过targetcli查看所有export的target:

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

(3) driver.initialize_connection()

这是最后一步。该方法位于cinder/volume/targets/lio.py:

  1. def initialize_connection(self, volume, connector): 
  2.     volume_iqn = volume['provider_location'].split(' ')[1] 
  3.     (auth_method, auth_user, auth_pass) = \ 
  4.         volume['provider_auth'].split(' ', 3) 
  5.     # Add initiator iqns to target ACL 
  6.     try: 
  7.         self._execute('cinder-rtstool''add-initiator'
  8.                       volume_iqn, 
  9.                       auth_user, 
  10.                       auth_pass, 
  11.                       connector['initiator'], 
  12.                       run_as_root=True
  13.     except putils.ProcessExecutionError: 
  14.         LOG.exception(_LE("Failed to add initiator iqn %s to target"), 
  15.                       connector['initiator']) 
  16.         raise exception.ISCSITargetAttachFailed( 
  17.             volume_id=volume['id']) 
  18.     self._persist_configuration(volume['id']) 
  19.     return super(LioAdm, self).initialize_connection(volume, connector) 

该方法的重要工作就是调用cinder-rtstool的add-initiator子命令,即把计算节点的initiator增加到刚刚创建的target acls中。

targetcli输出结果如下:

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

因此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:

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

使用lsblk查看映射路径:

  1. $ lsblk --scsi 
  2. NAME HCTL       TYPE VENDOR   MODEL             REV TRAN 
  3. ... # 略去部分输出 
  4. sdh  216:0:0:0  disk LIO-ORG  IBLOCK           4.0  iscsi 
  5. sdi  217:0:0:0  disk LIO-ORG  IBLOCK           4.0  iscsi 
  6. sdj  218:0:0:0  disk LIO-ORG  IBLOCK           4.0  iscsi 
  7. sdk  213:0:0:0  disk LIO-ORG  IBLOCK           4.0  iscsi 

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

  1. $ ls -l /dev/disk/by-path/ 
  2. total 0 
  3. lrwxrwxrwx 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 -> ../../sdk 
  4. lrwxrwxrwx 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 -> ../../sdi 
  5. lrwxrwxrwx 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 -> ../../sdh 
  6. lrwxrwxrwx 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 -> ../../sdj 

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:

  1. def get_config(self, connection_info, disk_info): 
  2.     """Returns xml for libvirt.""" 
  3.     conf = super(LibvirtISCSIVolumeDriver, 
  4.                  self).get_config(connection_info, disk_info) 
  5.     conf.source_type = "block" 
  6.     conf.source_path = connection_info['data']['device_path'
  7.     conf.driver_io = "native" 
  8.     return conf 

3. guest.attach_device()终于到了最后一步,该步骤其实就类似于调用virsh attach-device命令把设备挂载到虚拟机中,该代码位于nova/virt/libvirt/guest.py:

  1. def attach_device(self, conf, persistent=False, live=False): 
  2.    """Attaches device to the guest. 
  3.  
  4.    :param conf: A LibvirtConfigObject of the device to attach 
  5.    :param persistent: A bool to indicate whether the change is 
  6.                       persistent or not 
  7.    :param live: A bool to indicate whether it affect the guest 
  8.                 in running state 
  9.    ""
  10.    flags = persistent and libvirt.VIR_DOMAIN_AFFECT_CONFIG or 0 
  11.    flags |= live and libvirt.VIR_DOMAIN_AFFECT_LIVE or 0 
  12.    self._domain.attachDeviceFlags(conf.to_xml(), flags=flags) 

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()方法为空:

  1. def create_export(self, context, volume, connector): 
  2.     """Exports the volume.""" 
  3.     pass 

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

  1. def initialize_connection(self, volume, connector): 
  2.     hosts, ports = self._get_mon_addrs() 
  3.     data = { 
  4.         'driver_volume_type''rbd'
  5.         'data': { 
  6.             'name''%s/%s' % (self.configuration.rbd_pool, 
  7.                                volume.name), 
  8.             'hosts': hosts, 
  9.             'ports': ports, 
  10.             'auth_enabled': (self.configuration.rbd_user is not None), 
  11.             'auth_username': self.configuration.rbd_user, 
  12.             'secret_type''ceph'
  13.             'secret_uuid': self.configuration.rbd_secret_uuid, 
  14.             'volume_id': volume.id, 
  15.             'rbd_ceph_conf': self.configuration.rbd_ceph_conf, 
  16.         } 
  17.     } 
  18.     LOG.debug('connection data: %s', data) 

而前面介绍过了,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状态不一致问题。

【本文是51CTO专栏作者“付广平”的原创文章,如需转载请通过51CTO获得联系】

戳这里,看该作者更多好文

【编辑推荐】

  1. 学习Java虚拟机没用? 听听当事人是怎么说的!
  2. Openstack虚拟机防止误删除操作的几种实现方法
  3. 深入研究Openstack Nova组件对象模型以及数据库访问机制
  4. CNCF 会重蹈 OpenStack 的覆辙吗?
  5. 来人啊给我炸了那个Java虚拟机
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

热门职位+更多

× CTO训练营(深圳站)