Floyd&Raft的原始碼分析(三)

weixin_33890499發表於2018-04-15

這篇是這個系列的最後一篇了,整個分析持續了兩個月多,包括floyd和pink原始碼,差不多就每個週日會花幾個小時分析一下,和查些資料,畢竟自己也是在學習過程中,可能會有些理解不正確的地方。
在學習raft的過程中,看了好些文章和別人的分析,理解起來似乎比較容易,但如果是自己從零實現一個可能要花些時間,況且不會靈活的運用到具體專案中,和結合現有的開源專案,比如leveldb和rocksdb等。
這篇結束後,準備分析下leveldb裡的一些實現,可能網上已經有很多型別的文章,但看別人分析還不如自己深入原始碼,以前也做過類似的事情,比如redis和leveldb,但時間較久也沒有沉澱下來,所以也陌生了。計劃是後兩個月分析下libco/pebble裡的協程實現,後面大半年就分析單機版的kv儲存如leveldb和訊息中介軟體,比較傾向於rocketmq或訊息佇列phxqueue。

先列一下floyd框架,再分析下raft節點增加和減少的情況。
floyd是單程式多執行緒的框架專案,就直接上程式碼了,不畫流程圖和類圖了。看了下example程式,在main中例項化一個floyd物件,如下:

368 Status Floyd::Open(const Options& options, Floyd** floyd) {
369   *floyd = NULL;
370   Status s;
371   FloydImpl *impl = new FloydImpl(options);
372   s = impl->Init();
373   if (s.ok()) {
374     *floyd = impl;
375   } else {
376     delete impl;
377   }
378   return s;
379 }

全部功能都放在Init中,部分程式碼如下:

274 Status FloydImpl::Init() {
275   slash::CreatePath(options_.path);
276   if (NewLogger(options_.path + "/LOG", &info_log_) != 0) {
277     return Status::Corruption("Open LOG failed, ", strerror(errno));
278   }
279 
280   // TODO(anan) set timeout and retry
281   worker_client_pool_ = new ClientPool(info_log_);
282 
283   // Create DB
284   rocksdb::Options options;
285   options.create_if_missing = true;
286   options.write_buffer_size = 1024 * 1024 * 1024;
287   options.max_background_flushes = 8;
288   rocksdb::Status s = rocksdb::DB::Open(options, options_.path + "/db/", &db_);
289   if (!s.ok()) {
291     return Status::Corruption("Open DB failed, " + s.ToString());
292   }
293 
294   s = rocksdb::DB::Open(options, options_.path + "/log/", &log_and_meta_);
295   if (!s.ok()) {  
297     return Status::Corruption("Open DB log_and_meta failed, " + s.ToString());
298   }
300   // Recover Context
301   raft_log_ = new RaftLog(log_and_meta_, info_log_);
302   raft_meta_ = new RaftMeta(log_and_meta_, info_log_);
303   raft_meta_->Init();
304   context_ = new FloydContext(options_);
305   context_->RecoverInit(raft_meta_);
306 
307   // Recover Members when exist
308   std::string mval;
309   Membership db_members;
310   s = db_->Get(rocksdb::ReadOptions(), kMemberConfigKey, &mval);
311   if (s.ok()
312       && db_members.ParseFromString(mval)) {
315     for (int i = 0; i < db_members.nodes_size(); i++) {
316       context_->members.insert(db_members.nodes(i));
317     }
318   } else {
319     BuildMembership(options_.members, &db_members);
320     if(!db_members.SerializeToString(&mval)) {
322       return Status::Corruption("Serialize Membership failed");
323     }
324     s = db_->Put(rocksdb::WriteOptions(), kMemberConfigKey, mval);
325     if (!s.ok()) {
327       return Status::Corruption("Record membership in db failed! error: " + s.ToString());
328     }
330     for (const auto& m : options_.members) {
331       context_->members.insert(m);
332     }
333   }
337   primary_ = new FloydPrimary(context_, &peers_, raft_meta_, options_, info_log_);
340   worker_ = new FloydWorker(options_.local_port, 1000, this);
341   int ret = 0;
342   if ((ret = worker_->Start()) != 0) {
344     return Status::Corruption("failed to start worker, return " + std::to_string(ret));
345   }
347   apply_ = new FloydApply(context_, db_, raft_meta_, raft_log_, this, info_log_);
348 
349   InitPeers();
354   if ((ret = primary_->Start()) != 0) {
356     return Status::Corruption("failed to start primary thread, return " + std::to_string(ret));
357   }
358   primary_->AddTask(kCheckLeader);
361   apply_->Start();
365   return Status::OK();
366 }

