Floyd&Raft的原始碼分析(三)
這篇是這個系列的最後一篇了,整個分析持續了兩個月多,包括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/
相關文章
- Retrofit原始碼分析三 原始碼分析原始碼
- preact原始碼分析(三)React原始碼
- 5.2 spring5原始碼--spring AOP原始碼分析三---切面原始碼分析Spring原始碼
- 原始碼分析三:OkHttp—BridgeInterceptor原始碼HTTP
- 原始碼分析三:OkHttp—CacheInterceptor原始碼HTTP
- 原始碼分析三:OkHttp—ConnectInterceptor原始碼HTTP
- 原始碼分析三:OkHttp—CallServerInterceptor原始碼HTTPServer
- 原始碼分析三:OkHttp—RetryAndFollowUpInterceptor原始碼HTTP
- Spring原始碼分析(三) -- Spring中的BeanFactoryPostProcessorSpring原始碼Bean
- Vue原始碼分析系列三:renderVue原始碼
- Netty原始碼分析--建立Channel(三)Netty原始碼
- containerd 原始碼分析:建立 container(三)AI原始碼
- java集合原始碼分析(三):ArrayListJava原始碼
- SpringBoot2.0原始碼分析(三):整合RabbitMQ分析Spring Boot原始碼MQ
- Android主流三方庫原始碼分析(三、深入理解Glide原始碼)Android原始碼IDE
- gson-plugin深入原始碼分析(三)Plugin原始碼
- jQuery原始碼剖析(三) - Callbacks 原理分析jQuery原始碼
- weex原始碼分析(三) -- weex工程建立原始碼
- Flutter Dio原始碼分析(三)--深度剖析Flutter原始碼
- 友好 RxJava2.x 原始碼解析(三)zip 原始碼分析RxJava原始碼
- Dubbo原始碼分析(三)Dubbo的服務引用Refer原始碼
- Netty原始碼分析之NioEventLoop(三)—NioEventLoop的執行Netty原始碼OOP
- 容器類原始碼解析系列(三)—— HashMap 原始碼分析(最新版)原始碼HashMap
- tomcat原始碼分析(第三篇 tomcat請求原理解析--Connector原始碼分析)Tomcat原始碼
- Giraph原始碼分析(三)—— 訊息通訊原始碼
- JVM 原始碼分析(三):深入理解 CASJVM原始碼
- Java IO原始碼分析(三)——PipedOutputStream和PipedInputStreamJava原始碼
- Spark RPC框架原始碼分析(三)Spark心跳機制分析SparkRPC框架原始碼
- 基於Android5.0的Camera Framework原始碼分析 (三)AndroidFramework原始碼
- 集合原始碼分析[2]-AbstractList 原始碼分析原始碼
- 集合原始碼分析[3]-ArrayList 原始碼分析原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 集合原始碼分析[1]-Collection 原始碼分析原始碼
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- Shading – jdbc 原始碼分析(三) – sql 解析之 SelectJDBC原始碼SQL
- Netty原始碼分析(三):客戶端啟動Netty原始碼客戶端
- 原始碼分析三:OkHttp(1)—總體架構原始碼HTTP架構