RocksDB系列十八:二階段提交

??本文主要講解了RocksDB中二階段提交的實現。本文總結一下共有如下幾個要點:

  • Modification of the WAL format
  • Extension of the existing transaction API
  • Modification of the write path
  • Modification of the recovery path
  • Integration with MyRocks

1、 Modification of WAL Format

??WAL包含一個或多個log文件,每個log的內容都是序列化后的WriteBatches,在執行recovery 時,WriteBatches 可以從logs種重建出來。要修改WAL的格式或者擴展其功能,只需要關注WriteBatch即可。
??WriteBatch就是Records的有序集合,這些Record主要包括Put(k,v), Merge(k,v), Delete(k), SingleDelete(k),每一個都代表了RocksDB的一種寫操作。每一個Record都有一個二進制的字符串表示。當Records 添加到WriteBatch時,他們的二進制表示也被append到WriteBatch的二進制字符串表示中。WriteBatch的二進制字符串前綴是其起始的序列號以及batch中的record 個數。每個record都會有一個column family modifier record(如果column family是default的話,可以省略)。
??可以通過擴展WriteBatch::Handler來遍歷WriteBatch并執行一些操作。MemTableInserter 就是WriteBatch::Handler的擴展,其功能就是將WriteBatch中的操作寫入到對應的 column family的MemTable中。
?? WriteBatch的邏輯形式有可能是這樣:

Sequence(0);NumRecords(3);Put(a,1);Merge(a,1);Delete(a);

??2PC的WriteBatch format還包括另外四條Records

  • Prepare(xid)
  • EndPrepare()
  • Commit(xid)
  • Rollback(xid)
    ??一個可以2PC的WriteBatch可能類似下面的邏輯:
Sequence(0);NumRecords(6);Prepare(foo);Put(a,b);Put(x,y);EndPrepare();Put(j,k);Commit(foo);

??Prepare(foo)和EndPrepare()之間的記錄是transaction (ID='foo')的操作。Commit(foo)表示提交這個transaction,Rollback(foo)表示回滾這個transaction。

Sequence ID Distribution

??當WriteBatch通過MemTableInserter被寫入到memtable時,WriteBatch中的每一個operation的sequence ID加上這個WriteBatch中的Oprator的index。但是,在2PC的WriteBatch中并沒有繼續保持這種sequence id的映射方法。Operations contained within a Prepare() enclosure will consume sequence IDs as if they were inserted starting at the location of their relative Commit() marker. This Commit() marker may be in a different WriteBatch or log from the prepared operations to which it applies.

Backwards Compatibility

??WAL format并沒有版本話,所以我們需要注意后相兼容。當前版本的RocksDB不能從一個包含2PC 標記的WAL 文件中recovery。在實際recover時,遇到不能識別的Record會打印fatal 信息。有點麻煩,但是開發者可以對當前的RocksDB版本打patch以便能夠跳過prepared sections和不能識別的markers,這樣就可以從新版本的WAL format 恢復數據。

2、Extension of Transaction API

??當前我們只focus到樂觀事物的2PC。client必須提前聲明是否使用二階段提交,例如以下代碼:

TransactionDB* db;
TransactionDB::Open(Options(), TransactionDBOptions(), "foodb", &db);

TransactionOptions txn_options;
txn_options.two_phase_commit = tr
txn_options.xid = "12345";
Transaction* txn = db->BeginTransaction(write_options, txn_options);
    
txn->Put(...);
txn->Prepare();
txn->Commit();

transaction狀態有:

enum ExecutionStatus {
  STARTED = 0,
  AWAITING_PREPARE = 1,
  PREPARED = 2,
  AWAITING_COMMIT = 3,
  COMMITED = 4,
  AWAITING_ROLLBACK = 5,
  ROLLEDBACK = 6,
  LOCKS_STOLEN = 7,
};

