SOFAJRaft原始碼閱讀-模組啟動過程

yuan發表於2023-01-22

本篇文章旨在分析SOFAJRaft中jraft-example模組的啟動過程,由於SOFAJRaft在持續開源的過程中,所以無法保證示例程式碼永遠是最新的,要是有較大的變動或者紕漏、錯誤的地方,歡迎大家留言討論。
@Author:Akai-yuan
更新時間:2023年1月20日

SOFAJRaft-Start Our Journey

1. 開門見山:main方法概覽

public static void main(final String[] args) throws IOException {
    if (args.length != 4) {
        System.out
            .println("Usage : java com.alipay.sofa.jraft.example.counter.CounterServer {dataPath} {groupId} {serverId} {initConf}");
        System.out
            .println("Example: java com.alipay.sofa.jraft.example.counter.CounterServer /tmp/server1 counter 127.0.0.1:8081 127.0.0.1:8081,127.0.0.1:8082,127.0.0.1:8083");
        System.exit(1);
    }
    //日誌儲存路徑
    final String dataPath = args[0];
    //SOFAJRaft叢集的名字
    final String groupId = args[1];
    //當前節點的ip和埠
    final String serverIdStr = args[2];
    //叢集節點的ip和埠
    final String initConfStr = args[3];

    final NodeOptions nodeOptions = new NodeOptions();
    // for test, modify some params
    // 設定選舉超時時間為 1 秒
    nodeOptions.setElectionTimeoutMs(1000);
    // 關閉 CLI 服務
    nodeOptions.setDisableCli(false);
    // 每隔30秒做一次 snapshot
    nodeOptions.setSnapshotIntervalSecs(30);
    // 解析引數
    final PeerId serverId = new PeerId();
    if (!serverId.parse(serverIdStr)) {
        throw new IllegalArgumentException("Fail to parse serverId:" + serverIdStr);
    }
    final Configuration initConf = new Configuration();
    //將raft分組加入到Configuration的peers陣列中
    if (!initConf.parse(initConfStr)) {
        throw new IllegalArgumentException("Fail to parse initConf:" + initConfStr);
    }
    // 設定初始叢集配置
    nodeOptions.setInitialConf(initConf);

    // 啟動raft server
    final CounterServer counterServer = new CounterServer(dataPath, groupId, serverId, nodeOptions);
    System.out.println("Started counter server at port:"
                       + counterServer.getNode().getNodeId().getPeerId().getPort());
    // GrpcServer need block to prevent process exit
    CounterGrpcHelper.blockUntilShutdown();
}

我們在啟動CounterServer的main方法的時候,會將傳入的String[]型別引數args分別轉化為日誌儲存的路徑、SOFAJRaft叢集的名字、當前節點的ip和埠、叢集節點的ip和埠,並設值到NodeOptions中,作為當前節點啟動的引數。

2. 物件轉換:建立PeerId

引子:在main方法中,我們可以看到,程式將String型別引數轉換成了PeerId物件,那麼接下來我們需要探究轉換的具體過程。

在轉換當前節點並初始化為一個PeerId物件的過程中,呼叫了PeerId中的parse方法:

public boolean parse(final String s) {
    if (StringUtils.isEmpty(s)) {
        return false;
    }

    final String[] tmps = Utils.parsePeerId(s);
    if (tmps.length < 2 || tmps.length > 4) {
        return false;
    }
    try {
        final int port = Integer.parseInt(tmps[1]);
        this.endpoint = new Endpoint(tmps[0], port);
        switch (tmps.length) {
            case 3:
                this.idx = Integer.parseInt(tmps[2]);
                break;
            case 4:
                if (tmps[2].equals("")) {
                    this.idx = 0;
                } else {
                    this.idx = Integer.parseInt(tmps[2]);
                }
                this.priority = Integer.parseInt(tmps[3]);
                break;
            default:
                break;
        }
        this.str = null;
        return true;
    } catch (final Exception e) {
        LOG.error("Parse peer from string failed: {}.", s, e);
        return false;
    }
}

