BlueStore原始碼分析之事物狀態機

java06051515發表於2020-05-08

前言

BlueStore可以理解為一個支援ACID的本地日誌型檔案系統。所有的讀寫都是以 Transaction進行,又因為支援覆蓋寫,所以寫流程設計的相對複雜一些,涉及到一系列的狀態轉換。我們著重分析一下狀態機、延遲指標以及如何保證IO的順序性和併發性。

目錄

狀態機

queue_transactions

queue_transactions是ObjectStore層的統一入口,KVStore、MemStore、FileStore、BlueStore都相應的實現了這個介面。 state_t state變數記錄了當前時刻事物處於哪個狀態。在建立TransactionContext的時候會將 state初始化為 STATE_PREPARE,然後在 _txc_add_transaction中會根據操作碼型別(opcode)進行不同的處理。同時會獲取PG對應的OpSequencer(每個PG有一個OpSequencer)用來保證PG上的IO序列執行,對於deferred-write會將其資料寫入RocksDB(WAL)。

以下階段就進入BlueStore狀態機了,我們以寫流程為導向分析狀態機的每個狀態。

STATE_PREPARE

從state_prepare開始已經進入事物的狀態機了。這個階段會呼叫 _txc_add_transaction將OSD層面的事物轉換為BlueStore層面的事物;然後檢查是否還有未提交的IO,如果還有就將state設定為 STATE_AIO_WAIT並呼叫 _txc_aio_submit提交IO,然後退出狀態機,之後aio完成的時候會呼叫回撥函式 txc_aio_finish再次進入狀態機;否則就進入 STATE_AIO_WAIT狀態。
_txc_aio_submit函式呼叫棧:

bdev->aio_submit –> KernelDevice::aio_submit –> io_submit將aio提交到核心Libaio佇列。

主要工作:準備工作,生成大小寫、初始化TransContext、deferred_txn、分配磁碟空間等。

延遲指標l_bluestore_state_prepare_lat,從進入狀態機到prepare階段完成,平均延遲大概0.2ms左右。

STATE_AIO_WAIT

該階段會呼叫 _txc_finish_io進行SimpleWrite的IO保序等處理,然後將狀態設定為 STATE_IO_DONE再呼叫_txc_state_proc進入下一個狀態的處理。

主要工作:對IO保序,等待AIO的完成。

延遲指標l_bluestore_state_aio_wait_lat,從prepare階段完成開始到AIO完成,平均延遲受限於裝置,SSD 0.03ms左右。

STATE_IO_DONE

完成AIO,並進入 STATE_KV_QUEUED階段。會根據 bluestore_sync_submit_transaction做不同處理。該值為布林值,預設為false。

如果為true,設定狀態為 STATE_KV_SUBMITTED並且同步提交kv到RocksDB但是沒有sync落盤(submit_transaction),然後 applied_kv

如果為false,則不用做上面的操作,但是以下操作都會做。

最後將事物放在 kv_queue裡,透過 kv_cond通知 kv_sync_thread去同步IO和後設資料。

主要工作:將事物放入 kv_queue,然後通知 kv_sync_thread,osr的IO保序可能會block。

延遲指標l_bluestore_state_io_done_lat,平均延遲在0.004ms,通常很小主要耗在對SimpleWrite的IO保序處理上。

STATE_KV_QUEUED

該階段主要在 kv_sync_thread執行緒中同步IO和後設資料,並且將狀態設定為 STATE_KV_SUBMITTED。具體會在非同步執行緒章節分析 kv_sync_thread執行緒。

主要工作:從 kv_sync_thread佇列中取出事物。

延遲指標l_bluestore_state_kv_queued_lat,從事物進入佇列到取出事物,平均延遲在0.08ms,因為是單執行緒順序處理的,所以依賴於 kv_sync_thread處理事物的速度。

STATE_KV_SUBMITTED

等待 kv_sync_thread中kv後設資料和IO資料的Sync完成,然後將狀態設定為 STATE_KV_DONE並且回撥finisher執行緒。

