Ceph OSD日誌分析

Thomas是本人在Ceph中國社區翻譯小組所用的筆名,該文首次發佈在Ceph中國社區,現轉載到本人博客,以供大家傳閱

Ceph OSD日誌分析

本文由 Ceph中國社區-Thomas翻譯,陳曉熹校稿 。

英文出處:CEPH OSD JOURNAL 歡迎加入 翻譯小組

簡介

與ext4這類日誌文件系統類似,Ceph OSD日誌也是一種事務日誌。它是基於文件系統的OSD的關鍵組成部分,提供存儲系統所需的一致性保證。

讓我們從Ceph documentation中有關日誌的描述開始:

Ceph OSD使用日誌有兩個原因:速度及一致性。

速度: 日誌使得Ceph OSD Daemon進程能夠快速的提交小IO。Ceph將小的隨機IO順序的寫入日誌,讓後端文件系統有更多時間來合併IO,提升系統的突發負載能力。然而,這可能帶來劇烈的性能抖動,表現爲一段時間內的高速寫入,而之後一段時間內沒有任何寫入直至文件系統追上日誌的進度。

一致性:Ceph OSD Daemon進程需要一個文件系統接口來保證多個操作的原子性。Ceph OSD Daemon進程提交操作描述到日誌並將操作應用到文件系統。這使得能夠原子的更新一個對象(如:pg metadata)。在filestore配置的[min_sync_interval max_sync_interval]間隔範圍內,Ceph OSD Daemon進程會停止寫操作,並同步文件系統與日誌,建立同步點[譯者注:此時會記錄已同步的最大Seq號],以便Ceph OSD Daemon清除日誌項、重用日誌空間。在故障發生後,Ceph OSD Daemon從最後一個同步操作點開始重放日誌。

爲什麼?

一個事務完成後就會得到ObjectStore的通知。但是,何時完成纔算真的完成呢?

當一個write系統調用返回時只能保證後續的read能讀到之前寫入的數據。系統內核將數據放在緩存中,而不會立刻寫入到磁盤。這主要是性能原因。你可以通過設置特殊選項來打開磁盤[譯者注:O_DSYNC],等數據寫入到磁盤才讓write返回。但是,這會導致性能急劇下降,特別是有很多小IO寫的時候。Ceph通過完全日誌來解決這個問題。即日誌中同時包含數據和元數據。因此,每個操作都要寫兩次:首先是寫到日誌,之後要應用到磁盤。日誌按上面描述的方式打開[譯者注:Ceph默認以O_DSYNC + O_DIRECT 方式打開日誌]從而保證日誌直到實際寫入磁盤後才返回。日誌的順序寫一定程度上彌補了direct IO的低效。週期性地,或者由事務觸發,或者日誌滿,都會引起磁盤同步(fssync)及日誌清理。

術語

在下面的敘述中,我稱服務於OSD存儲的塊設備爲磁盤,承載日誌的設備、不管是何種設備類型都稱爲日誌。

相關工作

版本信息

本文基於Ceph master commit-ish 0a76aa5 from 2015-03-16而作。

概述

本文聚焦於日誌的實現,通過代碼分析達成該目的。

作爲該文的興趣引入點,我將先回顧源代碼。然後,在深入事務的執行原理前,描述下各種日誌模式的不同點。之後,我將描述數據是如何被寫入日誌的。接着,介紹日誌和文件系統同步以及日誌重做事務是怎麼回事。最後,介紹日誌文件結構。

代碼回顧

源碼文件

主要功能

一系列的ObjectStore::Transaction事務經由queue_transactions接口提交到FileStore。在這裏,基於不同的日誌模式,這些事務被按照不同的方式處理,很可能,通過JournalingObjectStore::_op_journal_transactions接口提交到日誌,然後調用FileJournal::submit_entry應用日誌。

週期性地,通過空間事務或者日誌滿引起的FileStore::sync\_entry()調用同步磁盤文件系統以及調用FileJournal::committed_thru刷新日誌。

