治肾虚不含糖,专注内核性能优化二十年。 https://github.com/KnightKu
分类: LINUX
2017-09-25 18:52:43
5. Allocation 与Bucket
Bcache将cache disk的空间线性划分为若干个bucket, 每个bucket对应的磁盘地址按bucket号线性增加,每个bucket的大小一致。
bch_bucket_alloc
a. 先查看当前是否有空闲的bucket, 可用fifo_pop(&ca->free[RESERVE_NONE], r) ||
fifo_pop(&ca->free[reserve], r)); 则goto out;
b. 若无free可用,则当前线程进入等待,知道有可用的bucket
c. wake_up_process(ca->alloc_thread);
d. 更新要分配bucket的信息
SET_GC_SECTORS_USED(b,ca->sb.bucket_size);
if(reserve <= RESERVE_PRIO) { //若该bucket分配给元数据使用
SET_GC_MARK(b, GC_MARK_METADATA); //元数据的bucket不能随意回收
SET_GC_MOVE(b, 0); //该bucket目前不需要gc 处理
b->prio = BTREE_PRIO;
}else { //
SET_GC_MARK(b, GC_MARK_RECLAIMABLE);
SET_GC_MOVE(b, 0);
b->prio = INITIAL_PRIO;
}
bch_allocator_thread:
a. 如果后备free_inc 不为空,fifo_pop(&ca->free_inc, bucket)一个后备bucket, 调用allocator_wait(ca, bch_allocator_push(ca, bucket));加入ca->free中,这样保证分配函数有可用的bucket. 然后唤醒由于wait alloc而等待的线程。 若ca->free以满,则alloc_thread阻塞
b. 如果free_inc已经空了,则需要invalidate当前正在使用的bucket
allocator_wait(ca, ca->set->gc_mark_valid&&
!ca->invalidate_needs_gc); //等待gc完成,或未执行
invalidate_buckets(ca);
c. 更新存储在磁盘中的bucket的 gen信息bch_prio_write
invalidate_buckets: 有三种invalidate正在使用的bucket的方式,fifo, lru和randorm, 这里分析fifo 与lru方式。
static voidinvalidate_buckets_fifo(struct cache *ca) {
while(!fifo_full(&ca->free_inc)) {
。。。。。。
b = ca->buckets +ca->fifo_last_bucket++; //最先分配的bucket
if(bch_can_invalidate_bucket(ca, b))
bch_invalidate_one_bucket(ca,b);
if (++checked >=ca->sb.nbuckets) { //若由于很多bucket不能回收, 这时需要唤醒gc
ca->invalidate_needs_gc= 1;
wake_up_gc(ca->set);
return;
}
}
static voidbch_invalidate_one_bucket(struct cache *ca, struct bucket *b) {
__bch_invalidate_one_bucket(ca, b);
fifo_push(&ca->free_inc, b -ca->buckets);
}
__bch_invaliate_one_bucket{
bch_inc_gen(ca, b); b->prio =INITIAL_PRIO;atomic_inc(&b->pin);
}
bch_inc_gen {
uint8_t ret = ++b->gen; //这里会更新bucket的gen
ca->set->need_gc =max(ca->set->need_gc, bucket_gc_gen(b));
}
//能invalidate的条件是为被gc mark,或gc设为可回收,且未被invalidate,且代数未到最大(96U)
boolbch_can_invalidate_bucket(struct cache *ca, struct bucket *b){
return (!GC_MARK(b) || GC_MARK(b) ==GC_MARK_RECLAIMABLE) &&
!atomic_read(&b->pin)&& can_inc_bucket_gen(b);
}
下面分析lru方式invalidate_buckets_lru
a. 遍历cache disk的每个bucket {
若不能回收则continue;
加bucket加入到heap中,比较函数为bucket_max_cmp
}
b. 按prio从小到大排序heap(bucket_min_cmps)
c. 一次从堆中取出bucket,做bch_invalidate_one_bucket; 直到ca->free_inc满
d. 若ca->free_inc未满,则wake_up_gc
比较函数如下:
#definebucket_max_cmp(l, r) (bucket_prio(l)< bucket_prio(r))
#definebucket_min_cmp(l, r) (bucket_prio(l) >bucket_prio(r))
通过前面的分析我们发现,当访问命中或刚分配bucket时prior会重置为较大值,那么何时减少bucket的prio呢?
check_should_bypass 中会随机调用bch_rescale_priorities来减少bucket的prio
bch_rescale_priorities:
a. 用atomic控制并发,当已有task执行rescale时,直接返回
b. 减少除元数据和未分配的bucket外的prio, 最小减到0
6. GC管理
上一节的bucket分配器在inc_free未满的情况下会唤醒gc thread, 本节将分析gc的工作原理。bch_gc_thread==> bch_btree_gc其流程如下:
a. btree_gc_start 设置软件标记表明gc开始工作( c->gc_mark_valid= 0;c->gc_done = ZERO_KEY;);清除bucket关联的gc flag ( SET_GC_MARK(b, 0);SET_GC_SECTORS_USED(b,0))
b. btree_root 工作调用bch_btree_gc_root来遍历btree,分析哪些bucket可被gc回收
c. bch_btree_gc_finish标记不能gc的bucket为meta, 并统计能gc的bucket数目
d.wake_up_allocators分配器thread
e. bch_moving_gc 根据标志位,完成实际gc工作
bch_btree_gc_root:
a. __bch_btree_mark_keya.如果bucket->gen > key->gen则不用gc; 该函数同时计算key->gen - bucket->gen的最大差值; 更新gc信息如下:
if (level) //非叶节点为元数据
SET_GC_MARK(g,GC_MARK_METADATA);
else if (KEY_DIRTY(k)) //bch_data_insert_start中会设置dirty位
SET_GC_MARK(g,GC_MARK_DIRTY);
else if (!GC_MARK(g))
SET_GC_MARK(g,GC_MARK_RECLAIMABLE);
/*占用的sector包含两个部分: bucket 所用的sector和key所占用的空间 */
SET_GC_SECTORS_USED(g,min_t(unsigned,
GC_SECTORS_USED(g) + KEY_SIZE(k),
MAX_GC_SECTORS_USED));
b. 调用btree_gc_recurse: 该函数遍历b+tree的每个node, 对每个node执行btree_gc_coalesce。 该函数判断若一个到多个(1 to 4)node的keys所占用的空间较小,则通过合并btree node的方式来减少bucket的使用量。
bch_moving_gc:
a. 遍历cache disk的bucket, 如果为元数据或数据占用量== bucket_size则 continue.
b. 统计哪些bucket可以通过移动来合并bucket的使用, 标记这些bucket为SET_GC_MOVE(b, 1);
c. callread_moving
read_moving:
a. w = bch_keybuf_next_rescan(c,&c->moving_gc_keys, //填充moving_gc_keys
&MAX_KEY, moving_pred);
b. 循环调用bch_keybuf_next_rescan每次从红黑树返回一个struct keybuf_key
c. moving_init(io);根据io生成&io->bio.bio;
bio->bi_rw = READ;
io->w = w;
d. closure_call(&io->cl,read_moving_submit, NULL, &cl);
staticvoid read_moving_submit(struct closure *cl) {
struct moving_io *io = container_of(cl,struct moving_io, cl);
struct bio *bio = &io->bio.bio;
// bio->bi_iter.bi_sector = PTR_OFFSET(&b->key, 0); io->w->key
该bio的bi_sector由bch_moving_gc中keybuf * w 中获得
bch_submit_bbio(bio, io->op.c,&io->w->key, 0);
continue_at(cl, write_moving,io->op.wq);
}
下面我们分析keybuf中的元素是如何得到的:
bch_moving_gcc中首次执行bch_keybuf_next_rescan时,由于初始时keybuf为空,所以会调用bch_refill_keybuf来填充。bch_refill_keybuf:调用bch_btree_map_keys(&refill.op,c, &buf->last_scanned, refill_keybuf_fn, MAP_END_KEY); 遍历b+tree叶子节点来填充.
refill_keybuf_fn将满足条件refill->pred的key加入到RBTtree中。
RB_INSERT(&buf->keys,w, node, keybuf_cmp);
pred = moving_pred
staticbool moving_pred(struct keybuf *buf, struct bkey *k) {
for (i = 0; i < KEY_PTRS(k); i++)
if (ptr_available(c, k, i)&&
GC_MOVE(PTR_BUCKET(c, k, i))) //若key对应的bucket被标记为move在bch_moving_gc中被设置
return true;
return false;
}
write_moving会尝试w->key的写moving,并调用replace功能(由于key已被移动):
bkey_copy(&op->replace_key,&io->w->key);
op->replace = true;
closure_call(&op->cl,bch_data_insert, NULL, cl);
小结gc的回收策略:
(1) 合并包含较少key的btree node, 来释放bucket
(2) 移动叶节点对应bucket(bucket->gen <= key->gen)数据区未用满的bucket来节省bucket;
7. Writeback机制
每个struct cached_dev 有一个struct keybuf writeback_keys成员用于记录要writeback的keys,本节将以这个变量为中心分析writeback的工作原理。writeback的主要工作在一个线程bch_writeback_thread中执行。
唤醒该线程的位置有如下三个:
(1) bch_cached_dev_detach/ bch_cached_dev_attach且super数据为dirty时
(2) bch_writeback_add(由cached_dev_write调用且不bypass时)
bch_writeback_thread:
a. 不为dirty或writeback机制未运行时该线程让出cpu控制权
b. searched_full_index = refill_dirty(dc);
c. 调用read_dirty, 该函数遍历writeback_keys,
io->bio.bi_bdev = PTR_CACHE(dc->disk.c, &w->key, 0)->bdev;
io->bio.bi_rw = READ; //从cache 设备读取数据
调用closure_call(&io->cl, read_dirty_submit, NULL, &cl); 提交读请求, 读请求完成后,read_dirty_submit==> write_dirty 向主设备写入数据。
d. 为了让writeback写不要太密集,调用delay = writeback_delay(dc,KEY_SIZE(&w->key));计算两次写之间的延迟时间
下次循环时: delay = schedule_timeout_uninterruptible(delay); 延迟一段时间
refill_dirty:
a if(dc->partial_stripes_expensive)
refill_full_stripes
b. structkeybuf *buf = &dc->writeback_keys;
struct bkey end = KEY(dc->disk.id, MAX_KEY_OFFSET, 0);
bch_refill_keybuf(dc->disk.c, buf, &end, dirty_pred);
refill_full_stripes: 用stripe来管理dirty区域,一个 stripe的默认扇区数为dc->disk.stripe_size = q->limits.io_opt>> 9; 该函数对每个dirty的stripe区域调用
bch_refill_keybuf(dc->disk.c, buf, &KEY(dc->disk.id,
next_stripe * dc->disk.stripe_size,0)
staticbool dirty_pred(struct keybuf *buf,struct bkey *k) {
return KEY_DIRTY(k);// bch_data_insert_start中根据情况会SET_KEY_DIRTY
}
bch_refill_keybuf的代码上节已经分析过了,这里不再重复。
stripe的dirty由bcache_dev_sectors_dirty_add函数设置,其在下面几种情况中被调用:
(1) bch_cached_dev_attach==> bch_sectors_dirty_init==> sectors_dirty_init_fn (super block dirty时)
(2) btree_insert_key ==> bch_btree_insert_key ==>
bch_extent_keys_ops . insert_fixup = bch_extent_insert_fixup 插入bkey到bset时
8. Journal管理
bcache在每次插入页节点时没有立即持久化元数据(一个bset),这样可以减少io开销; 而是引入journal,journal就是插入的keys的log,按照插入时间排序,只用记录叶子节点上bkey的更新,非叶子节点在分裂的时候就已经持久化了。这样每次写操作在数据写入后就只用记录一下log,在崩溃恢复的时候就可以根据这个log重新插入key。前面分析过bch_data_insert_keys会向b+tree插入叶节点.此时会调用:
if (!op->replace)
journal_ref =bch_journal(op->c, &op->insert_keys,
op->flush_journal ? cl : NULL);
来更新journal. 另外在启动bcache时会有如下调用:
run_cache_set:
a. bch_journal_read(c,&journal) 从cache disk中读出持久化的journal
b. bch_journal_markjournal list来确定那些journal需要重新提交
c. bch_journal_next:journal才用了双缓冲区,该函数交换两个缓冲区
d. bch_journal_replay(c,&journal);重做因为崩溃或突然关机以记录而为持久化的bset
journal模块初始化函数为:bch_cache_set_alloc==》int bch_journal_alloc(structcache_set *c) {
INIT_DELAYED_WORK(&j->work, journal_write_work);
c->journal_delay_ms = 100;
j->w[0].c = c;
j->w[1].c = c;
//建立journal的双缓冲区
if (!(init_fifo(&j->pin,JOURNAL_PIN, GFP_KERNEL)) ||
!(j->w[0].data = (void *) __get_free_pages(GFP_KERNEL, JSET_BITS)) ||
!(j->w[1].data = (void *) __get_free_pages(GFP_KERNEL, JSET_BITS)))
return -ENOMEM;
return 0;
}
bch_journal(struct cache_set *c, struct keylist *keys, structclosure *parent) 负责将keys写到journal中,它分两步进行:
(a) journal_wait_for_write,如果journal当前缓冲区能放下keylist中的key,直接返回,否则启动分为两种case: (1) cur journal未满则journal_try_write,尝试写一部分journal. (2) journal_reclaim尝试回收内存中journal空间;
(b) memcpy(bset_bkey_last(w->data),keys->keys, bch_keylist_bytes(keys));
w->data->keys +=bch_keylist_nkeys(keys); 将keys存入journal缓存
(c) if(parent) 则尝试持久化journal: journal_try_write(c);
else schedule_delayed_work(&c->journal.work,
msecs_to_jiffies(c->journal_delay_ms)); 延迟写
journal是否已满的依据时,实际用于存储journal的disk区域是否已满;
#definejournal_full(j) \
(!(j)->blocks_free ||fifo_free(&(j)->pin) <= 1)
journal_reclaim(struct cache_set *c): 由于journal在磁盘中的存储空间有限, 当journal已满时需要抛弃较旧的journal. 要抛弃的journal 对应的idx为:ja->discard_idx; 且有ja->discard_idx = ja->last_idx; 而一个idx对应一个bucket.
bucket_to_sector(ca->set, ca->sb.d[ja->discard_idx]);得到对应的扇区号。 抛弃的流程在do_journal_discard中实现。 而选择last_idx的依据如下:
last_seq =last_seq(&c->journal);
for_each_cache(ca, c, iter) {
struct journal_device *ja =&ca->journal;
while (ja->last_idx !=ja->cur_idx &&
ja->seq[ja->last_idx]
ja->last_idx =(ja->last_idx + 1) %
ca->sb.njournal_buckets;
}
#definelast_seq(j) ((j)->seq -fifo_used(&(j)->pin) + 1)
本节最后看看journal的写入流程:
journal_try_write: w->need_write = true; ==> journal_write_unlocked
a. 如果journal区域满调用journal_reclaim, 然后btree_flush_write找到最old的journal所对应的btree node,写到磁盘
b.插入journal,并更新到磁盘journal区域
小结:bcache提供有限大写的cache disk区域来存放journal,当该区域已满时需要选择最old的加以替换。