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存儲的塊設備爲磁盤,承載日誌的設備、不管是何種設備類型都稱爲日誌。
相關工作
- Sebastien Han: Ceph IO Patterns The Good The Bad and The Ugly
- Sebastien Han: Ceph: Recover OSDs after SSD Journal Failure
- Solid-state drives and Ceph OSD journals
- Ceph Documentation: Journal Config Reference
- Measure Ceph RBD performance in a quantitative way Part I Part II
版本信息
本文基於Ceph master commit-ish 0a76aa5 from 2015-03-16而作。
概述
本文聚焦於日誌的實現,通過代碼分析達成該目的。
作爲該文的興趣引入點,我將先回顧源代碼。然後,在深入事務的執行原理前,描述下各種日誌模式的不同點。之後,我將描述數據是如何被寫入日誌的。接着,介紹日誌和文件系統同步以及日誌重做事務是怎麼回事。最後,介紹日誌文件結構。
代碼回顧
源碼文件
- os/Filejournal.h (Github)
- os/Filejournal.cc (Github)
- os/JournalingObjectStore.h (Github)
- os/JournalingObjectStore.cc (Github)
- os/ObjectStore.h (Github)
- os/ObjectStore.cc (Github)
- os/FileStore.h (Github)
- os/FileStore.cc (Github)
主要功能
一系列的ObjectStore::Transaction事務經由queue_transactions接口提交到FileStore。在這裏,基於不同的日誌模式,這些事務被按照不同的方式處理,很可能,通過JournalingObjectStore::_op_journal_transactions接口提交到日誌,然後調用FileJournal::submit_entry應用日誌。
週期性地,通過空間事務或者日誌滿引起的FileStore::sync\_entry()
調用同步磁盤文件系統以及調用FileJournal::committed_thru
刷新日誌。
函數 | 調用者 | 功能 |
FileStore::queue_transactions | OSD | 基於日誌模式處理事務 |
FileStore::_do_op | OpWQ | 調用FileStore::_do_transaction |
FileStore::_finish_op | OpWQ | 調用onreadable, onreadable_sync |
FileStore::_do_transaction | _do_transactions | 應用事務、調用FileStore::_$OPERATION |
FileStore::sync_entry() | Thread; sync_cond | 同步OSD文件系統 |
JournalingObjectStore:: _op_journal_transactions | queue_transactions | Op轉換成Buffer、提交到日誌 |
FileJournal::submit_entry | _op_journal_transactions | 添加日誌到FileJournal |
FileJournal::committed_thru | sync_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::Transaction | FileStore::queue_transactions | 描述 |
oncommit | ondisk | 日誌已提交,可恢復,但還不可讀 |
onapplied | onreadable | 事務可讀,即:已寫入磁盤。異步回調 |
onapplied_sync | onreadable_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_DIRECT
及O_DSYNC
選項打開(Linux Man Page:open(2))。
FileStore 同步
同步在FileStore::sync_entry()
中實現。它運行在一個獨立的線程中並等待在條件變量sync_cond
上。同步完成後,磁盤或者快照中的committed_op_seq
與日誌中的committed_up_to
就一致。同步時,首先從ApplyManager獲得需要同步的序列號。具體的同步與文件系統相關。如果文件系統支持檢查點:(可以回頭看看並行模式)
- 創建檢查點
- 同步檢查點
- 寫序列號到快照
否則調用fssync同步整個文件系統,如果文件系統支持sync就調用它同步。之後記錄序列號。最後通過ApplyManager通知日誌清除已經同步的日誌項[譯者注:事實上由於日誌是循環使用的,類似於循環鏈表,只需移動頭指針即可]。
同步間隔
同步是週期性的,由事務或者日誌滿事件觸發[譯者注:事實上是日誌半滿,參見FileJournal::check_for_full]。同步間隔通過filestore max sync interval
及filestore 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數據。事務編碼後,傳遞到日誌也就被當成一個不透明的數據塊。