以上程式碼主要工作做了:創始日誌,建立客戶端物件池(裡面會對每個server addr建立一條連線,原始碼在pink專案中)用於後續傳送請求,建立兩個db,用於state machine和log entry;從log中恢復狀態資料比如term/voteip/voteport/commit_index/last_applied等;接著從db中或引數中恢復節點資訊;建立FloydPrimary執行緒,此時還沒啟動,它的主要工作是心跳,定時檢查leader,執行command,向其他節點傳送vote rpc或append log entry rpc,體現在這三個函式中,由AddTask分發:

 62   static void LaunchHeartBeatWrapper(void *arg);
 63   void LaunchHeartBeat();
 64   static void LaunchCheckLeaderWrapper(void *arg);
 65   void LaunchCheckLeader();
 66   static void LaunchNewCommandWrapper(void *arg);
 67   void LaunchNewCommand();

接著建立FloydWorker執行緒並啟動它,它的工作是處理請求,比如給節點執行緒發vote rpc/append log entry rpc任務,由DealMessage分發,然後根據request_type具體走不同的邏輯,呼叫FloydImpl類中的函式;
接著建立FloydApply執行緒,它的工作是執行command,即把log entry中的command apply到 state machine中,處理成員變更的情況;
接著根據節點個數,建立對應個數的節點執行緒並啟動,主要工作是向對應server傳送vote rpc/append log entry rpc請求和處理響應,維護狀態等;
最後是啟動FloydPrimary並立即發一個選舉leader的任務(節點剛啟動時都為follower),接著啟動FloydApply;

以上執行緒,有些建立便啟動,有些等其他執行緒啟動完後再啟動,這裡有個順序依賴,主要看誰驅動誰,比如FloydPrimary執行緒裡會用到節點執行緒,就不能以相反的順序start否則可能引起coredump;

大致整個框架差不多分析完了,pink中的相關原始碼沒有在這裡列出來,跳過了。

在成員變更的同時,需要保證安全必一,即“在任何時候,都不會出現雙主。”
主要有兩種方法:
One-Server變更:一階段變更,要求每次成員組從G1變成G2時,G2相比G1加一個成員或者減一個成員。
Joint Consensus:支援任意的變更,即從成員組G1變成G2,不要求G1和G2有什麼關聯,比如可以完全沒有交集。

第一種比較容易理解,實現起來也簡單,floyd中也使用的第一種,這邊大概說一下基本流程吧,至於為什麼這種方法可行,可以參考下面連結的分析;

以增加成員為例,raft在收到增加server成員請求時,每次只增加一臺server,後臺程式的Leader執行流程如下:
-->AddServer-->BuildAddServerRequest-->DoCommand-->ExecuteCommand-->BuildLogEntry-->Append-->AddTask-->NoticePeerTask-->AddAppendEntriesTask

假設經過大多數的返回後,leader 把command apply到狀態機中後,後續follower也推進apply id,把這條日誌apply 狀態機中去,此時流程如下:
-->Apply-->MembershipChange-->AddNewPeer

219 void FloydImpl::AddNewPeer(const std::string& server) {
220   if (IsSelf(server)) {
221     return;
222   } 
223   // Add Peer
224   auto peers_iter = peers_.find(server);
225   if (peers_iter == peers_.end()) {
226     LOGV(INFO_LEVEL, info_log_, "FloydImpl::ApplyAddMember server %s:%d add new peer thread %s",
227         options_.local_ip.c_str(), options_.local_port, server.c_str());
228     Peer* pt = new Peer(server, &peers_, context_, primary_, raft_meta_, raft_log_,
229         worker_client_pool_, apply_, options_, info_log_);
230     peers_.insert(std::pair<std::string, Peer*>(server, pt));
231     pt->Start();
232   }   
233 }

http://loopjump.com/raft_paper_note/
http://loopjump.com/raft_one_server_reconfiguration/

相關文章