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