写在前面

     刚刚过去的9月,人工智能、云计算和物联网界热闹非凡,接连迎来了世界物联网博览会、世界人工智能大会和阿里云栖大会。2018世界物联网博览会就在家门口举行,抓了空去现场看展览,外行人看看热闹,有感于科技的日新月异给生活带来的便利。

     话题扯远了,回到这篇文章,文章的标题包含“复盘”,顾名思义,是对以前的发生的现象或问题进行回顾,学而不思则罔,目的第一是从问题中总结经验,最大化发掘它的价值;第二是不断锻炼自己分析问题的能力;最后是希望强化在某项知识上的运用能力,毕竟以今天分析昨天,有思维就ok了,但是以今天设想未来,需要长期深厚的积累才行。


1 背景

    云硬盘的快照、克隆,属于块存储RBD基本功能,之前我并没有太多关注这两块,保证功能能用、好用就过去了。

    不过在最近遇到几个与克隆或快照相关联的问题,涉及到rbd clone、rbd flatten等,正好是之前未遇到过,因此找了时间对产生的问题复盘,也是希望借bug观察ceph对克隆和快照的处理流程。

    整篇文章分以下几小章节:

    1 、背景

    2 、bug回顾

    3、 利用rbd_max_clone_depth触发flatten

    4 、cinder和ceph层面对clone、flatten的实现


2 bug回顾

  2.1  bug1 通过快照创建云硬盘,删除父快照失败

      这个问题复现步骤很简单,如下流程图所示:

     

     遇到的问题就是在删除快照时发生失败,禁止该删除操作。

     由快照创建出的云硬盘,一般情况与快照是父子关系,在ceph下通过rbd info或rbd children可以查询到链的关系。 如下图红色框中所示,在parent一项中,可以看到volume-8e81a7b0-4fdc-49b0-a9ed-4124c8e61f7d是volume-117078c1-c724-44b5-a271-e0f708e9d6b3下的快照克隆得来:

      

                                                                                                                                              图1

     既然克隆盘与快照存在父子关系,要删除父快照的首要条件是斩断这种依赖关系,这个就需要通过rbd_flatten_volume_from_snapshot配置项来实现,见/etc/cinder/cinder.conf配置文件,如下图中:

     

     在我们的存储下,rbd_flatten_volume_from_snapshot=false,在false条件下volume和snapshot之间的关系是什么?

     在解决问题之前,先来了解下rbd_flatten_volume_from_snapshot在true、false下的不同作用,见下表:

     

     一目了然,在false条件下,volume和snapshot存在依赖,要想解决该bug,只要开启为true即可。

     不过先别急着改/etc/cinder/cinder.conf,在此之前我们通过flatten手动解除依赖关系,再重复bug的步骤看是否能够成功,这个小实验分5个步骤复现,如下图3中:

     


       (1)创建volume和快照   PASS

       (2)执行克隆    PASS

       (3)删除父快照   FAILED

                错误提示需要对快照先去除保护,unprotect后再执行删除快照,错误提示:cannot unprotect: at least 2 child(ren) [1a5e266b8b4567,1aa62d6b8b4567] in pool

       (4)执行flatten操作,解除依赖   PASS

       (5)再次删除父快照     PASS

     小实验OK,证明了解除克隆盘与snapshot的依赖后再删除快照成功。如果在控制节点的cinder配置文件中,开启了rbd_flatten_volume_from_snapshot = true,则 由快照创建出的云硬盘,会自动合并,清除依赖关系,这样一来这个云硬盘就变为扁平的没有层级的volume。最后记得重启cinder服务,使其生效!


  2.2  bug2 大容量的空盘创建快照,再通过该快照创建云硬盘,耗时过长

     这个bug提的比较优秀,功能本身属于正常流程,但之前遗漏了大容量云硬盘这个场景,比如1T、2T。

     问题复现步骤同bug1,只是少了删除快照的步骤,如下流程图所示:

   


     通常,我们在云平台上对云硬盘创建快照后,会同时创建快照卷,由于精简配置的属性,只需分配相对少量的存储空间即可,当再通过该快照clone出云硬盘,快照处于只读保护, 在cow的机制下,克隆操作会很迅速。下面引用一张ceph官方的图来解释:

       

   上图4中,parent是指源云硬盘的快照,而child是从快照克隆出来的云硬盘。

   这个bug 2与“2.1 bug 1”对比,相同点都是由rbd_flatten_volume_from_snapshot造成的bug,不同的地方在于true和false。

   在bug 2 的云环境下,rbd_flatten_volume_from_snapshot=true,在上文的bug1中曾说过解除volume和snapshot的依赖关系,取消这种依赖关系叫做flatten,这个flatten花费的时间和源云硬盘(volume)的大小成正比。

   回到bug本身,内部在做排查时,依照以下的顺序:

     (1)检查volume qos

        眼光先放在了volume qos上,在有数据条件下,qos速率大小(比如write=100MB/S)肯定是会影响到快照创建云硬盘的速度的。转念一想,云硬盘是空盘,并不存在任何object,因此克隆速度应该是很快的。

     (2)检查父云硬盘、快照、子云硬盘的实际容量

        我们环境中云硬盘是空盘,不存在任何数据,同样的由快照创建出的新盘也不会有任何数据。实际是否如此,通过下面的验证步骤来证实一下: 

  •  创建云硬盘和快照,并获取真实存储空间

         

                                                                                           图5

       图5中,新建1G的volume,并创建该盘的快照后,rados查询实际存在的object,找不到任何数据,这是正确的。

  • 由快照创建云硬盘,并获取真实存储空间

                                                                                                                 图6

    图6中,快照创建出新的云硬盘,叫volume-d7199f3d-ed96-446a-83c8-25083a752e23,可以看到在云硬盘创建过程中,新的云硬盘和快照时父子关系,创建成功后,新的云硬盘和快照时父子关系被解除。

   图7所示是获取新的云硬盘的实际数据对象,发现已经存在256个object(父云硬盘总容量为1GB,根据order 22 (4096 kB objects)来切分)

     图8中,随机抽查几个object,发现其实这些object的容量都是0,并不存在真实的数据。一般而言,从快照创建云硬盘,代码实现很简单,先克隆再flatten,Fill clone with parent data (make it independent),此时flatten会将所有块从父节点复制到child,但父云硬盘中没有数据,flatten操作是不应该产生object的。

     这个bug问题就在于flatten会对新建云盘的每一个对象进行一个写操作,从而创建无数个大小为0的对象,又在qos的限制下,所以耗时较长。


