dm-writeboost读写

题外话

近期抽空了解了一下ceph客户端缓存的问题,以期获取断电容灾的解决方案。ceph使用rbd的方式如下(参考ceph的jewel新支持的rbd-nbd):

再结合ceph用户邮件相关讨论,如Local ssd cache for ceph on each compute nodeCeph and EnhanceIO cache,可知可以从三个维度了解:
1、krbd
2、librbd
3、缓存插件

krbd:
krbd的内核代码阅读入口为以4.14.12版本为例,ceph相关代码逻辑在include/linux/ceph,Sage Weil在内核中扩展的rbd模块是以驱动模式实现的,具体的I/O数据由内核负责,rbd模块获取读写请求之后再与ceph集群通信。krbd模块重点在于与集群通信功能,因此,想要在krbd维度上考虑缓存的问题,相当于要修改内核关于bio的实现,目前来说是不可能的。

librbd:
librbd+nbd的模式似乎可以替代krbd,同时librbd也是虚拟机在使用的,因此从librbd上考虑缓存实现方式比较实际。这里还有一篇推荐文章Ceph 性能优化 之 带掉电保护的Rbd Cache。这篇文章里的主要逻辑是用读写文件的方式来取代内存缓存,我对这篇文章的疑虑在以下地方:
1)、没有讲明断电重启后的数据恢复逻辑,因此也没有讲明应当保存数据结构中具体的哪些字段。
2)、没有结合ssd的特性来读写。
3)、文章通过默认设定I/O大小和周期性统计应用I/O平均大小来避免性能问题,这种做法是否可以规避掉。

缓存插件:
基于目前的了解,缓存插件应该是通过使用ssd缓存数据来加快块设备落盘的,也就是说,使用缓存插件的目的是“加速”而不是“断电保护”。其次,是否所有缓存插件都具备数据一致性检查,也是需要读取相关源码才能真切了解到。

推荐的块设备文章深入浅出Flashcache(一)深入浅出Flashcache(二)深入浅出Flashcache(三)blktrace 深度了解linux系统的IO运作IO模式调查利器blkiomon介绍Linux下Fio和Blktrace模拟块设备的访问模式

基于上述背景,我首先阅读了rbd-nbd的相关代码rbd-nbd.cc,找到librbd的读写入口,了解了ceph客户端缓存数据的代码逻辑。接着,我阅读了缓存插件dm-writeboost的相关代码dm-writeboost,以期了解以下问题:
1)、如何读写ssd
2)、如何控制IO
3)、如何将随机写转换为顺序写

dm-writeboost

dm-writeboost采用驱动开发模式,通过定义IO处理规则来过滤IO请求。dm-writeboost使用了Device Mapper的相关知识,这有一篇推荐文章Linux 内核中的 Device Mapper 机制

知识点

1、dm-writeboost.h中定义的Overall-Superblock-Segment即为落盘ssd的最终数据,segment_header、metablock等等是在内存中组织的逻辑数据,这些逻辑数据的索引信息将填充在segment_header_device中,而具体缓存在rambuffer中的数据则将写入到Segment的data中,最后落盘。其中,Segment中的segment_header_device (512B) + metablock_device * nr_caches_inseg占据4K大小,一个Segment为512K。
2、Segment数据代表了ssd的数据,下刷到ssd是以Segment为单位的,Segment会记录ssd的扇区位置。Segment中包含多个metablock,metablock记录了hdd的扇区位置。Segment的metablock是无序的、随机的。
3、读缓存单元数据缓存了hdd的数据,使用了红黑树组织,根据bio指向的hdd扇区位置寻位。读缓存单元存在的意义是将只存在hdd而未存在ssd的数据缓存到ssd,以便加速后续读写。
4、所有Segment的所有metablock都关联到哈希表中,哈希表的大小等于nr_caches等于metablock总数,哈希表存在的意义是组织存储在ssd上的数据,并使用了hdd的扇区数进行索引,根据hdd扇区数查找哈希表上的metablock,存在则代表数据存在于ssd上(或即将写入),由metablock又可以定位到对应的Segment。根据bio来寻位哈希表的规则是:bio扇区对应第几个4K/哈希表大小的余数,找到对应的head,并从head的list中根据扇区位置找到对应的metablock。
5、I/O大小为4K范围内,通过dm接口设定。
6、如何将随机写转变成顺序写?
在dm-writeboost里,I/O大小为4K,随机写单元为一个metablock。dm-writeboost采取的策略是将随机写包含在大块里,然后对大块进行顺序写。具体的实现是:dm-writeboost在内存中按顺序维护Segment数组,然后从其中获取下一个空闲Segment,将metablock包含在其中,当Segment满了以后,下刷到ssd中,然后获取下一个空闲的Segment。因此,需要一定的机制来检验Segment是否下刷成功以及有无I/O请求情况,dm-writeboost采用了计数加减来完成这个目标。
7、由内存写入ssd时,称为flush,写入时受到缓存区rambuf_pool控制,共8个rambuf,每一个对应缓存一个Segment的数据,因此分配新的rambuf时,需要先等待[当前id - 8]之前的rabbuf下刷到ssd。由ssd写入hdd时,称为writeback,写入时受到Segment数组控制,共nr_segments个。writeback批处理上限默认为32个Segment,受nr_max_batched_writeback参数控制,因此分配新Segment时,需要先等待[当前id - nr_segments]之前的段都writeback完;实际的writeback实现是将每个段的metablock作为一个io处理,若metablock数据不满4K,还将分成扇区处理。writeback写入磁盘的数据将使用红黑树进行组织。
8、断电重启由ssd恢复内存组织数据时最多只需要恢复nr_segments段,由最老的段开始恢复,在nr_segments个段范围内,最老的段为已记录max段的下一个(若该段没有数据,则表明之前数据未写满nr_segments个)。恢复过程中一旦出现检验码验证失败,直接丢弃后面数据。恢复过程中,若出现metablock已被hash表索引,则将数据写入到hdd(若新数据不满4k或旧数据不满4k,且旧数据为脏数据未下刷hdd,则需要合并,这种情况下prepare_overwrite的实现是先wait_for_flushing等待旧数据所在段被flush;容易得知,该段断电前一定是被成功下刷过此时我们才能成功读取,因此wait_for_flushing不会出现问题。况且此时还没启动flush进程呢)。
9、系统启动的后台进程有:

1
2
3
4
5
writeback_daemon:用于将segment_header_array的数据下刷到hdd
flush_daemon:用于将rambuf_pool的数据下刷到ssd
writeback_modulator:用于监控io情况,设置allow_writeback
sb_record_updater:用于更新超级块,记录last_writeback_segment_id
data_synchronizer:用于定期唤醒flush进程,并等待当前段flush完成(可能是为了防止长时间未有io的情况)

10、为了防止数据破坏,使用了两种锁metux和spin-lock。
其中,metux用于:唤醒flush进程和分配新段(flush_current_buffer)、读缓存单元写入到内存以下刷到ssd的过程(inject_read_cache)、重新设置读缓存单元(reinit_read_cache_cells)、写入数据(do_process_write)、读取数据时查找cache和读缓存单元(process_read)。spin-lock用于:读取和设置metablock的dirtiness信息。

读请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
根据扇区位置查找cache(即对应ssd的哈希表数据,但索引项是hdd的扇区数)
判断是否保留读缓存单元(若原本无读缓存单元,则新增且状态为保留)

无缓存cache
保留读缓存单元
读取hdd到bio中
状态改为PBD_WILL_CACHE
将bio数据复制到读缓存单元中
读缓存工作排队
超过阈值则取消连续读缓存单元
将读缓存单元注入为写请求,将所有读缓存单元的数据复制到当前缓存段中,下刷到ssd
不保留读缓存单元(已有读缓存单元/bio请求数据不满4k)
请求重映射到hdd(修改bio指向的设备和扇区)

有缓存cache(数据在SSD中或当前内存缓存段中)
对应数据在内存中
根据bio指示的位置和扇区数读取hdd数据到buf中,将buf复制到bio中
若cache数据指明为脏数据(未下刷到hdd),在内存中缓存段上获取对应数据,复写到bio中
对应数据不在内存中(在ssd中)
等待found_seg下刷到ssd,保证后续ssd数据读取正确
若cache metablock块数据不满4k(此时已下刷到ssd)
读取hdd到buf中,并复制到bio中
若cache数据指明为脏数据(未下刷到hdd),按扇区读取ssd脏数据,复写到bio中
若cache metablock块数据满4k(此时已下刷到ssd)
状态改为PBD_READ_SEG
将读请求重新映射到ssd

其中,判断是否保留读缓存单元的逻辑是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bio请求不满4k,返回不保留
根据bio扇区在红黑树中查找读缓存单元,若找到,返回不保留
读缓存单元游标减一,从读缓存单元数组中获取一个新单元
设置新单元指向的扇区
新单元加入到红黑树中
bio状态改为PBD_WILL_CACHE
根据需要取消新单元
若当前读取扇区为上次读取扇区last_sector的下一扇区,seqcount++
否则,seqcount=1
顺序读次数seqcount大于阈值
当前已经超过阈值
取消新单元
当前未超过阈值
设置当前已超过阈值
将当前读缓存单元游标之后的seqcount个单元取消
设置当前读取扇区last_sector

写请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
初始化写I/O数据结构,将bio数据复制到写I/O
根据扇区位置查找cache(即对应ssd的哈希表数据,但索引项是hdd的扇区数)

有缓存cache(数据在SSD中或当前内存缓存段中)
对应数据在内存中
写入位置指向找到的metablock,跳到do_write
对应数据不在内存中
若bio所带数据不满4k或cache数据不满4k,且cache为脏数据
读ssd数据,合并到写I/O中
若cache数据不脏或bio所带数据满4k
忽略ssd数据
将找到的metablock标记为干净
无缓存cache
取消读缓存单元
若当前缓存段已满
下刷到ssd,获取新缓存段
从当前缓存段中获取空metablock作为写入位置

do_write:
将写I/O数据复制到rambuffer中(rambuffer缓存了当前应下刷到ssd的段实际数据,是缓存池中的一个单元)
将新的metablock标记为脏
将新的metablock加入到哈希表中(所以哈希表找到记录时,数据可能正在内存中,也可能已经下刷到ssd中)
(等待下次读写触发段刷新)
显示 Gitment 评论