該方法內部又呼叫了工具類Utils.parsePeerId,最終達到的效果如下:
其中,a、b分別對應IP和Port埠號,組成了PeerId的EndPoint屬性;c指代idx【同一地址中的索引,預設值為0】;d指代priority優先順序【節點的本地優先順序值,如果節點不支援優先順序選擇,則該值為-1】。

PeerId.parse("a:b")          = new PeerId("a", "b", 0 , -1)
PeerId.parse("a:b:c")        = new PeerId("a", "b", "c", -1)
PeerId.parse("a:b::d")       = new PeerId("a", "b", 0, "d")
PeerId.parse("a:b:c:d")      = new PeerId("a", "b", "c", "d")

3. 漸入佳境:構造CountServer

引子:在main方法中,我們可以看到,進行初步的引數解析後,呼叫了CountServer的構造器,要說這個構造器,第一次看裡面的步驟確實會感覺挺複雜的,接下來我們一起分析一下原始碼。

CountServer構造器的原始碼如下:

public CounterServer(final String dataPath, final String groupId, final PeerId serverId,
                         final NodeOptions nodeOptions) throws IOException {
        // 初始化raft data path, 它包含日誌、後設資料、快照
        FileUtils.forceMkdir(new File(dataPath));

        // 這裡讓 raft RPC 和業務 RPC 使用同一個 RPC server, 通常也可以分開
        final RpcServer rpcServer = RaftRpcServerFactory.createRaftRpcServer(serverId.getEndpoint());
        // GrpcServer need init marshaller
        CounterGrpcHelper.initGRpc();
        CounterGrpcHelper.setRpcServer(rpcServer);

        // 註冊業務處理器
        CounterService counterService = new CounterServiceImpl(this);
        rpcServer.registerProcessor(new GetValueRequestProcessor(counterService));
        rpcServer.registerProcessor(new IncrementAndGetRequestProcessor(counterService));
        // 初始化狀態機
        this.fsm = new CounterStateMachine();
        // 設定狀態機到啟動引數
        nodeOptions.setFsm(this.fsm);
       // 設定儲存路徑 (包含日誌、後設資料、快照)
        // 日誌(必須)
        nodeOptions.setLogUri(dataPath + File.separator + "log");
        // 後設資料(必須)
        nodeOptions.setRaftMetaUri(dataPath + File.separator + "raft_meta");
        // 快照(可選, 一般都推薦)
        nodeOptions.setSnapshotUri(dataPath + File.separator + "snapshot");
        // 初始化 raft group 服務框架
        this.raftGroupService = new RaftGroupService(groupId, serverId, nodeOptions, rpcServer);
        // 啟動
        this.node = this.raftGroupService.start();
    }

接下來仔細說說CountServer的構造器裡面具體做了什麼。

4. 追根溯源:RpcServer

引子:CountServer構造器中呼叫的RaftRpcServerFactory.createRaftRpcServer()方法,底層到底是如何構造出一個RpcServer的呢,接下來會和大家討論createRaftRpcServer()方法的具體實現

首先請看RaftRpcServerFactory.createRaftRpcServer(serverId.getEndpoint())方法:
createRaftRpcServer方法目前有createRaftRpcServer(final Endpoint endpoint)和
createRaftRpcServer(final Endpoint endpoint, final Executor raftExecutor,final Executor cliExecutor)兩個過載方法,其實不管哪個方法,本質上實現過程都有如下兩個步驟:
(1)首先呼叫了GrpcRaftRpcFactory的createRpcServer方法,這裡涉及gRpc構建server的底層知識,有時間會再寫一篇文章探究一下gRpc,這裡可以簡單理解為構建了一個rpc服務端。該方法實現如下:

