众所周知,ceph在Jewel版本发布的时候,release了一个块存储的重要特性,那就是rbd mirroring。rbd mirroring 是一种两个集群之间,异步镜像的机制。通过一个rbd-mirror的服务,依赖于image的journaling特性,来实现集群间的crash-consistent的image复制。

>> rbd-mirror 介绍

每个cluster都有一个RBD MIRROR 模块,这个模块可以将远端的cluster里面image的变动同步到本地。其中 RBD MIRROR模块是一个新的服务,叫做rbd-mirror服务。

>> rbd mirroring 配置管理

rbd mirroring的配置是通过rbd mirror的子命令来完成的,如下所示。

现在可以看到local cluster的rbd pool 已经启动了mirror 模式。Info 结果里面有两个信息比较重要。

a) Mode:pool

在rbd mirror里面,现在支持两种模式的mirroring,一个是image模式,这种模式下,用户需要手动指定需要mirror的image。第二个是pool模式,这种模式会对pool里面的所有image自动启动mirror。以后rbd引入了namespace之后,可能还会出现新的mirror 模式,只做一个namespace的mirror。

b) Peers: 0ba36216-787c-4085-9272-bf1e50516129

这个UUID是remote的cluster作为local cluster的peer的UUID,它记录了远端的信息。比如上图,当我们启动rbd-mirror 服务的时候,rbd-mirror进程就会去找remote.conf,并将其作为remote的configuration文件,而且以client.admin的用户去获取信息并进行同步。

为了更彻底的了解这个过程,我们必须dive到code里面看得更清楚。

当我们做mirror pool enable的时候,实际上我们进行了大概六个步骤的操作。

l USER:执行命令“rbd mirror pool enable POOL pool”

我们在命令行执行shell命令,调用rbd 可执行文件。这个过程和其他所有linux命令执行过程没有区别。

l rbd command:调用api: rbd.mirror_mode_set();

当rbd 可执行文件开始执行,解析参数之后,会走到代码文件src/tools/rbd/action/MirrorPool.cc ,这个文件会处理所有的rbd mirror pool 子命令。然后会解析到下一个参数:enable。

于是会执行到函数:execute_enable_disable();这个函数会处理enable和disable操作。在这个函数中我们会调用librbd的api:

至此,整个流程的处理从rbd command转交到了librbd。

l 3. librbd:发起mirror_mode_set()请求:cls_client::mirror_mode_set();

在librbd中,我们首先接受到api请求的是RBD类。然后RBD类调用librbd内部相应的处理函数(src/librbd/librbd.cc):

接下来librbd的内部函数会检查当前的mirror mode,如果和我们要设置的mode相同,直接返回:

如果mode需要修改,然后进行mode的修改。中间有很多准备和检查操作,但是最重要的是下面这段代码。

通过调用cls_client模块的接口进行mode的设置。

l 4. cls_client:发送远程调用请求:OSDOp(CEPH_OSD_OP_CALL)

首先,需要介绍一下cls模块。cls/cls_client 实现了一种远程调用机制。client端通过cls_client模块将需要执行的操作进行封装,然后发送CEPH_OSD_OP_CALL 的OSDOp给osd,osd接收到命令后,执行需要调用的函数进行相应的处理。

在我们这个实际应用场景中,我们需要做的是mode_set操作。话不多说,直接来看cls_client是怎么实现的。

总共做了两件事:

第一:1378行,把输入放到in_bl中,也就是我们想设置的mode。这个in_bl会跟随OSDOp传到osd端,osd才能根据in_bl得到我们想要设置的mode值。

第二:1382行,调用ioctx的exec()。Ioctx 是一个连接到指定pool的io上下文。Ioctx的exec()函数接受五个参数:第一个参数指明需要作用的object名字,在这里是RBD_MIRRORING,实际上是一个在rbd_types.h中的宏定义:

所以我们这次的请求是作用在rbd_mirroring这个object当中的。这个object是mirror相关的所有元数据对象。第二个参数指明是哪个模块的请求,我们这里是rbd,第三个参数指明需要调用的函数名,这里是"mirror_mode_set"。第四个参数是输入的bufferlist,这里的in_bl包含了我们想要设置的mode值。第五个参数是out_bl,表示返回值的bufferlist。

