bcache内核模块IO读写、writeback流程梳理
主要作用Bcache (block 缓存)是一个Linux内核“块存储层”缓存,类似于flashcache使用ssd作为hdd的缓存方案,允许使用较为高速的固态硬盘作为读写缓存(writeback模式)或者读缓存(writethrough 或者 writearound模式)来为另一个 block 设备(通常是机械硬盘或硬盘阵列)加速。上述三种模式的原理图如下:默认情况下,bcache只超速缓存随机
主要作用
Bcache (block 缓存)是一个Linux内核“块存储层”缓存,类似于flashcache使用ssd作为hdd的缓存方案,允许使用较为高速的固态硬盘作为读写缓存(writeback模式)或者读缓存(writethrough 或者 writearound模式)来为另一个 block 设备(通常是机械硬盘或硬盘阵列)加速。
上述三种模式的原理图如下:
默认情况下,bcache只超速缓存随机读取和写入,这也是 SSD 的强项。尤其在writeback模式下,对写性能的提升会较为明显,本文后续的内核态分析也主要针对该模式的逻辑进行介绍。
使用方法
需要安装bcache-tools用户态工具来配合内核实现bcache功能的部署,具体方法可参见博客:Linux下SSD缓存加速之bcache使用 本文不再赘述。
bcache内核态核心流程梳理(3.10)
IO下发流程
bcache在整个内核IO栈的位置如下,主要是在generic_make_request函数中调用q->make_request_fn的时候调用bcache设备的IO处理方法,进入bcache层。
- 下发总流程
submit_bio
-> generic_make_request
-> q->make_request_fn #调用bio对应块设备(bcache)队列队列的make_request_fn方法
-> cached_dev_make_request #bcache模块下发request方法
-> part_stat_inc #更新了读写完成次数(用于计算bcache的iops)
-> part_stat_add #更新了读写完成的扇区数(计算bcache的带宽)
-> bio->bi_bdev = dc->bdev; #先设置bio后续下发的设备后端设备
-> cached_dev_get(dc) #判断后端设备是否配置了cache设备
-> s = search_alloc(bio, d); #生成一个search
-> request_write || request_read #进入具体读写流程
流程图
- 写IO流程
request_write
-> check_should_skip #检查是否要跳过该IO,如果要跳过,标记s->op.skip为true
-> if (bio->bi_rw & REQ_DISCARD) #判断是否是DISCARD请求或者之前已经被标记skip
-> goto skip (见后面的PS)
-> if (should_writeback(dc, s->orig_bio)) #判断是否要执行writeback模式操作
-> s->writeback=true
-> if (!s->writeback) #如果不执行writeback操作
-> closure_bio_submit #直接下发该io到后端设备
-> else #如果执行writeback操作
-> bch_writeback_add #唤醒回写线程
-> if (bio->bi_opf & REQ_PREFLUSH)
-> closure_bio_submit #若IO被为FLUSH,则下刷该到后端设备,使磁盘缓存的数据下刷下去
以上几个分支走完,都会来到如下流程:
-> bch_data_insert
-> bch_data_insert_start
#判断该请求是否被pass(比方说writearound模式),如果是,直接返回,不下发到cache设备。
-> if (op->bypass)
bch_data_invalidate
#如果请求没有被pass,则继续执行,下发到cache设备
-> bch_submit_bbio
-> __bch_submit_bbio
-> bio_set_dev(bio, PTR_CACHE(c, &b->key, 0)->bdev); #设置bio后续下发的设备为前端cache设备
-> closure_bio_submit #下发IO到前端cache设备
-> bch_data_insert_keys #将bio相关信息添加到b+tree中。
写流程图
- 读IO流程
request_read
-> check_should_skip #检查是否要跳过该IO,如果要跳过,标记s->op.skip为true
-> btree_read_async #在cache中进行搜索
-> bch_btree_search_recurse
-> bch_btree_iter_next_filter #主要的搜索函数
#如果cache miss,调用submit_partial_cache_miss,从后端磁盘读数据(包括预读数据)
-> submit_partial_cache_miss
-> cached_dev_cache_miss #cache_miss相关操作
-> bio_get(s->op.cache_bio) #增加s->op.cache_bio计数
-> closure_bio_submit #从后端设备读取数据
#如果是cache命中,走下面流程
-> submit_partial_cache_hit
-> __bch_submit_bbio
-> bio->bi_bdev = PTR_CACHE(c, &b->key, 0)->bdev; #设置bio后续下发的设备为前端cache设备
-> closure_bio_submit #下发IO到前端cache设备
-> request_read_done_bh
#如果是发生了cache miss 读了后端数据上来,则s->op.cache_bio存在计数,走这个流程
-> request_read_done
-> bio_complete #标记该IO完成
-> bch_data_insert #将信息插入到cache设备中并根性b+tree
#如果是cache命中,则走这个流程
-> cached_dev_read_complete
-> cached_dev_bio_complete
-> search_free
-> bio_complete #标记该IO完成
-> bio_put; closure_debug_destroy; #释放各类结构体数据
读流程图
- IO下发过程中的其他流程
bio_complete
-> generic_end_io_acct #io结束数据统计
-> bio_endio #调用块设备层IO结束方法
closure_bio_submit #bcache层提交IO方法,实际还是调用generic_make_request调具体的设备函数(所以generic_make_request是可以递归调用)
-> generic_make_request
-> struct request_queue *q = bio->bi_disk->queue; #找到该bio对应的bi_disk(上面bio_set_dev中指定)->queue
-> ret = q->make_request_fn(q, bio); #调用queue对应的方法,这里也就是直接调用块设备自己的处理方法了
check_should_skip
跳过io的情况:
1、没有cache设备attach
2、cache使用量是否达到了95%(CUTOFF_CACHE_ADD)
3、io是discard请求
4、cache_mode为NONE或者为writeround
5、扇区没对齐
6、历史顺序IO
7、拥塞控制(读写共用,即如果由于读IO超时造成了cache拥塞,则写IO也会受到影响。目前测试拥塞与cache盘自身性能、下发IO线程数量有关)
should_writeback:
该函数主要判断两个内容
1、cache_mode是否是writeback,如果不是则返回false
2、cache的使用量是否达到了75%(CUTOFF_WRITEBACK_SYNC),如果超过则返回false
IO回写(writeback)流程
**bcache的writeback原理:扫描btree中的脏数据并将其添加到固定的内核缓冲区中,然后在通过遍历该缓冲区的数据,将其对应的数据从cache设备中读出,再写入到后端主设备中。**下面是内核3.10的流程,**通过refill_dirty和read_dirty两个函数协同工作实现。**3.12内核后启用了bcache_writeback线程来进行,流程不同,但原理一致。
- 触发扫描btree脏数据的时机
1、cache设备attach的时候
2、cache设备detach的时候
3、新的写请求出现,并走到writeback处理流程时,通过bch_writeback_add -> bch_writeback_queue
bch_writeback_add
-> bch_writeback_queue
-> refill_dirty #触发脏数据填充操作
-> schedule_delayed_work(writeback_rate_update) #若配置writeback_percent,开始定时更新writeback_rate
- 脏数据填充buf流程:refill_dirty
refill_dirty
-> !dc->writeback_running #若writeback_running为0
-> closure_return(cl); #直接返回
-> !atomic_read(&dc->has_dirty) #若没有脏数据
-> SET_BDEV_STATE; closure_return(cl); #设置CLEAN标记后返回
-> bkey_cmp(&buf->last_scanned, &end) >= 0 #如果上次搜索已经到了btree末尾
-> searched_from_start = true; #设置从头开始搜索整个磁盘
-> bch_refill_keybuf #遍历btree来进行填充dc->writeback_keys->keys
-> btree_root
-> bch_btree_refill_keybuf
-> RB_INSERT #将key插入到dc->writeback_keys->keys buffer中
-> if (searched_from_start) #如果是搜索了整个磁盘
-> if (RB_EMPTY_ROOT(&buf->keys)) #如果buf为空
-> atomic_set(&dc->has_dirty, 0); #清除设备的dirty标记
-> closure_delay( dc->writeback_delay * HZ) #因为遍历了整个磁盘,睡眠writeback_delay秒(默认30)
-> read_dirty #触发脏数据回写函数
- buf数据回写后端盘流程:read_dirty
read_dirty
/***************************此部分是个循环:START*******************************/
-> bch_keybuf_next #从dc->writeback_keys->keys获取一个key
-> if (!w);break #如果没有数据,代表buf为空,跳出循环
-> schedule_timeout_uninterruptible (delay) #根据delay值进行睡眠,这里就是在实际调整下发速率
-> dirty_init #初始化这个key所代表的io
-> read_dirty_submit #数据回写核心流程
-> closure_bio_submit #先cache设备读取这个数据
-> write_dirty
-> closure_bio_submit #再将读到的数据下发到后端主设备
->writeback_delay #根据dc->writeback_rate计算delay的值
-> bch_next_delay
/***************************此部分是个循环:END*******************************/
-> refill_dirty #触发脏数据填充buf函数
refill_dirty和read_dirty的联动流程图:
- writeback_rate更新相关
从上述writeback流程可以发现,回写的速率实际是靠writeback_rate来控制的。
writeback_rate默认在cache盘有脏数据的情况下以一定的周期(writeback_rate_update_seconds,默认30,高版本为5,可手动调节)实时更新。计算过程会参考writeback_percent值(默认10,最大40)以及当前的dirty数据量,通过PD(比例微分)控制器进行平滑调节。
writeback_rate更新任务是通过一个循环的延迟任务实现的:
writeback_rate更新任务初始化:注册后端主设备的时候,就会初始化更新writeback_rate的延时任务
register_bcache-> register_bdev-> cached_dev_init
-> bch_cached_dev_writeback_init
-> INIT_DELAYED_WORK #初始化delayed work,work函数为update_writeback_rate
(&dc->writeback_rate_update,
update_writeback_rate);
-> schedule_delayed_work #延时writeback_rate_update_seconds后执行writeback_rate_update
(&dc->writeback_rate_update,
dc->writeback_rate_update_seconds * HZ)
Delayed work函数:
update_writeback_rate
-> if (&dc->has_dirty) && writeback_percent #如果设备标记为dirty 且 配置了writeback_percent
-> __update_writeback_rate #更新writeback_rate核心函数
#其中一方面通过一个PD控制器来实现rate的平滑调整(具体实现待研究)
#另一方面调用schedule_delayed_work启动下一个update_writeback_rate延时任务
更多推荐
所有评论(0)