public RpcServer createRpcServer(final Endpoint endpoint, final ConfigHelper<RpcServer> helper) {
    final int port = Requires.requireNonNull(endpoint, "endpoint").getPort();
    Requires.requireTrue(port > 0 && port < 0xFFFF, "port out of range:" + port);
    final MutableHandlerRegistry handlerRegistry = new MutableHandlerRegistry();
    final Server server = ServerBuilder.forPort(port) //
        .fallbackHandlerRegistry(handlerRegistry) //
        .directExecutor() //
        .maxInboundMessageSize(RPC_MAX_INBOUND_MESSAGE_SIZE) //
        .build();
    final RpcServer rpcServer = new GrpcServer(server, handlerRegistry, this.parserClasses, getMarshallerRegistry());
    if (helper != null) {
        helper.config(rpcServer);
    }
    return rpcServer;
}

(2)緊接著呼叫addRaftRequestProcessors,這個方法為RpcServer新增RAFT和CLI服務核心請求處理器,關於RpcProcessor這個實體類,會在後面的文章中具體分析,這裡可以先"不求甚解"。

  //新增RAFT和CLI服務請求處理器
public static void addRaftRequestProcessors(final RpcServer rpcServer, final Executor raftExecutor,
                                                final Executor cliExecutor) {
        // 新增raft核心處理器
        final AppendEntriesRequestProcessor appendEntriesRequestProcessor = new AppendEntriesRequestProcessor(
            raftExecutor);
        rpcServer.registerConnectionClosedEventListener(appendEntriesRequestProcessor);
        rpcServer.registerProcessor(appendEntriesRequestProcessor);
        rpcServer.registerProcessor(new GetFileRequestProcessor(raftExecutor));
        rpcServer.registerProcessor(new InstallSnapshotRequestProcessor(raftExecutor));
        rpcServer.registerProcessor(new RequestVoteRequestProcessor(raftExecutor));
        rpcServer.registerProcessor(new PingRequestProcessor());
        rpcServer.registerProcessor(new TimeoutNowRequestProcessor(raftExecutor));
        rpcServer.registerProcessor(new ReadIndexRequestProcessor(raftExecutor));
        // 新增raft cli服務處理器
        rpcServer.registerProcessor(new AddPeerRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new RemovePeerRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new ResetPeerRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new ChangePeersRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new GetLeaderRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new SnapshotRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new TransferLeaderRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new GetPeersRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new AddLearnersRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new RemoveLearnersRequestProcessor(cliExecutor));
        rpcServer.registerProcessor(new ResetLearnersRequestProcessor(cliExecutor));
    }

5. 一探究竟:CounterGrpcHelper做了什麼

CountServer構造器在初步建立RpcServer後,呼叫了CounterGrpcHelper.initGRpc()和CounterGrpcHelper.setRpcServer(rpcServer)兩個方法,接下來和大家分析這兩個方法的實現過程

首先請看initGRpc方法:
RpcFactoryHelper.rpcFactory()實際是呼叫了GrpcRaftRpcFactory(因為GrpcRaftRpcFactory實現了RaftRpcFactory介面),GrpcRaftRpcFactory中維護了一個ConcurrentHashMap<String, Message> parserClasses 其中【key為各種請求/響應實體的名稱,value為對應請求/響應的例項】。
然後透過反射獲取到MarshallerHelper的registerRespInstance方法,實際上MarshallerHelper裡面維護了一個ConcurrentHashMap<String, Message> messages 其中【key為請求實體的名稱,value為對應響應的例項

    public static void initGRpc() {
        if ("com.alipay.sofa.jraft.rpc.impl.GrpcRaftRpcFactory".equals(RpcFactoryHelper.rpcFactory().getClass()
            .getName())) {
            
            RpcFactoryHelper.rpcFactory().registerProtobufSerializer(CounterOutter.GetValueRequest.class.getName(),
                CounterOutter.GetValueRequest.getDefaultInstance());
            RpcFactoryHelper.rpcFactory().registerProtobufSerializer(
                CounterOutter.IncrementAndGetRequest.class.getName(),
                CounterOutter.IncrementAndGetRequest.getDefaultInstance());
            RpcFactoryHelper.rpcFactory().registerProtobufSerializer(CounterOutter.ValueResponse.class.getName(),
                CounterOutter.ValueResponse.getDefaultInstance());

            try {
                Class<?> clazz = Class.forName("com.alipay.sofa.jraft.rpc.impl.MarshallerHelper");
                Method registerRespInstance = clazz.getMethod("registerRespInstance", String.class, Message.class);
                registerRespInstance.invoke(null, CounterOutter.GetValueRequest.class.getName(),
                    CounterOutter.ValueResponse.getDefaultInstance());
                registerRespInstance.invoke(null, CounterOutter.IncrementAndGetRequest.class.getName(),
                    CounterOutter.ValueResponse.getDefaultInstance());
            } catch (Exception e) {
                LOG.error("Failed to init grpc server", e);
            }
        }
    }