3 利用rbd_max_clone_depth触发flatten

      麦子迈在《解析Ceph: Librbd 的克隆问题》一文中提到 “Librbd 在卷的克隆时会形成子卷对父卷的依赖,在产生较长的克隆依赖链后会有严重的性能损耗”。这个理论其实和cow下多快照产生的性能衰减是一样的,对ceph的云硬盘做快照,每次做完快照后再对云硬盘进行写入时就会触发COW操作, 即1次读操作、2次写操作,volume→volume的克隆本质上就是将 volume 的某一个 Snapshot 的状态复制变成另一个volume。

      为解决在产生较长的克隆依赖链后会有严重的性能损耗问题,在OpenStack Cinder 的/etc/cinder/cinder.conf中提供一个参数,可以解除父子依赖关系,在超过自定义设置的阀值后选择强制 flatten。

     

    在图9中,通过 rbd_max_clone_depth来控制最大可克隆的层级。

    rbd_max_clone_depth = 5 这个参数控制卷克隆的最大层数,超过的话则使用 fallten。设为 0 的话,则禁止克隆。

    为了验证这个过程,下面我们做个实验,创建1个volume,命名为01,依次复制下,即由01复制成02,02复制为03,03复制为04,04复制为05,05复制为06,06复制为07,如下图流程图:

     

   实验预期结果,就是当从06复制到07时,满足rbd_max_clone_depth > 5,此时触发flatten操作。

图10

图11

   图10、图11是 复制云硬盘后的查询到克隆盘信息

图12

   图12中, 上面的log记录了复制07时,触发了flatten操作,对上级云硬盘06执行flatten操作,开始执行合并。

图13

   图13所示是Flatten成功后,可以看到云硬盘06 的parent一项消失,此时在页面上可以删除云硬盘06


4 cinder和ceph层面对clone、flatten的实现

     现在市面上很多讲ceph的书(大多数翻译自ceph中国社区之手),在RBD块存储章节都会对快照、克隆等操作花很多篇幅去描述,基本都是在rbd层通过命令一步步分解rbd clone过程来讲原理。

     对于类似我这样的刚接触ceph不久的人来说,知识点分散在各处,看了前面忘了后面,很难在脑子里建立完整的概念,当然主要原因还是自己太菜了,迷雾重重看不透!

                                                                                                                      

    

    言归正传,我只是想大概的了解下对云硬盘执行操作在底层是如何实现的,因此还是由上文中提到的小处(bug)来入手,自顶向下先设计一个思考流程,带着目标按照这个从上到下的顺序去理解,如下图所示:

                                                                                                                  

         注:以下涉及的代码均来自GitHub开源,如有雷同,纯属巧合!

