使用Ceph的块存储功能,为虚拟机提供块设备(卷)时,我们时不时需要对块设备(卷)打快照(Snapshot)。这里将简单介绍一下Ceph中快照的基本概念,然后介绍Ceph实现快照机制所涉及的数据结构,以及Ceph快照的的工作原理及读写流程的实现。
存储网络行业协会SNIA(StorageNetworking Industry Association)对快照(Snapshot)的定义是:关于指定数据集合的一个完全可用拷贝,该拷贝包括相应数据在某个时间点(拷贝开始的时间点)的映像。快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。
其实Ceph提供两种级别的快照:一种是针对Pool级别的快照,可以给整个Pool的数据做某一时刻的只读镜像;另一种是针对块设备(RBD)进行的快照。这两种快照是互斥的,不能同时共存。这里暂时只讨论针对块设备的快照。
无论Pool级别还是块设备级别的快照,Ceph实现的基本原理是相同的,基于对象的COW(copy-on-write)机制。Ceph可以实现秒级的快照操作。
除了快照,Ceph还提供对块设备的克隆(Clone)操作,克隆和快照的区别在于克隆是针对某一时刻全部数据的可写镜像。克隆的实现依赖于快照机制,克隆是在块设备的一个快照的基础上实现了可写功能。快照和克隆的示意图如下。
一个镜像的克隆生成过程如下所示:首先对镜像创建快照,然后protect这个快照,然后针对这个protected的快照进行克隆操作。
克隆的镜像有一个指向parent快照的引用,包括pool ID(这意味着可以在不同的pool之间进行克隆操作),image ID和snapshot ID。
快照和克隆的几个典型应用场景:
虽然可以创建无限层级的快照和克隆,但是当层级过多时,对克隆image的读写时需要不断递归到parent image上读取对应的快照对象,就会影响克隆镜像的性能。为了解决这个问题,Ceph提供了flatten操作,一次性将parent的数据拷贝给克隆image,这样就不需要向parent image查找对象数据了,从而提高性能。
这里需要注意的一点是,对象的clone操作指的是快照对应的克隆操作,是RADOS在OSD服务端实现的对象拷贝。RBD的Clone操作是RBD的客户端实现的RBD层面的克隆。是两个不同的概念。
虽然可以在不同的Pool直接创建克隆,但是一个RBD image的数据对象和快照对象都在同一个Pool中,每个image的对象和其快照的对象都存储在相同的OSD的相同PG中。快照的对象拷贝都是在OSD本地进行的。
实现快照的核心数据结构如下:
在文件src/common/snap_types.h中定义了snap相关的数据结构
struct SnapContext {
snapid_t seq; // 'time' stamp 即最新的快照序列号
vector<snapid_t> snaps; // existent snaps, in descending order
}
SnapContext数据结构用来在客户端(RBD端)保存snap相关的信息,这个结构持久化存储在RBD的元数据中。
librados Client中关于快照的信息定义在src/librados/IoCtxImpl.h中
struct librados::IoCtxImpl {
std::atomic<uint64_t> ref_cnt = { 0 };
RadosClient *client;
int64_t poolid;
snapid_t snap_seq; // 快照的id(snap id)
::SnapContext snapc; // snap的信息
uint64_t assert_ver;
version_t last_objver;
uint32_t notify_timeout;
object_locator_t oloc;
Mutex aio_write_list_lock;
ceph_tid_t aio_write_seq;
Cond aio_write_cond;
xlist<AioCompletionImpl*> aio_write_list;
map<ceph_tid_t, std::list<AioCompletionImpl*> > aio_write_waiters;
Objecter *objecter;
}
在IoCtxImpl中定义的snap_seq一般叫做快照的id(snap id)。当打开一个image时,如果打开的是一个卷的快照,则snap_seq为该快照的id,否则snap_seq的值为CEPH_NOSNAP,则表明操作的是卷本身。
在 src/osd/osd_types.h 中定义了SnapSet数据结构,用来保存Server端(OSD端)与快照相关的信息
/*
* attached to object head. describes most recent snap context, and
* set of existing clones.
*/
struct SnapSet {
snapid_t seq; // 最新的快照序列号
bool head_exists; // head对象是否存在
vector<snapid_t> snaps; // descending 所有的快照序列号,降序排列
vector<snapid_t> clones; // ascending 所有的clone对象序列号,升序排列
map<snapid_t, interval_set<uint64_t> > clone_overlap;
// overlap w/ next newest 和上次clone对象之间overlap的部分
map<snapid_t, uint64_t> clone_size; //clone对象的size
map<snapid_t, vector<snapid_t>> clone_snaps; // descending
}
SnapSet表示的信息持久化的保存在head对象的xattr扩展属性中:(此处待验证)
Ceph中对RBD创建一个快照的基本流程如下:
RBD的元数据保存有image所有的快照名字和对应的id,创建快照操作并不会触发OSD端的数据操作,所以是秒级。
image做了快照进行数据的写入,会触发copy-on-write(COW)机制。下面详解COW的工作机制。
写操作消息中携有SnapContext信息,包含了客户端元数据中保存的关于快照的信息,最新快照的序列号seq,以及该对象所有快照序列号的列表。在服务端,即OSD中,对象的Snap的有关信息存储在SnapSet中。写操作的处理流程按如下规则进行。
如果写操作携带的SnapContext的seq值小于Server端Snapset的seq值,则直接返回错误。
可能由于多个客户端的情景下,其他客户端创建了快照,本客户端没有获取到最新的快照序列号。
Ceph有一套Watcher回调机制来实现快照序列号的更新。一个客户端对image做了快照,Server收到最新的快照序列号,会通知相应的连接客户端更新快照序列号。如果客户端没有及时更新,当写时,Server端会返回错误,此时客户端主动更新image快照的最新信息,重新发起写操作。
如果head对象不存在,则创建该对象并写入数据,用SnapContext来更新SnapSet的信息。
如果写请求携带的SnapContext的seq值等于SnapSet的seq值,则对head对象做正常写入。
如果写请求携带的snap_seq值大于Server端的SnapSet中的snap_seq值:
读取快照数据时,输入image和快照的name,通过访问客户端中RBD的元数据,获取快照对应的id,也就是sanp_seq值。
Server端,获取head的SnapSet数据。根据snaps和clones来计算所对应的正确的快照对象。
将head对象回滚到某个快照对象。操作如下:
快照删除时,直接删除RBD元数据中保存的snap相关的信息,然后给Monitor发送删除信息;Monitor给相应OSD发送删除的快照序列号;然后OSD控制删除本地相对应的快照的对象。该快照是否被其他快照所共享。
Ceph快照的删除是延迟删除,并非立即删除。
to be continued
[1] http://docs.ceph.com/docs/hammer/rbd/rbd-snapshot/