接著我們再看setRpcServer方法:
CounterGrpcHelper裡面還維護了一個RpcServer例項,CounterGrpcHelper.setRpcServer(rpcServer)實際上會將構造的RpcServer裝配到CounterGrpcHelper裡面。

public static void setRpcServer(RpcServer rpcServer) {
        CounterGrpcHelper.rpcServer = rpcServer;
}

6.乘勝追擊:RaftGroupService

在CountServer構造器中,經過上述一系列操作步驟,走到了RaftGroupService構造器中,在構造RaftGroupService實體後,呼叫了它的start方法,這一步在於初始化 raft group 服務框架

public synchronized Node start(final boolean startRpcServer) {
    	//如果已經啟動了,那麼就返回
        if (this.started) {
            return this.node;
        }
    	//校驗serverId和groupId
        if (this.serverId == null || this.serverId.getEndpoint() == null
            || this.serverId.getEndpoint().equals(new Endpoint(Utils.IP_ANY, 0))) {
            throw new IllegalArgumentException("Blank serverId:" + this.serverId);
        }
        if (StringUtils.isBlank(this.groupId)) {
            throw new IllegalArgumentException("Blank group id:" + this.groupId);
        }
         //設定當前node的ip和埠
        NodeManager.getInstance().addAddress(this.serverId.getEndpoint());
    	//建立node
        this.node = RaftServiceFactory.createAndInitRaftNode(this.groupId, this.serverId, this.nodeOptions);
        if (startRpcServer) {
             //啟動遠端服務
            this.rpcServer.init(null);
        } else {
            LOG.warn("RPC server is not started in RaftGroupService.");
        }
        this.started = true;
        LOG.info("Start the RaftGroupService successfully.");
        return this.node;
    }

這個方法會在一開始的時候對RaftGroupService在構造器例項化的引數進行校驗,然後把當前節點的Endpoint新增到NodeManager的addrSet變數中,接著呼叫RaftServiceFactory#createAndInitRaftNode例項化Node節點。
每個節點都會啟動一個rpc的服務,因為每個節點既可以被選舉也可以投票給其他節點,節點之間需要互相通訊,所以需要啟動一個rpc服務。

7.刨根問底:Node節點的建立

以下就是Node節點的一系列建立過程,由於巢狀的層數比較多,所以就全部列舉出來了,整個過程簡而言之就是,createAndInitRaftNode方法首先呼叫createRaftNode例項化一個Node的例項NodeImpl,然後呼叫其init方法進行初始化,主要的配置都是在init方法中完成的。程式碼如下:

this.node = RaftServiceFactory.createAndInitRaftNode(this.groupId, this.serverId, this.nodeOptions);
public static Node createAndInitRaftNode(final String groupId, final PeerId serverId, final NodeOptions opts) {
        final Node ret = createRaftNode(groupId, serverId);
        if (!ret.init(opts)) {
            throw new IllegalStateException("Fail to init node, please see the logs to find the reason.");
        }
        return ret;
    }
public static Node createRaftNode(final String groupId, final PeerId serverId) {
        return new NodeImpl(groupId, serverId);
    }