函數調用者功能
FileStore::queue_transactionsOSD基於日誌模式處理事務
FileStore::_do_opOpWQ調用FileStore::_do_transaction
FileStore::_finish_opOpWQ調用onreadable, onreadable_sync
FileStore::_do_transaction_do_transactions應用事務、調用FileStore::_$OPERATION
FileStore::sync_entry()Thread; sync_cond同步OSD文件系統
JournalingObjectStore:: _op_journal_transactionsqueue_transactionsOp轉換成Buffer、提交到日誌
FileJournal::submit_entry_op_journal_transactions添加日誌到FileJournal
FileJournal::committed_thrusync_entry()設置日誌項爲提交狀態(之後丟失它們)

重要的工具類

功能
FileStore::Op多個事務及回調的包裝,與事務中包含的操作不同
JournalingObjectStore::SubmitManager管理Op序列號
JournalingObjectStore::ApplyManager將Op應用到日誌上
FileStore::OpWQ工作隊列
ObjectStore::Transaction多個操作的事務封裝

日誌模式

FileStore可以採用不同的日誌模式。各種日誌模式的不同之處在於:何時記錄日誌?何時通知?何時寫入到磁盤?常用的日誌模式是:writeback(ext4、xfs)以及parallel(btrfs)。其他的日誌模式還有:No journal(不鼓勵使用),Trailing(文檔引述:廢棄,不要使用)。

No Journal

沒有日誌的情況下,IO事務會被立刻調度。在sync調用後,會觸發oncommit回調。因此,當FileStore執行sync的時候,會觸發大量的回調通知。

Writeahead

先將事務記錄到日誌,日誌提交後,纔會將事務提交到應用隊列,並觸發回調函數通知客戶端寫入完成。這種模式是爲像XFS和ext4之類寫就緒文件系統準備的。在一個磁盤文件中存儲了日誌重做序列號commit_op_seq。該序列號在每次同步後都會遞增,重放日誌將從這個序列號標識的操作往後進行。

Parallel

日誌與事務調度同時執行。這種模式是爲Btrfs這類寫時複製文件系統而設計。它們提供了穩定的快照回滾機制。執行日誌重做時,當前的髒文件系統會被回滾到前一個快照。快照加上日誌會將文件系統恢復到一個一致性狀態。

Trailing

這種模式已經廢棄,它先執行事務再提交日誌。

默認模式

下面的代碼段摘自os/FileStore.cc:

// select journal mode?
if (journal) {
    if (!m_filestore_journal_writeahead &&
        !m_filestore_journal_parallel &&
        !m_filestore_journal_trailing) {
        if (!backend->can_checkpoint()) {
            m_filestore_journal_writeahead = true;
            dout(0) << "mount: enabling WRITEAHEAD journal mode: checkpoint is not enabled" << dendl;
        } else {
           m_filestore_journal_parallel = true;
           dout(0) << "mount: enabling PARALLEL journal mode: fs, checkpoint is enabled" << dendl;
        }
   } else {
       if (m_filestore_journal_writeahead)
           dout(0) << "mount: WRITEAHEAD journal mode explicitly enabled in conf" << dendl;
       if (m_filestore_journal_parallel)
           dout(0) << "mount: PARALLEL journal mode explicitly enabled in conf" << dendl;
       if (m_filestore_journal_trailing)
           dout(0) << "mount: TRAILING journal mode explicitly enabled in conf" << dendl;
   }
    if (m_filestore_journal_writeahead)
       journal->set_wait_on_full(true);
} else {
    dout(0) << "mount: no journal" << dendl;
}

如果後端文件系統(backend)支持檢查點,就採用Parallel模式。否則就是Writeahead模式。Trailing模式和No Journal模式需要在配置文件中直接設置。

事務:應用/日誌

事務通過os/FileStore.cc中實現的FileStore::queue_transactions接口進入到FileStore。該方法基於不同的日誌模式處理事務。事務包含三種類型的回調指針。在事務被處理後,FileStore及日誌會調用它們。在整個代碼庫中,它們的名稱不總是一樣。但它們的常用名稱及含義如下表所示:

ObjectStore::TransactionFileStore::queue_transactions描述
oncommitondisk日誌已提交,可恢復,但還不可讀
onappliedonreadable事務可讀,即:已寫入磁盤。異步回調
onapplied_synconreadable_sync與onreadable含義一樣。不過是同步回調

