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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章