public NodeImpl(final String groupId, final PeerId serverId) {
        super();
        if (groupId != null) {
            Utils.verifyGroupId(groupId);
        }
        this.groupId = groupId;
        this.serverId = serverId != null ? serverId.copy() : null;
        this.state = State.STATE_UNINITIALIZED;
        this.currTerm = 0;
        updateLastLeaderTimestamp(Utils.monotonicMs());
        this.confCtx = new ConfigurationCtx(this);
        this.wakingCandidate = null;
        final int num = GLOBAL_NUM_NODES.incrementAndGet();
        LOG.info("The number of active nodes increment to {}.", num);
    }

8.一窺到底:Node節點的初始化

老實說,NodeImpl#init方法確實挺長的,所以我打算分成幾個部分來展示,方便分析

(1)《引數的賦值與校驗》

這段程式碼主要是給各個變數賦值,然後進行校驗判斷一下serverId不能為0.0.0.0,當前的Endpoint必須要在NodeManager裡面設定過等等(NodeManager的設定是在RaftGroupService的start方法裡)。
然後會初始化一個全域性的的定時排程管理器TimerManager:

    	//一系列判空操作
		Requires.requireNonNull(opts, "Null node options");
        Requires.requireNonNull(opts.getRaftOptions(), "Null raft options");
        Requires.requireNonNull(opts.getServiceFactory(), "Null jraft service factory");
        //JRaftServiceFactory目前有3個實現類
        // 1.BDBLogStorageJRaftServiceFactory    
        // 2.DefaultJRaftServiceFactory
        // 3.HybridLogJRaftServiceFactory
		this.serviceFactory = opts.getServiceFactory();
        this.options = opts;
        this.raftOptions = opts.getRaftOptions();
    	//基於 Metrics 類庫的效能指標統計,具有豐富的效能統計指標,預設為false,不開啟度量工具
        this.metrics = new NodeMetrics(opts.isEnableMetrics());
        this.serverId.setPriority(opts.getElectionPriority());
        this.electionTimeoutCounter = 0;
    	//Utils.IP_ANY = "0.0.0.0"
        if (this.serverId.getIp().equals(Utils.IP_ANY)) {
            LOG.error("Node can't started from IP_ANY.");
            return false;
        }

        if (!NodeManager.getInstance().serverExists(this.serverId.getEndpoint())) {
            LOG.error("No RPC server attached to, did you forget to call addService?");
            return false;
        }

        if (this.options.getAppendEntriesExecutors() == null) {
            this.options.setAppendEntriesExecutors(Utils.getDefaultAppendEntriesExecutor());
        }
    	//定時工作管理員
		//此處TIMER_FACTORY獲取到的是DefaultRaftTimerFactory
    	//this.options.isSharedTimerPool()預設為false
		//this.options.getTimerPoolSize()取值為Utils.cpus() * 3 > 20 ? 20 : Utils.cpus() * 3
        this.timerManager = TIMER_FACTORY.getRaftScheduler(this.options.isSharedTimerPool(),
                this.options.getTimerPoolSize(), "JRaft-Node-ScheduleThreadPool");

此處淺析一下__TimerManager:
初始化一個執行緒池,根據傳入的引數this.options.getTimerPoolSize()==Utils.cpus() * 3 > 20 ? 20 : Utils.cpus() * 3可以分析得知如果當前的伺服器的cpu執行緒數_3 大於20 ,那麼這個執行緒池的coreSize就是20,否則就是cpu執行緒數的_3倍。

public TimerManager(int workerNum, String name) {
        this.executor = ThreadPoolUtil.newScheduledBuilder() //
            .poolName(name) //
            .coreThreads(workerNum) //
            .enableMetric(true) //
            .threadFactory(new NamedThreadFactory(name, true)) //
            .build();
    }

(2)《計時器的初始化》

由於這些計時器的實現比較繁雜,所以具體功能等到後面對應章節再一併梳理。

  • voteTimer是用來控制選舉的,如果選舉超時,當前的節點又是候選者角色,那麼就會發起選舉。
  • electionTimer是預投票計時器。候選者在發起投票之前,先發起預投票,如果沒有得到半數以上節點的反饋,則候選者就會識趣的放棄參選。
  • stepDownTimer定時檢查是否需要重新選舉leader。當前的leader可能出現它的Follower可能並沒有整個叢集的1/2卻還沒有下臺的情況,那麼這個時候會定期的檢檢視leader的Follower是否有那麼多,沒有那麼多的話會強制讓leader下臺。
  • snapshotTimer快照計時器。這個計時器會每隔1小時觸發一次生成一個快照。