主要工作:等待kv後設資料和IO資料的Sync完成,回撥finisher執行緒。

延遲指標l_bluestore_state_kv_committing_lat,從佇列取出事物到完成kv同步,平均延遲1.0ms,有極大的最佳化空間。

STATE_KV_DONE

如果是SimpleWrite,則直接將狀態設定為 STATE_FINISHING;如果是DeferredWrite,則將狀態設定為 STATE_DEFERRED_QUEUED並放入 deferred_queue

主要工作:如上。

延遲指標l_bluestore_state_kv_done_lat,平均延遲0.0002ms,可以忽略不計。

STATE_DEFERRED_QUEUED

主要工作:將延遲IO放入 deferred_queue等待提交。

延遲指標l_bluestore_state_deferred_queued_lat,通常不小,沒有資料暫不貼出。

STATE_DEFERRED_CLEANUP

主要工作:清理延遲IO在RocksDB上的WAL。

延遲指標l_bluestore_state_deferred_cleanup_lat,通常不小,沒有資料暫不貼出。

STATE_FINISHING

主要工作:設定狀態為 STATE_DONE,如果還有DeferredIO也會提交。

延遲指標l_bluestore_state_finishing_lat,平均延遲0.001ms。

STATE_DONE

主要工作:標識整個IO完成。

延遲指標l_bluestore_state_done_lat

延遲分析

BlueStore定義了狀態機的多個延遲指標,由PerfCounters採集,函式為 BlueStore::_init_logger()

可以使用 ceph daemon osd.0 perf dump或者 ceph daemonperf osd.0來檢視對應的延遲情況。

除了每個狀態的延遲,我們通常也會關注以下兩個延遲指標:

b.add_time_avg(l_bluestore_kv_lat, "kv_lat",
 "Average kv_thread sync latency", "k_l",
 
b.add_time_avg(l_bluestore_commit_lat, "commit_lat",
 "Average commit latency", "c_l",


BlueStore延遲主要花費在 l_bluestore_state_kv_committing_lat也即 c_l,大概1ms左右。

BlueStore統計狀態機每個階段延遲的方法如下:


// 該階段延遲 = 上階段完成到該階段結束
void log_state_latency(PerfCounters *logger, int state) {
 utime_t lat, now = ceph_clock_now();
 lat = now - last_stamp;
 logger->tinc(state, lat);
 last_stamp = now;
}


在塊儲存的使用場景中,除了使用者併發IO外,通常使用者也會使用 dd等序列IO的命令,此時便受限於讀寫的絕對延遲,擴容加機器、增加執行緒數等橫向擴充套件的最佳化便是無效的,所以我們需要關注兩方面的延遲: 併發IO延遲序列IO延遲

併發IO延遲最佳化:kv_sync_thread、kv_finalize_thread多執行緒化;自定義WAL;async read。

序列IO延遲最佳化:並行提交後設資料、資料;將sync操作與其他狀態並行處理。

IO保序

保證IO的順序性以及併發性是分散式儲存必然面臨的一個問題。因為BlueStore使用非同步IO,後提交的IO可能比早提交的IO完成的早,所以更要保證IO的順序,防止資料發生錯亂。 客戶端可能會對PG中的一個Object連續提交多次讀寫請求,每次請求對應一個Transaction,在OSD層面透過PGLock將併發的讀寫請求在PG層面序列化,然後按序依次提交到ObjectStore層,ObjectStore層透過PG的OpSequencer保證順序處理讀寫請求

BlueStore寫型別有SimpleWrite、DeferredWrite兩種,所以我們分析一下SimpleWrite、DeferredWrite下的IO保序問題。

SimpleWrite

因為 STATE_AIO_WAIT階段使用Libaio,所以需要保證PG對應的OpSequencer中的txc按排隊的先後順序依次進入kv_queue被 kv_sync_thread處理,也即txc在OpSequencer中的順序和在kv_queue中的順序是一致的。

void BlueStore::_txc_finish_io(TransContext *txc)
{
 // 獲取txc所屬的OpSequencer,並且加鎖,保證互斥訪問osr
 OpSequencer *osr = txc->osr.get();
 std::lock_guard<std::mutex> l(osr->qlock);
 
 // 設定狀態機的state為STATE_IO_DONE
 txc->state = TransContext::STATE_IO_DONE;
 
 // 清除txc正在執行的aio
 txc->ioc.running_aios.clear();
 
 // 定位當前txc在osr的位置
 OpSequencer::q_list_t::iterator p = osr->q.iterator_to(*txc);
 
 while (p != osr->q.begin()) {
 --p;
 // 如果前面還有未完成IO的txc,那麼需要停止當前txc操作,等待前面txc完成IO。
 // 目的是:確保之前txc的IO都完成。
 if (p->state < TransContext::STATE_IO_DONE) {
 return;
 }
 
 // 前面的txc已經進入大於等於STATE_KV_QUEUED的狀態了,那麼遞增p並退出迴圈。
 // 目的是:找到狀態為STATE_IO_DONE的且在osr中排序最靠前的txc。
 if (p->state > TransContext::STATE_IO_DONE) {
 ++p;
 break;
 }
 }
 
 // 依次處理狀態為STATE_IO_DONE的tx
 // 將txc放入kv_sync_thread的kv_queue、kv_queue_unsubmitted佇列
 do {
 _txc_state_proc(&*p++);
 } while (p != osr->q.end() && p->state == TransContext::STATE_IO_DONE);
 ......
}


DeferredWrite

DeferredWrite在IO的時候也是透過Libaio提交到核心Libaio佇列進行寫資料,也需要保證IO的順序性。

相應的資料結構如下:

class BlueStore {
 typedef boost::intrusive::list<
 OpSequencer, boost::intrusive::member_hook<
 OpSequencer, boost::intrusive::list_member_hook<>,
 &OpSequencer::deferred_osr_queue_item>>
 deferred_osr_queue_t;
 
 // osr's with deferred io pending
 deferred_osr_queue_t deferred_queue;
}
 
class OpSequencer {
 DeferredBatch *deferred_running = nullptr;
 DeferredBatch *deferred_pending = nullptr;
}
 
struct DeferredBatch {
 OpSequencer *osr;
 
 // txcs in this batch
 deferred_queue_t txcs;
}


BlueStore內部包含一個成員變數deferred_queue;deferred_queue佇列包含需要執行DeferredIO的OpSequencer;每個OpSequencer包含deferred_running和deferred_pending兩個DeferredBatch型別的變數;DeferredBatch包含一個txc陣列。

如果PG有寫請求,會在PG對應的OpSequencer中的deferred_pending中排隊加入txc,待時機成熟的時候,一次性提交所有txc給Libaio,執行完成後才會進行下一次提交,這樣不會導致DeferredIO亂序。

void BlueStore::_deferred_queue(TransContext *txc)
{
 deferred_lock.lock();
 // 排隊osr
 if (!txc->osr->deferred_pending && !txc->osr->deferred_running) {
 deferred_queue.push_back(*txc->osr);
 }
 
 // 追加txc到deferred_pending中
 txc->osr->deferred_pending->txcs.push_back(*txc);
 
 _deferred_submit_unlock(txc->osr.get());
 ......
}
 
void BlueStore::_deferred_submit_unlock(OpSequencer *osr)
{
 ......
 // 切換指標,保證每次操作完成後才會進行下一次提交
 osr->deferred_running = osr->deferred_pending;
 osr->deferred_pending = nullptr;
 ......
 
 while (true) {
 ......
 // 準備所有txc的寫buffer
 int r = bdev->aio_write(start, bl, &b->ioc, false);
 }
 
 ......
 // 一次性提交所有txc
 bdev->aio_submit(&b->ioc);
}


執行緒佇列

執行緒+佇列是實現非同步操作的基礎。BlueStore的一次IO經過狀態機要進入多個佇列並被不同的執行緒處理然後回撥,執行緒+佇列是BlueStore事物狀態機的重要組成部分。BlueStore中的執行緒大致有7種。

  • mempool_thread:無佇列,後臺監控記憶體的使用情況,超過記憶體使用的限制便會做trim。
  • aio_thread:佇列為Libaio核心queue,收割完成的aio事件。
  • discard_thread:佇列為discard_queued,對SSD磁碟上的extent做Trim。
  • kv_sync_thread:佇列為kv_queue、deferred_done_queue、deferred_stable_queue,sync後設資料和資料。
  • kv_finalize_thread:佇列為kv_committing_to_finalize、deferred_stable_to_finalize,執行清理功能。
  • deferred_finisher:呼叫回撥函式提交DeferredIO的請求。
  • finishers:多個回撥執行緒Finisher,通知使用者請求完成。

我們主要分析 aio_threadkv_sync_threadkv_finalize_thread

aio_thread

aio_thread比較簡單,屬於KernelDevice模組的,主要作用是收割完成的aio事件,並觸發回撥函式。

void KernelDevice::_aio_thread() {
 while (!aio_stop) {
 ......
 // 獲取完成的aio
 int r = aio_queue.get_next_completed(cct->_conf->bdev_aio_poll_ms, aio, max);
 // 設定flush標誌為true。
 io_since_flush.store(true);
 // 獲取aio的返回值
 long r = aio[i]->get_return_value();
 ......
 // 呼叫aio完成的回撥函式
 if (ioc->priv) {
 if (--ioc->num_running == 0) {
 aio_callback(aio_callback_priv, ioc->priv);
 }
 } 
 }
}

涉及延遲指標state_aio_wait_latstate_io_done_lat

kv_sync_thread

當IO完成後,要麼將txc放入佇列,要麼將dbh放入佇列,雖然對應不同佇列,但都是由 kv_sync_thread執行後續操作。

對於SimpleWrite,都是寫新的磁碟block(如果是cow,也是寫新的block,只是事務中k/v操作增加對舊的block的回收操作),所以先由aio_thread寫block,再由kv_sync_thread同步元資訊,無論什麼時候掛掉,資料都不會損壞。

對於DeferredWrite,在事物的prepare階段將需要DeferredWrite的資料作為k/v對(也稱為WAL)寫入基於RocksDB封裝的db_transaction中,此時還在記憶體,kv_sync_thread第一次的commit操作中,將wal持久化在了k/v系統中,然後進行後續的操作,異常的情況,可以透過回放wal,資料也不會損壞。

kv_sync_thread主要執行的操作為:在Libaio寫完資料後,需要透過kv_sync_thread更新後設資料k/v,主要包含object的Onode、擴充套件屬性、FreelistManager的磁碟空間資訊等等,這些必須按順序操作。

涉及的佇列如下:

  • kv_queue:需要執行commit的txc佇列。將kv_queue中的txc存入kv_committing中,並提交給RocksDB,即執行操作db->submit_transaction,設定狀態為STATE_KV_SUBMITTED,並將kv_committing中的txc放入kv_committing_to_finalize,等待執行緒kv_finalize_thread執行。
  • deferred_done_queue:已經完成DeferredIO操作的dbh佇列,還沒有sync磁碟。這個佇列的dbh會有兩種結果: 1) 如果沒有做flush操作,會將其放入deferred_stable_queue待下次迴圈繼續處理 2) 如果做了flush操作,說明資料已經落盤,即已經是stable的了,直接將其插入deferred_stable_queue佇列。這裡stable的意思就是資料已經sync到磁碟了,前面RocksDB中記錄的wal沒用可以刪除了。
  • deferred_stable_queue:DeferredIO已經落盤,等待清理RocksDB中的WAL。依次操作dbh中的txc,將RocksDB中的wal刪除,然後dbh入佇列deferred_stable_to_finalize,等待執行緒kv_finalize_thread執行。
