天天看點

leveldb設計分析之log

在leveldb中log的意義是什麼?

所有的寫操作都必須先成功的append到記錄檔中,然後再更新記憶體memtable。這樣做有兩個有點:

1可以将随機的寫IO變成append,極大的提高寫磁盤速度;

2防止在節點down機導緻記憶體資料丢失,造成資料丢失,這對系統來說是個災難。

log的結構

+-------------+-------------+-------------+-------------+
| block       | block       | block       | block       |
+-------------+-------------+-------------+-------------+
           

log由連續的block組成,每個大小為固定的32kb,而每個block由連續的record組成。

+--------+
| record |
+--------+
| record |
+--------+
| record |
+--------+
           

record的内部結構如下:

4 bytes       2 bytes       1 byte
+-------------+-------------+--------------+-----------+
| crc32       | length      | log type     | data      |
+-------------+-------------+--------------+-----------+
           

log有四種類型: FULL,FIRST, MIDDLE, LAST。為什麼會有這四種類型?分開說。如果一個使用者的資料小于32kb。那麼它可以在一個block裡存放。這時,資料所在record(隻有一個)的logtype就是FULL,如果使用者資料很大,超過32kb,并且占據了好多個block。那麼第一個block中存放的資料record,就是FIRST(它其實是上次資料占用block部分空間後,剩餘部分)。中間block的所有的record都是MIDDLE類型,而最後一個block,也有一部分資料,那麼這個record就是LAST,當然它可能占不滿block,那麼剩下的空間就是下一個user data的FIRST。下面有個圖比較直覺:

leveldb設計分析之log

我們可以看出,由于record有data資料,是以它不是定長的。而且使用者角度的record跟底層log中結構中的record是不一樣的。使用者的record資料可能很大,可能跨越很多block。由于record是block内部的結構,導緻最終落實到内部結構時無法跨越block,是以導緻了分拆。

write

寫入的接口是AddRecord。

Status Writer::AddRecord(const Slice& slice)
           

在實作方面,通過leftover來跟蹤目前block内剩餘的空間,其中block_offset_在block内的偏移。當剩餘空間少于7個字元(record内部結構圖中的4+2+1)用0填充(一般是6個字元的空間)。剩下的邏輯就要其實是按block為機關大小來寫入的,一直寫直到處理完使用者送出的資料。實際的寫入動作其實委托給了EmitPhysicalRecord函數。在EmitPhysicalRecord函數内部首先會生成一個record頭部(包括前面提到的4+2+1結構),然後把使用者的payload追加到後面就可以了。

read

ReadRecord函數執行具體的讀取log的動作。在一開始需要先設定一下處理點,如果last_record_offset_小于initial_offset_,那麼需要将處理點移動到initial_offset_開始(函數SkipToInitialBlock)。在reader構造函數中,initial_offset_跟last_record_offset_都是0。那麼在什麼情況下,last_record_offset_會比initial_offset_小呢?

實際的讀取動作在ReadPhysicalRecord函數裡。在函數裡面首先處理一個情況就是buffer_.size() < kHeaderSize。該條件成立,一種情況是出現在第一次read,因為buffer_在reader的構造函數裡是初始化空的,第二種情況就是上次read是一個FULL類型的record,那麼目前讀到record後面剩餘的内容為6個0的trailer。這裡需要識别到這種情況,并回報給上層告知EOF。

除了上面的情況,那麼意味着目前讀到了一個有效的record。核心的處理就是解析record header并告知上層,同時使用Slice *result參數攜帶解析得到的payload資料。

ReadRecord接下來處理各種record類型,并依次将user data追加到scratch參數提供的記憶體中, 并在發現FIRST類型時标記處本次處理的record的位置,即last_record_offset_。

其中使用in_fragmented_record來檢測record的完整性。當發現FIRST類型的record時,該變量被置為true,那麼在任何時候碰到MIDDLE,LAST,但是in_fragmented_record卻為false的,都算是異常情況。