這些計時器有一個共同的特點就是會根據不同的計時器返回一個在一定範圍內隨機的時間。返回一個隨機的時間可以防止多個節點在同一時間內同時發起投票選舉從而降低選舉失敗的機率。

    	//設定投票計時器
        final String suffix = getNodeId().toString();
        String name = "JRaft-VoteTimer-" + suffix;
        this.voteTimer = new RepeatedTimer(name, this.options.getElectionTimeoutMs(), TIMER_FACTORY.getVoteTimer(
                this.options.isSharedVoteTimer(), name)) {
        	//處理投票超時
            @Override
            protected void onTrigger() {
                handleVoteTimeout();
            }
        	//在一定範圍內返回一個隨機的時間戳
            @Override
            protected int adjustTimeout(final int timeoutMs) {
                return randomTimeout(timeoutMs);
            }
        };
		//設定預投票計時器
		//當leader在規定的一段時間內沒有與 Follower 艦船進行通訊時,
		// Follower 就可以認為leader已經不能正常擔任旗艦的職責,則 Follower 可以去嘗試接替leader的角色。
		// 這段通訊超時被稱為 Election Timeout
		//候選者在發起投票之前,先發起預投票
        name = "JRaft-ElectionTimer-" + suffix;
        this.electionTimer = new RepeatedTimer(name, this.options.getElectionTimeoutMs(),
                TIMER_FACTORY.getElectionTimer(this.options.isSharedElectionTimer(), name)) {

            @Override
            protected void onTrigger() {
                handleElectionTimeout();
            }

            //在一定範圍內返回一個隨機的時間戳
        	//為了避免同時發起選舉而導致失敗
            @Override
            protected int adjustTimeout(final int timeoutMs) {
                return randomTimeout(timeoutMs);
            }
        };

    	//leader下臺的計時器
		//定時檢查是否需要重新選舉leader
        name = "JRaft-StepDownTimer-" + suffix;
        this.stepDownTimer = new RepeatedTimer(name, this.options.getElectionTimeoutMs() >> 1,
                TIMER_FACTORY.getStepDownTimer(this.options.isSharedStepDownTimer(), name)) {

            @Override
            protected void onTrigger() {
                handleStepDownTimeout();
            }
        };
		//快照計時器
        name = "JRaft-SnapshotTimer-" + suffix;
        this.snapshotTimer = new RepeatedTimer(name, this.options.getSnapshotIntervalSecs() * 1000,
                TIMER_FACTORY.getSnapshotTimer(this.options.isSharedSnapshotTimer(), name)) {

            private volatile boolean firstSchedule = true;

            @Override
            protected void onTrigger() {
                handleSnapshotTimeout();
            }

            @Override
            protected int adjustTimeout(final int timeoutMs) {
                if (!this.firstSchedule) {
                    return timeoutMs;
                }

                // Randomize the first snapshot trigger timeout
                this.firstSchedule = false;
                if (timeoutMs > 0) {
                    int half = timeoutMs / 2;
                    return half + ThreadLocalRandom.current().nextInt(half);
                } else {
                    return timeoutMs;
                }
            }
        };

(3)《消費佇列Disruptor》

關於Disruptor的內容,後面有時間會寫一篇相關的文章進行分享