void BlueStore::_kv_sync_thread() {
 while (true) {
 // 交換指標
 kv_committing.swap(kv_queue);
 kv_submitting.swap(kv_queue_unsubmitted);
 deferred_done.swap(deferred_done_queue);
 deferred_stable.swap(deferred_stable_queue)
 
 // 處理 deferred_done_queue
 if (force_flush) {
 // flush/barrier on block device
 bdev->flush();
 // if we flush then deferred done are now deferred stable
 deferred_stable.insert(deferred_stable.end(),
 deferred_done.begin(),
 deferred_done.end());
 deferred_done.clear();
 }
 
 // 處理 kv_queue
 for (auto txc : kv_committing) {
 int r = cct->_conf->bluestore_debug_omit_kv_commit
 ? 0
 : db->submit_transaction(txc->t);
 _txc_applied_kv(txc);
 }
 
 // 處理 deferred_stable_queue
 for (auto b : deferred_stable) {
 for (auto &txc : b->txcs) {
 get_deferred_key(wt.seq, &key);
 synct->rm_single_key(PREFIX_DEFERRED, key);
 }
 }
 
 // submit synct synchronously (block and wait for it to commit)
 // 同步kv,有設定bluefs_extents、刪除wal兩種操作
 int r = cct->_conf->bluestore_debug_omit_kv_commit
 ? 0
 : db->submit_transaction_sync(synct);
 
 // 放入finalize執行緒佇列,並通知其處理。
 std::unique_lock<std::mutex> m(kv_finalize_lock);
 kv_committing_to_finalize.swap(kv_committing);
 deferred_stable_to_finalize.swap(deferred_stable);
 kv_finalize_cond.notify_one();
 }
}