exec()函数接受这五个参数之后,会做这么几件事情,新建一个::ObjectOperation rd;然后构造成一个Objecter::Op,通过objecter->submit_op() 提交这个op给osd。并且等待结果返回。

这个过程涉及到client和osd 交互的设计,也是很有意思,不过不是本文的重点,可以另外开一个主题进行详细分析。这里我们只需要知道,ioctx->exec() 将远程调用的请求发送给了osd,并等待执行结果,就行了。

l 5. cls:根据远程调用请求,执行操作:

mirror_mode_set() {

cls_cxx_map_set_val();

}

在osd端,osd接收到OSDOp,分发到PrimaryPG进行处理:

do_osd_ops()就做了一件事,打开我们指定的class(4993行),也就是rbd,然后找到我们指定的method(4996行),也就是mirror_mode_set,然后执行这个method(5011行)。

那osd端cls的mirror_mode_set()又做了些什么呢?在src/cls/rbd/cls_rbd.cc中,可以看到,函数mirror_mode_set();

操作很简单,调用cls_cxx_map_set_val()设置了rbd_mirroring这个object的一个key-value值,ceph里面这种值叫做omap值。

至此,rbd_mirroring的mode值已经修改结束了。我们可以看到通过rados命令确认一下修改结果:

可以看到 mirror_mode的结果是2,也就是我们设置的pool 模式。

以上就是命令rbd mirror pool enable rbd pool 的整个执行过程,这样我们就已经将rbd pool的mirror 模式改成了pool 模式。现在我们还需要通过命令:rbd --cluster local mirror pool peer add rbd client.admin@remote 给rbd pool 添加一个peer。

这个过程和上面类似,区别就在于enable是修改mirror_mode的值,add peer是添加一个omap值到rbd_mirroring的object里面。

至此,一个pool的mirroring 配置就结束了,接下来需要做的就是启动rbd-mirror服务,执行mirroring事务了。

>>rbd-mirror 服务

下面我们来看看rbd-mirror服务。

rbd-mirror里面几个非常重要的类以及他们之间的关系,包括Mirror,ClusterWather,Replayer,PoolWather,ImageReplayer。下面我们来看看这几个类是怎样相互配合完成rbd mirroring的。

Class Mirror: 这是rbd-mirror 服务的主类,一个rbd-mirror服务其实可以看做就是一个Mirror类的实例。Mirror里面有几个很重要的成员,m_local_cluster_watcher,这是一个ClusterWatcher的实例,用来watch本地cluster的mirror状态的,包括mirror mode和peer变化。

m_replayers,这是一个std::map<PoolPeer, std::unique_ptr<Replayer> > 类型的成员变量。用来记录所有的PoolPeer和Replayer的对应关系。

除了这两个重要的成员变量,Mirror类还有一个很重要的函数:update_replayers()。这个函数用来更新m_replayers变量,当m_local_cluster_watcher发现我们需要更新replayers,就会调用update_replayers()。

Class ClusterWatcher: 这个类顾名思义是用来watch 一个cluster的,他通过读取cluster里面每个pool的mirror相关元数据(如前文所述,这些元数据都存放在rbd_mirroring的object里面),得到我们需要做mirroring的所有pool以及这个pool的peer信息。

他有两个很重要的函数值得注意,refresh_pools()用来刷新最新的pool和peer的关系。get_pool_peers()得到最新的pool和peer的关系列表。

Class Replayer: 这个类是用来做replay操作的。一个replayer对应一个pool。实际上,在最新的ceph里面,我们已经将replayer重命名为PoolReplayer。Replayer有两个很重要的成员变量,m_pool_watcher,这是一个PoolWatcher的实例,用来watch这个pool里面的image情况。

m_image_replayers这是一个std::map<std::string, std::unique_ptr<ImageReplayer<> > >类型的实例。保存了这个pool里面所有image和image对应的ImageReplayer的关系。除了这两个成员变量,Replayer里面还有一个线程,m_replayer_thread。这个线程用来执行这个pool的watch和update操作。

