瑣事太多,又太懶了,好久不想動筆,進展也比較慢。
5 日誌系統
有偉大的GFS作指引,OceanBase的master也是採用redo log加checkpoint機制,以保證master的響應速度。此外root server採取了主備機制,因此redo log一寫兩份, 在flush redolog時先寫slave,成功了才寫master並重置log的內存緩存區。
日誌系統是比較核心的一塊,這塊的體量也不小。慢慢看吧。
5.1 類圖
首先來一張類圖,列示日誌系統涉及到的類,顯然我們需要從ObRootLogManager這個類入手。
先來整理這幾個類之間的關係,ObRootLogWorker向Root Server提供了寫入各種系統log的接口,比如registerchunk server的regist_cs(),register merge server的regist_ms(),增加tablet的add_new_tablet()等等。
ObRootLogManager,如其名,日誌管理器。主要就是日誌系統初始化和回放log,以及做checkpoint,管理log目錄等信息。
ObRootLogManager又繼承了ObLogWriter,ObLogWriter是寫log file的底層類。所以ObRootLogWorker寫log的接口如regist_cs(),最終都是通過調用ObRootLogManager完成的,它看不到ObLogWriter。這種設計風格因人而異,最終結果大同小異。
5.2 LogManager初始化
接着上面master啓動的流程,看看日誌系統初始化和replay log的邏輯。初始化流程相對簡單。Logmanager會在initialize時調用OpLogDirScanner掃描日誌目錄,取得log id和checkpoint id。
5.2.1 Scanner掃描
另外,log id和checkpoint id都是數字。Scanner將log id排序,得到最小和最大的log id,以及最大的checkpoint id,作爲狀態基點。
Scnner從最大id向前檢查,如果碰到不連續的id,則跳出;
1 如果log文件集合爲空,則設置max min log id爲0,返回OK;
2 log文件id連續,最正常的情況,返回OK;
3 log文件id不連續,舉個例子:2,3,5,6,7,8;那麼此時在5處開始不連續了,min log id就等於5;並返回OB_DISCONTINUOUS_LOG。
5.2.2 獲取掃描狀態
1沒有checkpoint
設置replay_point_ = 1;如果log爲空,則設置max_log_id_ = 1;否則根據結果取出max log id,scanner.get_max_log_id(max_log_id_);
2 有checkpoint
scanner.get_max_ckpt_id(ckpt_id_),合法性檢查,如果min log id > ckpt_id_+1,則表明有錯誤;
設置replay_point_ = ckpt_id_ + 1;
後面從checkpoint恢復後,master還將從id爲replay_point_的log開始回放日誌,以到達最終狀態。
5.3 LogManager回放日誌
5.3.1 回放邏輯
初始化之後,緊接着就是replay 日誌了,這又有幾個步驟。
1 從checkpoint文件讀取server 狀態,文件的第一個32byte是server的狀態字段。
load_server_status()
2 執行成功就調用ObRootServer2接口從最近的checkpoint恢復狀態
root_server_->recover_from_check_point(rt_server_status_,ckpt_id_)這塊邏輯比較多,後面再詳細分析。
3 恢復成功後,就調用do_after_recover_check_point(),它就是啓動root_server_,並等待root_server_初始化完成。
4 調用ObLogReader從replay_point_開始讀取日誌並回放。
從這裏也可以看明白了,checkpoint和log的id號都是嚴格按照事件順序生產的。如果checkpoint的id爲5,那麼表明所有id>5的log都是在checkpoint之後生產的,其操作的結果都在checkpoint之後。
接下來看看日誌回放的邏輯,這需要藉助於ObLogReader完成redo log的讀取。在當前log 讀完後,ObLogReader會自動打開並讀取下一個log文件(log id + 1)。
log_reader.init(log_dir_,replay_point_, 0, false);
log_reader.read_log(cmd,seq, log_data, log_length);
主要就是循環讀取,每讀到一條log entry,就調用log worker回放日誌。
while (ret == OB_SUCCESS) {
set_cur_log_seq(seq);
if (OB_LOG_NOP != cmd)
ret = log_worker_.apply(cmd, log_data, log_length);
ret = log_reader.read_log(cmd,seq, log_data, log_length);
} // end while
// handle exception, when the last log filecontain SWITCH_LOG entry
// but the next log file is missing
if(OB_FILE_NOT_EXIST == ret) {
max_log_id_++;
ret = OB_SUCCESS;
}
// reach the end ofcommit log
if(OB_READ_NOTHING == ret) ret = OB_SUCCESS;
這兩個函數都很簡單,來看原型:
init(constchar* log_dir, const uint64_t log_file_id_start,
const uint64_t log_seq,boolis_wait)
read_log(LogCommand &cmd,uint64_t &seq, char* &log_data, int64_t &data_len)
其中seq表明從哪一條log開始讀,read_log會把讀取的log entry的seq賦予參數seq傳出來;參數cmd是log類型。
日誌回放的主體邏輯就這麼多,不過我們還欠了不少功課,包括:
1 日誌文件格式和讀取;
2負責日誌回放的類ObRootLogWorker;
3 從checkpoint恢復狀態;
5.3.2 完整日誌文件的結束標誌
OB_LOG_SWITCH_LOG這條日誌用來標記一個完整日誌文件的結尾,在ObRootLogManager的讀取回放循環中,如果最後一條記錄不是OB_LOG_SWITCH_LOG,將會被認爲是最後一個日誌文件,read_log返回OB_READ_NOTHING,於是循環停止,Root server認爲回放完成;
如果遇到OB_LOG_SWITCH_LOG記錄,read_log函數會自動切換到下一個日誌文件,並嘗試讀取;
5.4 日誌log格式&讀取&校驗
實際上真正的log文件讀取由ObLogReader通過調用ObSingleLogReader完成,一個注意點就是它使用的是dio的文件接口,這個類的主要操作邏輯在read_log和read_log_兩個函數裏。
5.4.1 日誌格式
先來看看log的格式,首先是logheader,長度是256byte,分別是:
int16_t magic_; // magicnumber,固定值
int16_t header_length_; // header length= sizeof(log header struct)
int16_t version_; // version
int16_t header_checksum_;// header checksum
int64_t reserved_; //reserved,must be 0
int32_t data_length_; // lengthbefore compress
int32_t data_zlength_; // length aftercompress, if without compresssion
// data_length_=data_zlength_
int64_t data_checksum_; // record checksum
其後是
int64_tseq // 本條log entry的seq no
int32_tcmd;// 本條log entry的命令類型
最後就是log entry的內容了。
5.4.2 讀取log entry和校驗
前面講過主要邏輯在ObSingleLogReader的read_log和read_log_中,簡單點來說就是讀取文件內容到一個循環使用的buffer中,然後調用OpLogEntry的deserialize()接口反向序列log header、seq和cmd;如果序列化失敗,說明內容不是一個完整的entry header,繼續讀取文件內容到buffer中,再次嘗試。
每次調用都期望讀取到buffer可用大小的數據。
代碼實現並沒有壓縮log,因此接下來的data_zlength – sizeof(cmd) –sizeof(seq)就是實際的log內容。
讀取到一條完整的log entry就返回,並做正確性檢查。反序列化、檢查邏輯都在類ObLogEntry中。
這兩個函數被ObLogReader調用,順便來看看相關的函數。
intObLogReader::read_log(LogCommand &cmd, uint64_t &seq,
char* &log_data, int64_t&data_len)
該函數嘗試讀取log entry,如果遇到OB_LOG_SWITCH_LOG日誌,則自動切換到下一個log文件,嘗試讀取。邏輯如下,這裏省去了錯誤處理邏輯。
ret = read_log_(cmd, seq,log_data, data_len);
while (OB_SUCCESS == ret&& OB_LOG_SWITCH_LOG == cmd) {
ret = log_file_reader_.close();
cur_log_file_id_ ++;
ret = open_log_(cur_log_file_id_, seq);
ret = log_file_reader_.read_log(cmd, seq,log_data, data_len);
}
而其中的read_log_,則是直接調用ObSingleLogReader的read_log()函數。
5.4.3 校驗
正確性檢查就是分別對header和body做校驗和,校驗和在創建填充log entry的時候生成。
其中header的校驗和借鑑了TCP的做法,是header各個字段每16byte做XOR運算的結果;body的校驗和是CRC64,先對seq做CRC64,得到的結果和cmd做CRC64,得到的結果再和log data做CRC64運算。
5.5 ObRootLogWorker概覽
如前面所示,主要調用入口就是
apply(common::LogCommand cmd,const char* log_data, const int64_t& data_len)
該函數根據不同的cmd執行不同的處理流程,
所有的具體處理流程都在對應的do_xxx系列函數中,這些函數被定義爲public,而實際上,只會在本類內調用。
具體處理流程還應該在相應的場景中分析才能理清上下文,拋開整體流程,孤立的分析處理流程是不合適的。
ObRootLogWorker對日誌的回放最終都是調用ObRootServer2 root_server_這個成員變量完成的。
有必要先對LogCommand做一個瞭解。日誌類型定義在LogCommand中,文件common/ob_log_entry.h中,這是一個enum類型。摘錄幾個類型:
OB_LOG_SWITCH_LOG = 101, //日誌切換命令
OB_LOG_CHECKPOINT = 102, //checkpoint操作
OB_RT_CS_REGIST = 401, // chunk server註冊
OB_RT_ADD_NEW_TABLET = 409, // 添加新的tablet
文件定義了超過20種日誌類型,遇到時再具體分析。
5.6 寫入日誌
在後面分析root server時將會看到,最終所有類型的日誌都是通過flush_log函數寫入的。其函數原型爲:
intObRootLogWorker::flush_log(const LogCommand cmd, const char* log_data,
const int64_t& serialize_size)
函數flush_log,就兩行代碼:加鎖,調用log manager flush flog。
tbsys::CThreadGuardguard(log_manager_->get_log_sync_mutex());
log_manager_->write_and_flush_log(cmd,log_data, serialize_size);
函數write_and_flush_log繼承自ObLogWriter,來看看函數實現。
ret = write_log(cmd, log_data,data_len);
ret = flush_log();
在write_log中,如果當前日誌文件大小超過了設置的值,將會觸發switch log操作,切換日誌文件。
切換日誌文件時,首先調用flush_log(),刷新當前日誌;然後生產一條SWITCH的日誌記錄,再次flush當前日誌;類型爲OB_LOG_SWITCH_LOG,並且日誌seq等於當前seq,而其它日誌seq都是當前seq+1,遞增的。OB_LOG_SWITCH_LOG這條日誌用來標記一個完整日誌文件的結尾。
最後打開新的日誌文件,文件id等於當前文件id+1。
接着再看flush_log()這個函數,基本邏輯就4行代碼。
ret = serialize_nop_log_();
ret =slave_mgr_->send_data(log_buffer_.get_data(), log_buffer_.get_position());
ret = store_log(log_buffer_.get_data(),log_buffer_.get_position());
if (OB_SUCCESS == ret) log_buffer_.get_position() = 0;
1 函數serialize_nop_log_是幹什麼的呢,還記得前面說過oceanbase使用的是dio文件嗎,因此每次寫入必須保證是對齊的。如果沒有對齊,就構造一個OB_LOG_NOP類型的日誌,對其到邊界。
2 向slave發送日誌,比較土;多個slave,就依次發送,由ObSlaveMgr完成。ObSlaveMgr很簡單,管理註冊的salve server,後面單獨講講。
3 只要有一部分slave發送成功了,就調用store_log,以append方式追加寫入文件,寫入成功就清空buffer。