涉及延遲指標state_kv_queued_latstate_kv_committing_latkv_lat

kv_finalize_thread

清理執行緒,包含兩個佇列:

  • kv_committing_to_finalize:再次呼叫_txc_state_proc進入狀態機,設定狀態為STATE_KV_DONE,並執行回撥函式通知使用者io操作完成。
  • deferred_stable_to_finalize:遍歷deferred_stable中的dbh,呼叫_txc_state_proc進入狀態機,設定狀態為STATE_FINISHING,繼續呼叫_txc_finish,設定狀態為STATE_DONE,狀態機結束,事物完成。
void BlueStore::_kv_finalize_thread() {
 while (true) {
 // 交換指標
 kv_committed.swap(kv_committing_to_finalize);
 deferred_stable.swap(deferred_stable_to_finalize);
 
 // 處理kv_committing_to_finalize佇列
 while (!kv_committed.empty()) {
 TransContext *txc = kv_committed.front();
 _txc_state_proc(txc);
 kv_committed.pop_front();
 }
 
 // 處理deferred_stable_to_finalize
 for (auto b : deferred_stable) {
 auto p = b->txcs.begin();
 while (p != b->txcs.end()) {
 TransContext *txc = &*p;
 p = b->txcs.erase(p); // unlink here because
 _txc_state_proc(txc); // this may destroy txc
 }
 delete b;
 }
 deferred_stable.clear();
 }
}


涉及延遲指標state_deferred_cleanup_latstate_finishing_lat

IO狀態

主要分為SimpleWrite、DeferredWrite、SimpleWrite+DeferredWrite。

已有文章寫的不錯,這裡便不再詳細描述(寫文章+畫圖真是體力活: ),可參見: Ceph BlueStore Write Type章節。

最後YY

BlueStore原始碼分析的系列文章就到此結束了,也算是學習BlueStore的一個總結。貌似還差BlueFS的分析,已有大神寫出 BlueStore-先進的使用者態檔案系統《二》-BlueFS

由於個人水平有限,對BlueStore也僅限於粗淺的理解,如有錯誤,還請指出。

參考資源

作者:史明亞

為研發提效,全是技術乾貨的滴滴雲技術沙龍報名中!

馬上關注滴滴雲公眾號:

回覆「上課」獲取免費報名資格

回覆「伺服器」免費獲得雲伺服器入門1個月體驗


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559758/viewspace-2690750/,如需轉載,請註明出處,否則將追究法律責任。

相關文章