這裡初始化了一個Disruptor作為消費佇列,然後校驗了metrics是否開啟,預設是不開啟的

 this.configManager = new ConfigurationManager();
		//初始化一個disruptor,採用多生產者模式
        this.applyDisruptor = DisruptorBuilder.<LogEntryAndClosure>newInstance() //
            //設定disruptor大小,預設16384    
            	.setRingBufferSize(this.raftOptions.getDisruptorBufferSize()) //
                .setEventFactory(new LogEntryAndClosureFactory()) //
                .setThreadFactory(new NamedThreadFactory("JRaft-NodeImpl-Disruptor-", true)) //
                .setProducerType(ProducerType.MULTI) //
                .setWaitStrategy(new BlockingWaitStrategy()) //
                .build();
		//設定事件處理器
        this.applyDisruptor.handleEventsWith(new LogEntryAndClosureHandler());
		//設定異常處理器
        this.applyDisruptor.setDefaultExceptionHandler(new LogExceptionHandler<Object>(getClass().getSimpleName()));
        // 啟動disruptor的執行緒
		this.applyQueue = this.applyDisruptor.start();
    	//如果開啟了metrics統計
        if (this.metrics.getMetricRegistry() != null) {
            this.metrics.getMetricRegistry().register("jraft-node-impl-disruptor",
                    new DisruptorMetricSet(this.applyQueue));
        }

(4)《功能初始化》

對快照、日誌、後設資料等功能進行初始化

    	//fsmCaller封裝對業務 StateMachine 的狀態轉換的呼叫以及日誌的寫入等
		this.fsmCaller = new FSMCallerImpl();
    	//初始化日誌儲存功能
        if (!initLogStorage()) {
            LOG.error("Node {} initLogStorage failed.", getNodeId());
            return false;
        }
		//初始化後設資料儲存功能
        if (!initMetaStorage()) {
            LOG.error("Node {} initMetaStorage failed.", getNodeId());
            return false;
        }
    	//對FSMCaller初始化
        if (!initFSMCaller(new LogId(0, 0))) {
            LOG.error("Node {} initFSMCaller failed.", getNodeId());
            return false;
        }
    	//例項化投票箱
        this.ballotBox = new BallotBox();
        final BallotBoxOptions ballotBoxOpts = new BallotBoxOptions();
        ballotBoxOpts.setWaiter(this.fsmCaller);
        ballotBoxOpts.setClosureQueue(this.closureQueue);
    	//初始化ballotBox的屬性
        if (!this.ballotBox.init(ballotBoxOpts)) {
            LOG.error("Node {} init ballotBox failed.", getNodeId());
            return false;
        }
    	//初始化快照儲存功能
        if (!initSnapshotStorage()) {
            LOG.error("Node {} initSnapshotStorage failed.", getNodeId());
            return false;
        }
    	//校驗日誌檔案索引的一致性
        final Status st = this.logManager.checkConsistency();
        if (!st.isOk()) {
            LOG.error("Node {} is initialized with inconsistent log, status={}.", getNodeId(), st);
            return false;
        }
    	//配置管理raft group中的資訊
        this.conf = new ConfigurationEntry();
        this.conf.setId(new LogId());
        // if have log using conf in log, else using conf in options
        if (this.logManager.getLastLogIndex() > 0) {
            checkAndSetConfiguration(false);
        } else {
            this.conf.setConf(this.options.getInitialConf());
            // initially set to max(priority of all nodes)
            this.targetPriority = getMaxPriorityOfNodes(this.conf.getConf().getPeers());
        }

        if (!this.conf.isEmpty()) {
            Requires.requireTrue(this.conf.isValid(), "Invalid conf: %s", this.conf);
        } else {
            LOG.info("Init node {} with empty conf.", this.serverId);
        }

初始化replicatorGroup、rpcService以及readOnlyService:

// TODO RPC service and ReplicatorGroup is in cycle dependent, refactor it
        this.replicatorGroup = new ReplicatorGroupImpl();
    	//收其他節點或者客戶端發過來的請求,轉交給對應服務處理
        this.rpcService = new DefaultRaftClientService(this.replicatorGroup, this.options.getAppendEntriesExecutors());
        final ReplicatorGroupOptions rgOpts = new ReplicatorGroupOptions();
        rgOpts.setHeartbeatTimeoutMs(heartbeatTimeout(this.options.getElectionTimeoutMs()));
        rgOpts.setElectionTimeoutMs(this.options.getElectionTimeoutMs());
        rgOpts.setLogManager(this.logManager);
        rgOpts.setBallotBox(this.ballotBox);
        rgOpts.setNode(this);
        rgOpts.setRaftRpcClientService(this.rpcService);
        rgOpts.setSnapshotStorage(this.snapshotExecutor != null ? this.snapshotExecutor.getSnapshotStorage() : null);
        rgOpts.setRaftOptions(this.raftOptions);
        rgOpts.setTimerManager(this.timerManager);

        // Adds metric registry to RPC service.
        this.options.setMetricRegistry(this.metrics.getMetricRegistry());
    	//初始化rpc服務
        if (!this.rpcService.init(this.options)) {
            LOG.error("Fail to init rpc service.");
            return false;
        }
        this.replicatorGroup.init(new NodeId(this.groupId, this.serverId), rgOpts);

        this.readOnlyService = new ReadOnlyServiceImpl();
        final ReadOnlyServiceOptions rosOpts = new ReadOnlyServiceOptions();
        rosOpts.setFsmCaller(this.fsmCaller);
        rosOpts.setNode(this);
        rosOpts.setRaftOptions(this.raftOptions);
    	//只讀服務初始化
        if (!this.readOnlyService.init(rosOpts)) {
            LOG.error("Fail to init readOnlyService.");
            return false;
        }

(5)《邏輯變動》

這段程式碼裡會將當前的狀態設定為Follower,然後啟動快照定時器定時生成快照。
如果當前的叢集不是單節點叢集需要做一下stepDown,表示新生成的Node節點需要重新進行選舉。
最下面有一個if分支,如果當前的jraft叢集裡只有一個節點,那麼個節點必定是leader直接進行選舉就好了,所以會直接呼叫electSelf進行選舉。

    	// 將當前的狀態設定為Follower
        this.state = State.STATE_FOLLOWER;

        if (LOG.isInfoEnabled()) {
            LOG.info("Node {} init, term={}, lastLogId={}, conf={}, oldConf={}.", getNodeId(), this.currTerm,
                    this.logManager.getLastLogId(false), this.conf.getConf(), this.conf.getOldConf());
        }
    	//如果快照執行器不為空,並且生成快照的時間間隔大於0,那麼就定時生成快照
        if (this.snapshotExecutor != null && this.options.getSnapshotIntervalSecs() > 0) {
            LOG.debug("Node {} start snapshot timer, term={}.", getNodeId(), this.currTerm);
            this.snapshotTimer.start();
        }
    	//新啟動的node需要重新選舉
        if (!this.conf.isEmpty()) {
            stepDown(this.currTerm, false, new Status());
        }

        if (!NodeManager.getInstance().add(this)) {
            LOG.error("NodeManager add {} failed.", getNodeId());
            return false;
        }

        // Now the raft node is started , have to acquire the writeLock to avoid race
        // conditions
        this.writeLock.lock();
    	//這個分支表示當前的jraft叢集裡只有一個節點,那麼個節點必定是leader直接進行選舉就好了
        if (this.conf.isStable() && this.conf.getConf().size() == 1 && this.conf.getConf().contains(this.serverId)) {
            // The group contains only this server which must be the LEADER, trigger
            // the timer immediately.
            electSelf();
        } else {
            this.writeLock.unlock();
        }

        return true;

9.寫在最後

SOFAJRaft 是一個基於 RAFT 一致性演算法的生產級高效能 Java 實現。
第一次閱讀這種複雜的開原始碼,老實說確實非常吃力,但其實步步深入,反覆推敲,逐漸會從恐懼陌生甚至牴觸,轉變為驚喜與讚歎。你會慢慢痴迷於裡面很多優雅且優秀的實現。
在這裡,感謝SOFAJRaft的每一位程式碼貢獻者。原始碼的閱讀過程中,的的確確學到了很多東西。我也會繼續學習下去,希望能夠鞏固、深入我對RAFT一致性演算法的理解與體悟。

相關文章