BCache源码浅析之四分配管理与Journal

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] <last_seq)

                            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的加以替换。

展开阅读全文

没有更多推荐了,返回首页