oceanbase之RootServer(三)

瑣事太多,又太懶了,好久不想動筆,進展也比較慢。

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 entryseq 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。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章