通常oncommit/ondisk在事務提交到日誌後被調用,在No Journal模式中是一個例外,它在同步磁盤數據後被調用。

兩組onapplied/onreadable回調的區別在於FileStore調用它們的方式。事務處理線程執行事務後立即調用同步onapplied_sync/onreadable_sync回調,並將異步onapplied/onreadable投遞到finisher服務線程。

事務在Trailing模式下是另一個例外,它不在線程池中執行,而是首先調用_do_op,然後調用_finish_op,onreadable回調在_finish_op中被調用。

Parallel及Writeahead

if (journal && journal->is_writeable() && !m_filestore_journal_trailing) {
    Op *o = build_op(tls, onreadable, onreadable_sync, osd_op);
    op_queue_reserve_throttle(o, handle);
    journal->throttle();
    uint64_t op_num = submit_manager.op_submit_start();
    o->op = op_num;

    if (m_filestore_do_dump)
        dump_transactions(o->tls, o->op, osr);

    if (m_filestore_journal_parallel) {
        dout(5) << "queue_transactions (parallel) " << o->op << " " << o->tls << dendl;

        _op_journal_transactions(o->tls, o->op, ondisk, osd_op);

        // queue inside submit_manager op submission lock
        queue_op(osr, o);
    } else if (m_filestore_journal_writeahead) {
        dout(5) << "queue_transactions (writeahead) " << o->op << " " << o->tls << dendl;

        osr->queue_journal(o->op);

        _op_journal_transactions(o->tls, o->op,
                           new C_JournaledAhead(this, osr, o, ondisk),
                           osd_op);
    } else {
        assert(0);
    }
    submit_manager.op_submit_finish(op_num);
    return 0;
}

Parallel

先將事務放入日誌隊列,然後將磁盤IO操作放入另一個隊列。

Writeahead

用C_JournaledAhead封裝ondisk回調。新的ondisk通過queue_op加入隊列,原先的ondisk回調在之後被處理。

No Journal

if (!journal) {
    Op *o = build_op(tls, onreadable, onreadable_sync, osd_op);
    dout(5) << __func__ << " (no journal) " << o << " " << tls << dendl;

    op_queue_reserve_throttle(o, handle);

    uint64_t op_num = submit_manager.op_submit_start();
    o->op = op_num;

    if (m_filestore_do_dump)
        dump_transactions(o->tls, o->op, osr);

    queue_op(osr, o);

    if (ondisk)
        apply_manager.add_waiter(op_num, ondisk);
    submit_manager.op_submit_finish(op_num);
    return 0;
}

No Journal模式與上面兩種模式相似,不同之處在於ondisk回調的處理方式。由於沒有使用日誌,要等到磁盤同步完成後事務才被看成是提交的。apply_manager.add_waiter(op_num, ondisk)就是用來幹這個事。磁盤同步完成後ApplyManager會調用隊列中的waiters。

Trailing

uint64_t op = submit_manager.op_submit_start();
dout(5) << "queue_transactions (trailing journal) " << op << " " << tls << dendl;

if (m_filestore_do_dump)
    dump_transactions(tls, op, osr);

apply_manager.op_apply_start(op);
int r = do_transactions(tls, op);

if (r >= 0) {
    _op_journal_transactions(tls, op, ondisk, osd_op);
} else {
    delete ondisk;
}

// start on_readable finisher after we queue journal item, as on_readable callback
// is allowed to delete the Transaction
if (onreadable_sync) {
    onreadable_sync->complete(r);
}
op_finisher.queue(onreadable, r);

submit_manager.op_submit_finish(op);
apply_manager.op_apply_finish(op);

return r;

Trailing模式與其他模式有很大的不同,事務沒有在線程池中執行,而是在當前線程中執行。事務完成後才提交到日誌。最後會觸發onreadable回調,在其他模式中該操作由sync_entry完成。

比與其他模式的明顯區別更有意思的是,這種模式下事務的執行代碼十分的簡潔。例如調用submit_manager和apply_manager以及由finisher完成回調。

寫日誌