Class PoolWatcher: 这个类是用来watch一个pool的。他通过定时做refresh()来更新m_images,m_images表示pool里面需要做mirror的image列表。这个类中有两个很重要的函数:refresh_images()和get_images()。

refresh_images()会定时去访问rbd_mirroring读取现在这个pool里面需要做mirror的image,然后更新到m_images。get_images()直接返回m_images,也就是最新的需要做mirror的image列表。

Class ImageReplayer: 这是最终执行replay操作的类,这个类很重要。他有一个m_remote_journaler,这个成员变量用来连接到remote cluster里面我们需要mirror的那个image的journal。

m_local_journal这是local image的journal。 m_local_replay,这是 m_local_journal的replayer,用来将remote的journal event在本地的journal做replay。我们会定时调用bootstrap()函数对这个image 进行replay操作。

那么到底ImageReplayer是怎样对一个image进行replay操作的呢?实际上有两种同步方式,event和image。event表示对这个image的一些操作,比如resize,会记录到journal里面,然后event通过librbd的journal/replay机制来实现同步。Image方式是通过 sync point来实现。

在每次同步的时候做一个snapshot,作为sync point,然后进行数据拷贝。当remote 的image没有完整的journal来做replay的时候,我们就必须使用sync point的方式来做image 同步。比如我们将一个使用了很久的image加入到mirroring里面。

关于这个设计,可以参考https://www.spinics.net/lists/ceph-devel/msg24169.html, 这是Josh Durgin在设计rbd-mirror的时候发出来的邮件。里面说明了为什么使用这种journaling的方式来做mirroring。

邮件里面列出了三种方式, snapshot, log-structed rbd, 和现在使用的journaling。最终选择了journaling,主要原因是journaling综合了snapshot和log-structed rbd两种方式的优点。对于event,使用log-struct的方式来记录并replay,对于没有event的image数据,使用snapshot做sync point的方式来同步。

 

那下面从两个方面来介绍一下ImageReplayer是怎样工作的。

第一, event replay。首先我们需要知道,现在有哪些event我们会放到journal里面?

这是我们现在所支持的所有event,以resize为例我们来看看rbd-mirror是怎样让remote的image resize 也发生到local的image上的。

当用户执行resize操作将image做resize的时候,remote cluster的librbd会在journal里面记录一个ResizeEvent。之后,local cluster的replayer会定期唤起ImageReplayer去做image 同步操作。

ImageReplayer会通过m_remote_journaler去获取remote cluster里面对应image的journal的entry。然后通过m_local_replay去decode并且调用local cluster的librbd来处理。

其实就是librbd里面的journal/replay机制。这样remote cluster里面的resize操作就同步到local cluster里面来了。

 

第二, image 同步。除了event的同步,当image没有完整的journal保存所有的event的时候,就需要使用sync point的方式来同步了。

当我们将一个image 通过rbd mirror image enable test 的方式加入到mirror中时,我们local cluster的PoolWatcher会发现这个image,并放到m_images里面。Replayer拿到images为这个image起动ImageReplayer,ImageReplayer会调用create_local_image()创建一个新的image来作为mirrored image。然后使用image_sync()来做image的同步。大概过程如下:

可以看到,这个过程将一个image的所有数据都进行了同步。

 

>> rbd-mirror TODO

至此,我们已经了解了rbd-mirror的基本实现原理。包括rbd-mirror的架构设计,rbd mirroring的配置,以及rbd-mirror数据同步。下面我们再来看看rbd mirroring的发展计划。

(1) update notify

根据上文我们可以看到,rbd-mirror是通过polling的模式来进行数据同步的,这种方式简单但不够高效且消耗太多资源。我们需要一种notification的机制来让rbd-mirror知道什么时候需要更新。

(2) rbd-mirror HA

我们可以看到rbd-mirror这个服务是独立的,没有提供高可用机制。那么这个容灾方案就不够安全,所以我们需要rbd-mirror的HA方案,包括Active/Passive 或者Active/Active。

以上所有的分析和代码都是基于ceph最新的stable版本Kraken的。实际上我提到的这两个问题,在upstream里面已经有了设计,期待下一个release的ceph里面能够解决这些问题。