Braft的日志存儲(chǔ)引擎實(shí)現(xiàn)分析
1.架構(gòu)設(shè)計(jì)
1.1 函數(shù)接口說明
日志存儲(chǔ)引擎是用于存儲(chǔ)raft lib產(chǎn)生的日志。提供的接口如下:
class LogStorage {
public:
virtual ~LogStorage() {}
// init logstorage, check consistency and integrity
virtual int init(ConfigurationManager* configuration_manager) = 0;
// first log index in log
virtual int64_t first_log_index() = 0;
// last log index in log
virtual int64_t last_log_index() = 0;
// get logentry by index
virtual LogEntry* get_entry(const int64_t index) = 0;
// get logentry's term by index
virtual int64_t get_term(const int64_t index) = 0;
// append entries to log
virtual int append_entry(const LogEntry* entry) = 0;
// append entries to log, return append success number
virtual int append_entries(const std::vector<LogEntry*>& entries) = 0;
// delete logs from storage's head, [first_log_index, first_index_kept) will be discarded
virtual int truncate_prefix(const int64_t first_index_kept) = 0;
// delete uncommitted logs from storage's tail, (last_index_kept, last_log_index] will be discarded
virtual int truncate_suffix(const int64_t last_index_kept) = 0;
// Drop all the existing logs and reset next log index to |next_log_index|.
// This function is called after installing snapshot from leader
virtual int reset(const int64_t next_log_index) = 0;
// Create an instance of this kind of LogStorage with the parameters encoded
// in |uri|
// Return the address referenced to the instance on success, NULL otherwise.
virtual LogStorage* new_instance(const std::string& uri) const = 0;
static LogStorage* create(const std::string& uri);
};
LogStorage只是一個(gè)抽象類,只定義了函數(shù)接口。具體的日志操作由SegmentLogStorage實(shí)現(xiàn)。
1.2 存儲(chǔ)引擎的數(shù)據(jù)組織
SegmentLogStorage實(shí)現(xiàn)了LogStorage的全部接口。其數(shù)據(jù)組織格式如下:

- segment名字為first_raft_index-last_raft_index,表示該segment的raft index范圍。
- 只有最后一個(gè)segment可讀寫,其文件名為log_inprogress_first_raft_index,其他segment只讀。
- segment文件對(duì)應(yīng)的index entry,Segment文件初始化時(shí)構(gòu)造出來,存儲(chǔ)在內(nèi)存中。不會(huì)持久化到磁盤。因此追加一次Log Entry只會(huì)引起一次磁盤操作。
2.核心流程實(shí)現(xiàn)
2.1 存儲(chǔ)引擎的接口函數(shù)
2.2 存儲(chǔ)引擎的初始化
存儲(chǔ)引擎的初始化操作主要檢查文件信息,將segment的索引信息加載到內(nèi)存,為讀寫操作做準(zhǔn)備。

函數(shù)主要功能如下所述:
- init函數(shù)是SegmentLogStorage初始化的入口函數(shù),調(diào)用load_meta函數(shù),list_segment函數(shù)和load_segment函數(shù)。
- load_meta函數(shù):從log_meta文件中讀取從SegmentLogStorage的第一個(gè)raft index值。
- list_segment函數(shù):建立起segment的范圍信息,并將范圍異常的segment文件刪除。范圍信息存儲(chǔ)在一個(gè)map表中,map的key是first_raft_index,value是segment對(duì)象。
- load_segments函數(shù):構(gòu)建出每個(gè)segment對(duì)應(yīng)的索引項(xiàng),通過解析segement內(nèi)容完成。索引項(xiàng)存儲(chǔ)在一個(gè)vector中。至此,就可以根據(jù)范圍信息來定位到某個(gè)raft_index對(duì)應(yīng)的文件偏移。
2.3 寫數(shù)據(jù)流程
寫數(shù)據(jù)到存儲(chǔ)引擎,會(huì)涉及到兩個(gè)函數(shù):
// append entry to log
int append_entry(const LogEntry* entry);
// append entries to log, return success append number
int append_entries(const std::vector<LogEntry*>& entries);
append_entry表示追加單條Log Entry到日志存儲(chǔ)引擎,append_entries用于同時(shí)追加多條Log Entry到日志存儲(chǔ)引擎。兩個(gè)函數(shù)主要流程相差不大,我們以append_entries為例,分析一下寫入Log Entry的主要流程。函數(shù)流程圖如下所示:

- 檢查日志連續(xù)性:主要檢查last_raft_index 是否和追加的Log Entry保持連續(xù)。
- 獲取Last_Segment:檢查last_segment是否超過Max_Segment_Size,如果超過則進(jìn)行rolling操作(保存最后一個(gè)segment,并生成一個(gè)新的segment)。如果文件大小未超過Max_Segment_Size,則直接返回。
- 循環(huán)追加日志:追加Log Entry到文件末尾。
- Last_Segment強(qiáng)制刷盤:調(diào)用fsync函數(shù)強(qiáng)制刷盤。
2.4 讀數(shù)據(jù)流程
根據(jù)raft_index讀取對(duì)應(yīng)的raft Log,根據(jù)我們前面提到的索引信息,braft很容易實(shí)現(xiàn),流程圖如下所示:

get_entry是入口函數(shù),get_segment函數(shù)主要是通過raft_index來定位到segment,通過之前建立的Map范圍信息很容易定位到。然后根據(jù)每個(gè)segment的Vector索引數(shù)組,定位到raft_index對(duì)應(yīng)的文件偏移信息。然后讀取文件。
2.5 刪除數(shù)據(jù)流程
刪除數(shù)據(jù)分為兩類:
- 從前往后刪除,對(duì)應(yīng)的函數(shù)是:
SegmentLogStorage::truncate_prefix(const int64_t first_index_kept)
truncate_prefix函數(shù)先將first_index_kept保存到Log_meta文件中,這樣保證了即使后續(xù)的文件刪除操作失敗時(shí),也可以知道整個(gè)日志的起始raft_index是多少。保存完first_index_kept之后,將first_index_kept之前的segment文件全部刪除。
- 從后往前刪除,對(duì)應(yīng)的函數(shù)是:
int SegmentLogStorage::truncate_suffix(const int64_t last_index_kept)
主要用于raft lib中刪除未達(dá)成一致的Log Entry。根據(jù)last_index_kept找到對(duì)應(yīng)的文件偏移,然后截?cái)辔募?。如果跨文件,還需要?jiǎng)h除最后一個(gè)segment文件,然后再截?cái)嘀耙粋€(gè)segment的內(nèi)容。
3.測試
在test/test_log.cpp文件中,包含SegmentLogStorage類中主要的接口函數(shù)的單元測試,對(duì)理解SegmentLogStorage有比較大的幫助。
4.總結(jié)
Braft的日志存儲(chǔ)引擎,主要用于存儲(chǔ)raft log。當(dāng)執(zhí)行完一次snapshot操作后,就可以進(jìn)行Log Compaction。將snapshot之前的raft log全部刪除。這使得Braft可以將Log的索引信息全部存儲(chǔ)在內(nèi)存中,因?yàn)榇鎯?chǔ)引擎中的Raft Log Entry不會(huì)太大。這樣追加或讀取Raft Log只需要一次磁盤操作,性能方面有保證。