4.1  从快照克隆卷的流程

    (1)openstack cinder

         自顶向下,先从cinder层入手,通过代码可以看到从快照克隆出volume的思路,从本质上讲,快照克隆出新的卷,也是volume create的性质,所以先来了解下volume create过程

         cinder:/cinder/volumes.py

     volumes.py中def create方法我省略了很多,主要就是通过req、body的参数来获取创建volume所需要的参数,根据不同参数来发送具体的创建volume请求,因为我是从快照来创建,snapshot id自然必不可少,在 volumes.py最后实际调用new_volume = self.volume_api.create()去实现。


cinder:/cinder/volume/api.py

     经过volume_api.create(),在/cinder/volume/api.py来处理前端发来的卷相关的所有请求,通过create_what{}表示volume的实现参数,然后分别就调用cinder.scheduler的scheduler_rpcapi,cinder.volume的volume_rpcapi建立创建volume的工作流:create_volume.get_flow

    注:关于create volume flow的流程及具体实现,见/cinder/volume/rpcapi.py:def create_volume(),/cinder/volume/flows/api/create_volume.py,本篇省略过程


cinder:/cinder/volume/manager.py

    对于api来讲,只是做到处理前端发来的卷相关的所有请求,具体实现交由manager下的去完成,rpcapi调用inder/volume/manager.py:def create_volume()去操作

     执行中发现crate voluem 有snapshot id,然后调用/cinder/volume/flows/manager/create_volume.py下的私有方法_create_volume_from_snapshot()

   最后根据配置文件指定的RBD后端请求/cinder/volume/drivers/rbd.py的create_volume_from_snapshot()


cinder:/cinder/volume/drivers/rbd.py

     众所周知,一般cinder使用RBD驱动来对接底层的后端存储(比如ceph、xsky),在openstack cinder层面最终交由create_volume_from_snapshot()实现,因为是通过快照来创建volume,还需要调用私有方法_clone(),满足条件的话,还要调用_flatten()和_resize()。


(2)librbd

     经历多方接力才结束在cinder层面的流程,这还不算完,真正要实现create volume from snapshot的创建,核心在调用ceph执行。

    ceph:/src/pybind/rbd/rbd.pyx


/ceph/blob/v10.2.3/src/librbd/librbd.cc

    在librbd中对外提供api在class RBD中,从librbd.cc函数中看到有多个clone()、clone2()、clone3()函数,区别在于根据传入的不同参数来调用对应的函数,但这些函数都不像是具体的功能实现,只是一些相关参数传值。

    再看看/ceph/blob/v10.2.3/src/librbd/internal.cc函数,同librbd.cc一样,对应的clone()也是3种,因为篇幅如下展示的是clone3()函数(实际命名并不如此,通过参数来区分得知是clone2):

    将librbd.cc、internal.cc两个函数联系起来看,librbd.cc只是定义了对外的各种函数接口,接口的具体实现,调用的还是internal.cc中定义的函数内容。

    总结一下,根据自己的理解将整个流程绘成图,如下图所示中,需要一提的是,我没有涉及到librados的实现过程,因为clone等volume的操作,librbd可以说就是rbd的完整实现,rados只是作为后端的存储


4.2  flatten的流程

    在前文“ 利用rbd_max_clone_depth触发flatten”小节中,我们描述了一个volume clone的过程,通过cinder.conf的一个参数,当满足rbd_max_clone_depth最大层数后,触发flatten操作,下面我们通过代码去看一看具体实现的流程。

    (1)openstack cinder

     对于上层云平台而言,从云硬盘1克隆出云硬盘2,或者从快照创建云硬盘,一般是能够触发flatten操作的主要场景,其实两者实现原理基本一致。

     因此,和之前的由snapshot来实现创建新的云硬盘一样,首要都是从create()开始,只是参数不同,克隆盘在create过程先要获取parent volume id

    之后也是一样经历api→manager→driver的过程,这里省掉重复的过程,直接看cinder调用rbd驱动对克隆云硬盘的实现代码,如下图中/cinder/volume/drivers/rbd.py:

     调用了私有方法_get_clone_depth()来判断depth,调用_flatten()来实现flatten操作,当然flatten过程经历一系列过程,在parent volume上创建snapshot,对snapshot加保护、再执行clone,然后flatten,这个过程一样可以通过rbd 命令来完成。


(2)librbd

      创建RADOSClient,连接到ceph rados,这里也是先调用clone()去执行,再触发flatten()操作,和我预期不同,flatten的过程比想象中还要复杂,才疏学浅,对整个过程的了解还需要更多的时间,只能先用根据自己的理解画出一张流程图表示一下: