背景
快照、克隆、回滚作为传统企业级特性,在分布式存储中同样也是必需的功能。
原理
本质上快照、回滚、克隆的原理还是基于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节点,并记录它和卷的切片的对应关系 - 在上面找到的每个复制组里:
- 提升对应切片的版本号;
- 拉取或上传对应切片的数据;
- 生产总的snapshot/rollback索引信息
上面复制组所有切片完成相应操作之后,生产一个总的索引信息,用来索引对应snapshot/clone的所有切片。
在分布式场景中,为了避免单点故障导致某个切片snapshot/clone 任务失败而被遗漏,可以通过分布式一致性协议(比如在相应阶段添加raft 日志)来持久化相应的进度信息。
快照和用户IO的交互
发起快照前有在飞的IO请求
为了保证快照的语义,对卷开始做快照前,需要检查当前版本的卷是否还有在飞的IO请求没有返回,如果有就需要等快照前所有的IO请求完成,同时阻止后面的IO进入。
快照开始执行后有新的IO请求进入
为了防止新来的IO写入污染快照,一般可以这么做:
- 提升逻辑卷版本号
具体来说,就是让cur_version 自增1; -
根据逻辑卷的版本和IO请求的版本分情况处理
用户对逻辑卷的读写请求都带有版本号request_version,表示对哪个版本的数据进行访问。写入请求到来的时候,需要比较二者:
- 如果request_version == cur_version:
表示是对当前卷的写入,此时要看之前版本的快照是否完成,如果:
a. 还没有开始
执行COW: 先拷贝一份老数据到之前版本,完成后更新当前版本cur_version的数据;
b. 正在执行:
等待上面的COW 完成,完成后更新当前版本cur_version的数据;
c. 已经完成
直接更新当前版本cur_version的数据;
- 如果request_version < cur_version:
此时表面是对老版本的数据进行读写,需要判断:
a. 如果老版本数据在当前节点上:
直接读取对应版本的数据返回,或者直接写;
b. 如果老版本的数据不在当前节点上:
对于读:从存储快照的后端读取数据返回,可以把读请求排队在数据拉取完成之后的回调过程中;
对于写: 可以直接写到本地节点,后面拉取的时候,不拉取这部分数据。