通過FileStore::submit_entry方法將日誌項添加到日誌。日誌項首先被添加到FileJournal的writeq隊列。除了IO數據外,oncommit回調被添加到另外一個隊列並在日誌完成時調用。日誌的磁盤數據結構請查看後文的磁盤數據結構一節。

日誌操作由一個獨立的線程完成。線程函數是FileJournal::write_thread_entry,它是一個循環。基於libaio的支持情況,最終的寫操作由do_write或者do_aio_write完成。

儘管如此,在日誌完成後還需要更新日誌文件超級快中的journaled_seq並由finisher線程負責完成oncommit回調。

日誌文件以O_DIRECTO_DSYNC選項打開(Linux Man Page:open(2))。

FileStore 同步

同步在FileStore::sync_entry()中實現。它運行在一個獨立的線程中並等待在條件變量sync_cond上。同步完成後,磁盤或者快照中的committed_op_seq與日誌中的committed_up_to就一致。同步時,首先從ApplyManager獲得需要同步的序列號。具體的同步與文件系統相關。如果文件系統支持檢查點:(可以回頭看看並行模式)

  1. 創建檢查點
  2. 同步檢查點
  3. 寫序列號到快照

否則調用fssync同步整個文件系統,如果文件系統支持sync就調用它同步。之後記錄序列號。最後通過ApplyManager通知日誌清除已經同步的日誌項[譯者注:事實上由於日誌是循環使用的,類似於循環鏈表,只需移動頭指針即可]。

同步間隔

同步是週期性的,由事務或者日誌滿事件觸發[譯者注:事實上是日誌半滿,參見FileJournal::check_for_full]。同步間隔通過filestore max sync intervalfilestore min sync interval配置。默認值分別是5s及0.01s。

Ceph’s documentation中對同步間隔的描述如下:

週期性地,FileStore需要停止寫入並同步文件系統,以創建一致的提交點。之後才能釋放提交點之前的日誌項。同步得越頻繁,同步所需要的時間就越短同時留在日誌文件中的數據也就越少。同步得越不頻繁,後端的文件系統就能夠更好的聚合小IO寫及優化元數據更新,可能帶來更好的同步效率。

日誌刷新

日誌刷新與FileStore同步是等效的。通過FileJournal::committed_thru(uint64_t seq)方法通知日誌刷新。seq參數需要大於上次的提交序列號。基於日誌文件頭中的start指針及序列號來丟棄老的日誌項。如果支持TRIM,磁盤塊數據也會被丟棄。

日誌重做

非常直觀。日誌重做在JournalingObjectStore::journal_replay中實現。

總之:

  • 打開日誌文件
  • 讀出日誌項
  • 解析出事務
  • 將事務傳遞到do_transactions中執行

Mount調用中發起的日誌重做是OSD初始化的一部分。如果文件系統支持檢查點,它會將OSD回滾到最後一個一致性檢查點。

磁盤數據結構

在這一節,我將描述日誌的數據結構。

日誌

flags 只定義了一個標誌:FLAG_CRC。每個新OSD的默認值。

fsid Ceph FSID

block_size 通常與頁大小一致

alignment 通常與block_size一致

max_size 按block_size對齊的日誌文件最大大小

start 第一個日誌項的起始偏移

committed_up_to 已提交的最大日誌序列號(該序列號之前的日誌項都已經提交了)

start_seq 第一個日誌項的序列號

日誌項

seq 日誌序列號

crc32c 數據部分的CRC32哈希值

pre_pad 數據前的填充區

post_pad 數據後的填充區

magic1 日誌項存放位置

magic2 fsid與seq及len的異或值(fsid XOR seq XOR len

每個日誌項都有一個日誌頭和日誌尾,實際上日誌尾是日誌頭的一個拷貝。日誌數據按照日誌文件頭中指定的方式對齊。

日誌數據

一系列的事務被傳遞到日誌。在日誌處理過程中,首先用encoding.h中定義的編碼函數編碼這些事務。每種類型的事務都在ObjectStore.h中定義了自己的解碼器。需要注意的是日誌項中不僅包括元數據,還包含IO數據。也就是說,一個寫事務包括了它的IO數據。事務編碼後,傳遞到日誌也就被當成一個不透明的數據塊。

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