快照底层设计实现
本文通过具体的设计思路来加深对快照的理解,如有错误,欢迎指出
基于CoFW的设计
工作流程
文件系统初始数据如下图
文件系统映射表保存着数据和保存地址的对应关系,当上层要更改A0时,会寻址到逻辑地址0进行接下来的操作
准备一个IO仓库用来存放变化的IO块
T0时刻,系统触发快照,此时创建一份针对T1时刻的快照S0的地址映射表F0
T1时刻,上层应用将B0数据更新为B1
系统查看F0,发现没有对应条目,执行CoFW
首先将逻辑地址2中的数据B0拷贝到IO仓库地址0的地方,更新F0,意思表示逻辑地址2的数据保存在IO仓库0位置上
然后上层将B1覆盖写入逻辑地址2,更新文件系统映射表
T2时刻,C0更新为C2,流程同上
此时系统再次触发一份快照S1,映射表为F1
T3时刻,B1更新为B3
系统发现F1没有针对逻辑地址2的对应条目,进行CoFW操作
F0表已经保存过当时时刻的数据了,所以无需更新
T4时刻,将A0更新为A4
系统发现F0,F1均没有对应表项,首先将A0拷贝到IO仓库,然后更新快照映射表
最后将A4写入,更新文件映射表
此时IO仓库容量已满,后续如果再有数据更新需要扩容
T5时刻,B3更新为B5
系统发现F0,F1均更新过,无需进行CoFW
将B5更新,更新文件映射表
任何时刻,读IO都能直接通过文件系统映射表来查询数据,无需查询快照映射表
客户端挂载快照
T6时刻,客户端挂载快照S0
T8时刻,客户端请求逻辑地址0的数据,F0表中查询到对应表项,从IO仓库3读出A0返回
T9时刻,客户端请求逻辑地址5的数据,F0表未查询到数据,从源表查询中找到逻辑地址5读出D0返回
PS:查询线程在查找映射表之前,必须先想CoFW线程查询当前时刻是否存在针对源物理卷的写IO,如果有,IO地址是否恰好为查询的地址,如果是,则查询线程要等待CoFW完成再去查询映射表,此时就可以查到最新数据了。
如果查询线程结果是从源物理卷读取内容,则在查询线程读取完成之前,CoFW不能针对这个地址操作,否则查询线程可能读取的是最新刚被写入的数据而不是对应以前快照时刻的数据了。
对于客户端来说,它挂载S0后文件系统看到的内容如下:
T10时刻,客户端更新A0为A10
因为客户端不能影响原系统,所以此时需要对新IO单独放在另一个地方,数据可以放入IO仓库或其它设计,增加RoFW表来记录写IO
首先将A10写入IO仓库,更新客户端文件系统映射表和RoFW表
T11时刻,客户端读取A10,首先查看RoFW表,发现存在表项,从IO仓库4中读出A10
T12时刻,客户端读取B0,RoFW表未查到表项,查询F0表项,从IO仓库0读出B0
T13时刻,客户端读取E0,RoFW和F0均未查到表项,从源表查询得到从逻辑地址7读出E0
上述过程同样考虑线程依赖关系
rollback
T14时刻,客户端要求将快照S0回滚覆盖源物理卷。
在rollback之前主机客户端一定要把buffer里的数据flush到源卷,不然源卷被恢复之后,缓存里的数据再写下去,数据就不一致了。所以一般在rollback之前,索性先umount掉,rollback完成再mount回来
这里可以前台或后台操作,前台操作即等回滚完毕之后才能进行IO操作,为了保证友好性,设计为后台操作,即直接返回成功。
因为主机认为成功了,可能就会继续有读写IO。
T15时刻,系统开始执行与挂载快照读写时相同的操作,但是在后台,系统会将原先CoFW的数据以及被RoFW的数据覆盖到源物理卷对应地址上。
在处理客户端写IO请求时也可以直接将覆盖源卷对应地址,无需RoFW,节约一轮IO操作,这时要记录每个在Rollback开始与完成之间发生的写IO地址以便Rollback时跳过这些地址。
同时,线程依赖关系必须考虑。
T15时刻,所有条目都被恢复之后,F0表和RoFW表被删掉,此刻之后,就相当于S0快照没被创建过,系统只剩下S1快照
两个注意点:
- 比如S0快照恢复时,要覆盖S1的基准块如C2,如果此时要保存S2快照,那么就需要先把C2拷贝到IO仓库然后更新F1表之后再恢复C0
即在做覆盖操作之前,必须参考恢复快照点之后所有快照点的映射表,一旦任何一份表中对应的地址没有映射条目,就要CoFW操作
然而,目前市面上的产品几乎都严格遵照历史规则,即,既然选择回到了S0,那么S0之后所发生的所有事件就不应该存在,所以S1也要被删除
- rollback没有完成之前,有新的快照建立,新快照的CoFW操作过程和后台rollback操作冲突,除非引入额外的CoFW操作才能保持数据一致性
删除
T14时刻, 客户端卸载S0快照,系统接收到请求后,删除RoFW映射表,其他不做更改。如果再次挂载,则产生新的干净的映射表
如果客户希望卸载虚拟卷之后系统仍然保持所做的更改,则需要通知系统创建一份针对S0的克隆卷,所谓的clone卷也不过只是永久保存RoFW映射表而已。
T15时刻,系统请求删除快照S0,此时扫描所有快照映射表,找出只有F0存在而其余映射表不存在的表项,将对应IO仓库对应位置置为空闲。
查询完毕发现IO仓库0和1位置是只给S0使用的,置为空闲,然后删除表F0。这个过程依然涉及线程依赖。
基于RoFW的设计
工作流程
文件系统初始数据如下图
文件系统映射表保存着数据和保存地址的对应关系,当上层要更改A0时,会寻址到逻辑地址0进行接下来的操作
准备一个IO仓库用来存放变化的IO块
T0时刻,触发快照S0,创建对应的映射表F0
T1时刻,写入A1
系统直接将A1写入IO仓库,更新F0,表示逻辑地址0的数据被重定向到IO仓库位置0处了
源卷自身永远都是S0时刻的影像,永久冻结,拒绝写入,除非所有快照都被删除
T2时刻,写入C2,流程同上
写完之后触发快照S1
T3时刻,写入A3
此时源卷,F0以及F0对应IO仓库的数据共同构成了S1快照的基准卷,这三者共同体也将会被永久冻结
IO仓库写入A3后,更新F1
T4时刻,写入A4
此时,系统检查最晚的映射表即F1是否有对应条目,如果有,则将A4在IO仓库覆盖A3
映射表不做任何更改
T5时刻,B5写入
系统检查F1中没有对应的条目,将B5写入IO仓库空闲位置,更新F1
T6时刻,系统要读取逻辑地址0和5的数据
系统先检查最后一张表即F1,发现了逻辑地址0对应的条目,从IO仓库2位置读取A4
F1没有找到逻辑地址5的条目,寻找上一张表,依旧没有,于是从源卷逻辑地址5读出D0
T7时刻,触发快照S2,创建F2,此时F0,F1均被冻结
写入A7,D7
F2中均没有对应条目,将数据写入IO仓库并更新F2
客户端挂载快照
T8时刻,客户端挂载S0快照
S0其实就是当前物理源卷,客户端发起的读请求直接对应到源卷,不牵扯任何映射运算
T9时刻,客户端挂载快照S1
客户端读取内容时先去查询F0,没有找到条目就会从源上读取
比如读取逻辑地址0,会从IO仓库中读取A1;读取逻辑地址2,会从源卷读取B0
这里要搞清楚的是本次快照影响实际是有源+上一次快照共同冻结而成,而最后一份快照映射表指针表示源最新数据状态,而不是这份快照时刻的状态
PS:RoFW设计模式不像CoFW模式存在线程依赖关系,对快照虚拟卷的读操作不会对源卷的写操作有任何的牵连和影响
T10时刻,系统受到针对S2快照的写操作,与CoFW模式相同,这里也是建立一个针对S2的RoFW数据映射表,后续读数据也是先查这个表
rollback
T11时刻,客户端要求将S0回滚
由于S0快照点就是当前源物理卷,所以不需要任何额外操作,直接就完成了
T11时刻,客户要求将S1回滚,这里不保留任何其它的快照
系统首先将F1之前的映射表做合并,如果在多个表有相同条目,则保留最晚的表条目
表合并完毕后,将IO仓库对应的数据覆盖到源卷,处理完毕后,IO仓库清空,rollback完成
这个过程中遇到写IO处理流程同CoFW
删除
T11时刻,删除快照S0
比较F0和后面的映射表,只在F0中有的条目,将数据拷贝回源卷,并将IO仓库对应位置置为空闲
也可以将只在F0中存在的表项移到F1中