??transaction API會調用一個Prepare()函數。Prepare函數會通過一個context調用WriteImpl,通過context,WriteImpl和WriteThread可以訪問ExcutionStatus、XID和WriteBatch。WriteBatch會先寫入一個Prepare(xid)標記,然后寫入WriteBatch的內容,再寫入EndPrepare()標記。這期間并沒有memtable的寫入。當transaction執行了commit時,會再次調用WriteImpl。此時,Commit()標記會寫入WAL,WriteBatch的內容會寫入相應的memtable。當transaction調用Rollback()時,transaction內容會被清除,然后調用WriteImpl,寫入Rollback(xid)標記(如果當前事物處于Prepare狀態)。
??這些所謂的"meta markers"(Prepare(xid), EndPrepare(), Commit(xid), Rollback(xid))不會直接寫入到write batch中。write path (WriteImpl())會持有正在寫的事物的context,并使用這個context將相關的markers寫入到WAL(所以這些標記在寫入到WAL之前先寫入到聚合后的WriteBatch)。在recovery時,這些標記會被MemTableInserter 用來重建prepared transactions。

Transaction Wallclock Expiration

??在transaction 提交時,會有一個callback,這個callback在transaction過期后會fail掉整個寫操作。如果transaction過期了,那么鎖很容易被其他transction搶占。如果一個transaction在prepare階段沒有過期的話,那么也不可能在commit階段過期。

TransactionDB Modification

??使用transaction前,client必須打開一個TransactionDB。這個TransactionDB 實例接下來就可以創建Transactions。TransactionDB 會持有一個映射(from XID to 其創建的所有兩階段的Transaction)。當Transaction被刪除或者Rollback時,就會從mapping中刪除掉。RocksDB提供API來查詢所有正在進行中的處于Prepare狀態的transaction。
??TransactionDB 記錄著一個min heap(所有包含prepared section的log numbers)。當transaction處于prepared狀態時,WriteBatch也會寫入log,這個log number就會存儲在transaction 對象中,隨后存入到min heap。當transaction commit時,log number就會從min heap中刪除,但是log number并不會用于被遺忘掉。接下來,就是各個memtable來記錄the oldest log,直到memtable flush到L0為止。

3、Modification of the Write Path

??write path可以被拆解為兩個主要點:DBImpl::WriteImpl(...) and the MemTableInserter。多個client線程都會調用WriteImpl。第一個線程會被設定角色為 leader,剩余的線程會被設定為follower。leader和followers會被group到一起,成為一個邏輯上的write group。leader負責取出writegroup中的所有WriteBatches,聚合在一起,然后將blob寫入到WAL。結合writegroup的大小和當前內存表對并行寫的支持,leader可以將所有WriteBatches寫入到memtable,也可以由各個線程寫入線程自己負責的WrtieBatches到內存表中。
??所有的memtable inserts都是由MemTableInserter負責。 a WriteBatch iterator handler也是WriteBatch::Handler的一種實現。這個handler遍歷WriteBatch中的所有元素(Put, Delete, Merge),將每個call寫入到對應的MemTable。MemTableInserter 也會處理已就緒的merges, deletes and updates。
??Modification of the write path需要傳入一個參數到DBImpl::WriteImpl,這個參數是一個指針,指向一個2PC的transaction實例。通過這個實例,可以查詢到二階段transaction的當前狀態。一個2PC transaction會在preparation、commit和roll-back時各調用一次WriteImpl 。

