背景

快照、克隆、回滚作为传统企业级特性,在分布式存储中同样也是必需的功能。

原理

本质上快照、回滚、克隆的原理还是基于MVCC(multiply version concurrent version), 对于需要做快照、回滚的卷,赋予卷一个版本号cur_version。每次快照是基于卷的版本号做,这个版本号可以称为snap_version。同时还会提升当前卷的版本号。

每次IO请求,带有一个卷的版本号request_version。如果request_version == cur_version,表示写的是当前卷;如果request_version < cur_version, 表示访问老数据。如果request_version < cur_version,程序出错。

过程

在分布式场景中,一个卷有多个切片,每个切片可能分布到不同复制组上去,每个复制组通常有一个leader 副本。对卷的克隆、快照操作实际就是对这个卷所有的切片统一做克隆、快照操作。主要步骤如下:

  • 等待卷在飞的IO请求完成,同时阻止新的IO请求进入
  • 向主控节点获取逻辑卷所有的切片的复制组信息
    找到每个复制组的leader节点,并记录它和卷的切片的对应关系
  • 在上面找到的每个复制组里:
  1. 提升对应切片的版本号;
  2. 拉取或上传对应切片的数据;
  • 生产总的snapshot/rollback索引信息
    上面复制组所有切片完成相应操作之后,生产一个总的索引信息,用来索引对应snapshot/clone的所有切片。

在分布式场景中,为了避免单点故障导致某个切片snapshot/clone 任务失败而被遗漏,可以通过分布式一致性协议(比如在相应阶段添加raft 日志)来持久化相应的进度信息。

快照和用户IO的交互

发起快照前有在飞的IO请求

为了保证快照的语义,对卷开始做快照前,需要检查当前版本的卷是否还有在飞的IO请求没有返回,如果有就需要等快照前所有的IO请求完成,同时阻止后面的IO进入。

快照开始执行后有新的IO请求进入

为了防止新来的IO写入污染快照,一般可以这么做:

  • 提升逻辑卷版本号
    具体来说,就是让cur_version 自增1;
  • 根据逻辑卷的版本和IO请求的版本分情况处理

用户对逻辑卷的读写请求都带有版本号request_version,表示对哪个版本的数据进行访问。写入请求到来的时候,需要比较二者:

  1. 如果request_version == cur_version:
    表示是对当前卷的写入,此时要看之前版本的快照是否完成,如果:
    a. 还没有开始

执行COW: 先拷贝一份老数据到之前版本,完成后更新当前版本cur_version的数据;

b. 正在执行:
等待上面的COW 完成,完成后更新当前版本cur_version的数据;

c. 已经完成
直接更新当前版本cur_version的数据;

  1. 如果request_version < cur_version:
    此时表面是对老版本的数据进行读写,需要判断:

a. 如果老版本数据在当前节点上:
直接读取对应版本的数据返回,或者直接写;

b. 如果老版本的数据不在当前节点上:
对于读:从存储快照的后端读取数据返回,可以把读请求排队在数据拉取完成之后的回调过程中;

对于写: 可以直接写到本地节点,后面拉取的时候,不拉取这部分数据。