Status DBImpl::WriteImpl(
  const WriteOptions& write_options, 
  WriteBatch* my_batch,
  WriteCallback* callback,
  Transaction* txn
) {
  WriteThread::Writer w;
  //...
  w.txn = txn; // writethreads also have txn context for memtable insert

  // we are now the group leader
  int total_count = 0;
  uint64_t total_byte_size = 0;
  for (auto writer : write_group) {
    if (writer->CheckCallback(this)) {
      if (writer->ShouldWriteToMem())
        total_count += WriteBatchInternal::Count(writer->batch)
       }
  }
  const SequenceNumber current_sequence = last_sequence + 1;
  last_sequence += total_count;

  // now we produce the WAL entry from our write group
  for (auto writer : write_group) {
    // currently only optimistic transactions use callbacks
    // and optimistic transaction do not support 2pc
   if (writer->CallbackFailed()) {
      continue;
    } else if (writer->IsCommitPhase()) {
      WriteBatchInternal::MarkCommit(merged_batch, writer->txn->XID_);
    } else if (writer->IsRollbackPhase()) {
      WriteBatchInternal::MarkRollback(merged_batch, writer->txn->XID_);
    } else if (writer->IsPreparePhase()) {
      WriteBatchInternal::MarkBeginPrepare(merged_batch, writer->txn->XID_);
      WriteBatchInternal::Append(merged_batch, writer->batch);
      WriteBatchInternal::MarkEndPrepare(merged_batch);
      writer->txn->log_number_ = logfile_number_;
    } else {
      assert(writer->ShouldWriteToMem());
      WriteBatchInternal::Append(merged_batch, writer->batch);
    }
  }
  //now do MemTable Inserts for WriteGroup
}

WriteBatchInternal::InsertInto也可以調整為只遍歷沒有相關聯的Transaction 或處于COMMIT狀態的寫。由上述代碼可以看出,當transaction處于prepared狀態時,transaction會記錄log num。在insert時,每個Memtable都會記錄最小的log number。

4、Modification of Recovery Path

??當前的recovery path已經很好地適配了兩階段提交,按照順序,依次遍歷log中的所有batches,按照log number 依次feed到MemTableInserter。MemTableInserter 會遍歷所有的batches,然后將值寫入到正確的MemTable中。基于當前的log number,每個MemTable知道該忽略掉哪些values。
??要想recovery 時可以處理2PC的一些操作,我們需要擴展MemTableInserter ,使其感知到4個新的meta markers。
??需要記住的是:當2PC transaction commit時,就會包含一些操作在多個CF上的insertions。這些MemTable是在不同的時間點上執行flush。我們仍然可以使用CF的log number,在recovered, two phase, committed transaction時避免重復寫入。
1、Two Phase Transactions TXN inserts into CFA and CFB
2、TXN prepared to LOG 1
3、TXN marked as COMMITTED in LOG 2
4、TXN is inserted into MemTables
5、CFA is flushed to L0
6、CFA log_number is now LOG 3
7、CFB has not been flushed and it still referencing LOG 1 prep section
8、CRASH RECOVERY
9、LOG 1 is still around because CFB was referencing LOG 1 prep section
10、Iterate over logs starting at LOG 1
11、CFB has prepared values reinserted into mem, again referencing LOG 1 prep section
12、CFA skips insertion from commit marker in LOG 2 because it is 13、consistent to LOG 3
13、CFB is flushed to L0 and is now consistent to LOG 3
14、LOG 1, LOG 2 can now be released

Rebuilding Transactions

??如上所述,modification of the recovery path只需修改MemTableInserter ,使其可以handle 新的meta-markers即可。在recovery時,我們不能訪問TransactionDB的實例,我們必須重建一個hollow ‘shill’的transaction。這就是所有recovered prepared transactions的衣蛾mapping(XID → (WriteBatch, log_number))。當遇到一個Commit(xid) marker時,就會嘗試查找對應xid的shill transaction,然后寫入到Mem。如果遇到一個rollback(xid) marker,我們就會delete 這個shill transaction。recovery末期,以shill的形式剩下一個所有處于Prepared狀態的transaction。

log lifespan

??要想知道最小的log,我們必須找到每個CF的最小的log number。我們也需要考慮TransactionDB的prepared sections heap中的最小value。這代表了最早的log(包含一個還沒有提交的prepared section)。我們也需要考慮all MemTables和沒有flush的ImmutableMemTables 的最小prep section。這三種value的最小值就是含有數據但是還沒有flush到L0的最早的log。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容