SOFAJRaft模块启动过程

本篇文章旨在分析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一致性算法的理解与体悟。

本文转载于网络 如有侵权请联系删除

相关文章

  • 派学车融资、YY学车倒闭,互联网驾培旱涝两重天

    配图来自Canva可画翻开互联网驾培行业发展史,曾经辉煌一时的平台相继走下坡路,紧接着新平台乘着数字化转型风口崛起,头部平台排名在悄然变化中。十年前,互联网时代的到来颠覆了很多传统行业,驾考市场也是其中之一。彼时互联网的介入改变了驾校、教练、学员之间的连接方式,提高了沟通的效率,“互联网+驾培”被认为是改变传统驾考模式的最佳途径,催生了凸凸学车、OK学车、猪兼强、YY学车等网红互联网教培平台。好景不长,互联网驾培平台沉迷盲目烧钱、过度营销,导致入不敷出造成严重亏损,凸凸学车、OK学车、猪兼强几个大头相继宣告破产停止运营。与此同时,用户学车权益得不到保障,投诉案例时有发生,“互联网+驾培”模式遭质疑。互联网驾培热潮涨涨落落,潮涨之时,互联网驾培平台沉迷于“金钱游戏”日夜狂欢,潮退之后,互联网驾培平台深陷泥潭,野蛮生长的行为难以为继。破产、关停、制约近期互联网驾校YY学车宣布破产的消息一出,社交网络、媒体上就出现了各种唱衰互联网驾培的声音。有媒体爆料,YY学车传出疑似倒闭,众多学员前往公司现场要求退还欠款,但公司已人去楼空。目前YY学车在广州的各个分校、教练场已经关门,联系电话停机,官方网

  • find 命令的参数详解

    使用name选项 文件名选项是find最常用的选项,要么单独使用该选项,要么和其他选项一起使用。可以使用某种文件名模式来匹配文件,记住要用引号将文件名模式引起来。不管当前路径是什么,如果想要在自己的根目录$HOME中查找文件名符合*.log的文件,使用~作为'pathname'参数,波浪号~代表了你的$HOME目录。示例1:想要在当前目录及子目录中查找所有的‘*.log‘文件find.-name"*.log"-print复制示例2:想要的当前目录及子目录中查找文件名以一个大写字母开头的文件find.-name"[A-Z]*"-print复制示例3:想要在/etc目录中查找文件名以host开头的文件find/etc-name"host*"-print复制示例4:想要查找$HOME目录中的文件find~-name“*”-print或find.-print 示例5:要想让系统高负荷运行,就从根目录开始查找所有的文件find/-name"*"-print复制示例6:想在当前目录查找文件名以一个个

  • 【漫画】为什么C盘太满系统会卡?

    PS:本文原作者发于去年中秋的时候。假期的最后一天,二毛坐高铁回到了这座一不努力就要每天吃猪脚饭的城市,转乘地铁回到了租房里。随后两人一起吃着月饼,二毛此时打开了电脑,发现他的系统C盘已经快要爆满了。计算机存储简单可以分为两类:内部存储器:一断电就会丢失记住的东西。就是我们通常说的内存,内存的信息存取速度很快,但是通常容量较小,并且依赖电源,断电后其中存储的内容就会丢失。外部存储器:断了电也能存住。相对内存来说,容量会大一些,但是存取速度会相对慢一点。常见的外存储器包括磁盘、光盘、U盘等。我们这里说的C盘就是磁盘了。CPU运算所需要的程序代码和数据来自于内存,内存中的东西则是从磁盘加载进来的,而磁盘存放着各种各样的软件代码、数据文件等等。虚拟内存别称虚拟存储器(VirtualMemory)。电脑中所运行的程序均需经由内存执行,若执行的程序占用内存很大或很多,则会导致内存消耗殆尽。为解决该问题,Windows中运用了虚拟内存技术,即拿出一部分硬盘空间来充当内存使用。当内存耗尽时,电脑就会自动调用硬盘来充当内存,以缓解内存的紧张。而C盘恰恰就是虚拟内存的所在地,如果C盘满了,也就没有虚拟内

  • 字节跳动年前再招聘1W+人,距离大厂 Offer,你还差这篇Android干货!

    大厂的招聘,并不会因为疫情而影响太多。前段时间,字节跳动发布了年前再招1万人的消息。从字节的招聘岗位需求来看,研发人员仍占多数。我身边的研发朋友们又开始蠢蠢欲动了,是啊,谁还没个字节梦呢?网上也出现远程入职的字节跳动员工,还没去公司就收到笔记本电脑的新闻。放心,这不是广告,只是想说明,疫情打不垮中国企业的发展,更不会阻挡中国的用工需求。而如果你要面试大厂Android岗位,这篇文章或许应该作为你的“面经”。1、网络网络协议模型应用层:负责处理特定的应用程序细节 HTTP、FTP、DNS传输层:为两台主机提供端到端的基础通信 TCP、UDP网络层:控制分组传输、路由选择等 IP链路层:操作系统设备驱动程序、网卡相关接口TCP和UDP区别TCP连接;可靠;有序;面向字节流;速度慢;较重量;全双工;适用于文件传输、浏览器等全双工:A给B发消息的同时,B也能给A发半双工:A给B发消息的同时,B不能给A发UDP无连接;不可靠;无序;面向报文;速度快;轻量;适用于即时通讯、视频通话等TCP三次握手A:你能听到吗? B:我能听到,你能听到吗? A:我能听到,开始吧A和B两方都要能确保:我说的话,你能

  • SAP Spartacus ProductCarouselComponent

    入口被CMSService控制:构造函数里componentuid:ElectronicsHomepageProductCarouselComponentScope是硬编码的ProductScope.LIST:这里只能看到Observable回调的触发,而很难看到谁触发的它。顺着调用栈一层一层往外找:注意这里的value结构已经不一样了:经过了switchMap操作:

  • Lambda表达式最佳实践

    简介Lambda表达式java8引入的函数式编程框架。之前的文章中我们也讲过Lambda表达式的基本用法。本文将会在之前的文章基础上更加详细的讲解Lambda表达式在实际应用中的最佳实践经验。优先使用标准Functional接口之前的文章我们讲到了,java在java.util.function包中定义了很多Function接口。基本上涵盖了我们能够想到的各种类型。假如我们自定义了下面的Functionalinterface:@FunctionalInterfacepublicinterfaceUsage{ Stringmethod(Stringstring);}复制然后我们需要在一个test方法中传入该interface:publicStringtest(Stringstring,Usageusage){ returnusage.method(string);}复制上面我们定义的函数接口需要实现method方法,接收一个String,返回一个String。这样我们完全可以使用Function来代替:publicStringtest(Stringstring,Function<S

  • IDEA 中比较骚的技巧!你可能没用过

    IDEA有个很牛逼的功能,那就是后缀补全(不是自动补全),很多人竟然不知道这个操作,还在手动敲代码。这个功能可以使用代码补全来模板式地补全语句,如遍历循环语句(for、foreach)、使用String.format()包裹一个字符串、使用类型转化包裹一个表达式、根据判(非)空或者其它判别语句生成if语句、用instanceOf生成分支判断语句等。 使用的方式也很简单,就是在一个表达式后按下点号.,然后输入一些提示或者在列表中选择一个候选项,常见的候选项下面会给出GIF演示。 1.var声明 2.null判空 3.notnull判非空 4.nn判非空 5.for遍历 6.fori带索引的遍历 7.not取反 8.if条件判断 9.cast强转 10.return返回值

  • 技术分析!要我怎么说!

    你以为是MACD呢或者设想下,病人在诊所拍了张心电图,医师用手比划并标出黄金分割比;在复核病人脉搏血压数据时,指出“三角形态”。这种情况病人肯定不敢再请这位医师看病。上述情况并没有发生在科研领域,原因是这些领域的数据分析标准近年来大幅提高。典型的有美国计量协会(ASA)对P值和显著性统计的使用发表的声明。该声明批评所谓的“P-hacking”,即通过在单一组数据集上测试不计其数的假说,直到其中一个假说满足要求的P值。券商及金融媒体眼中的技术分析那么,对于那些推动技术分析的众多主要券商和金融媒体机构,包括“趋势”,“波浪”,“突破模式”,“三角形模式”和“斐波那契比率”,又是什么呢?▍嘉信理财(CharlesSchwab)认为技术分析对主动交易员来说不可或缺。▍美林银行(MerrillLynch)提供一本技术分析手册,其中章节标题有“价格动量指标”“阻力支撑”“斐波那契”等。▍美国银行(BankofAmerica)和美林银行(MerrillLynch)BankofAmericaMerrillLynch全球主要FICC技术策略师PaulCiana引用了“双突破”模式,相信债券市场将很快反弹

  • 苹果华人自动驾驶工程师跳槽小鹏汽车前夕被FBI逮捕:被指窃取商业机密

    机器之心报道 参与:李泽南、王淑婷、高璇 7月7日,一名曾在苹果自动驾驶汽车项目工作的员工在加州圣何塞机场准备登机飞往中国时,被美国联邦调查局(FBI)的执法人员逮捕。据知情人士称,这位名为XiaolangZhang的工程师正准备跳槽入职国内创业公司小鹏汽车。 美国加州圣何塞当局表示,苹果公司一名前员工被联邦法院指控窃取公司商业机密。 对XiaolangZhang的指控星期一在美国北加州地区法院提出。Zhang曾是苹果公司的硬件工程师,被控计划跳槽到国内自动驾驶汽车公司时带走了前东家的一些商业机密。 当局称,联邦特工周六逮捕了试图在圣何塞机场通过安检的Zhang。有关部门说,他购买了「最后一张」往返北京的机票,最终目的地是杭州,乘坐的是海南航空。 苹果发言人TomNeumayr在一封电子邮件中说:「苹果非常注重保密和保护知识产权。我们正就此事与当局合作,并会尽一切可能确保该名人士及其他相关人士对其行为负责。」 声称当前在山景城为小鹏汽车工作的Zhang,于2015年12月加入苹果公司,并在一个试图研发自动驾驶汽车的团队中担任硬件工程师。苹果公司一直将这一研究和开发作为一个严格保密的机密

  • Nature机器学习子刊被讽开历史倒车,Jeff Dean等数百学者签名抵制

    伊瓢发自麦拜德 量子位报道|公众号QbitAI全球数百位学者联手署名反对的事情并不太常见。这次,大名鼎鼎的学术期刊《自然》(Nature)杂志却被机器学习界的朋友们集体抵制了。去年年底,《自然》宣布将于2019年1月推出机器智能子刊NatureMachineIntelligence(《自然-机器智能》)。当时都以为,除了三大顶会CVPR、ICCV、ECCV和其他会议之外,搞ML的同学们终于可以发Nature了,看起来似乎是个好事。但是,事情终于走向另一面。为了反对《自然》自身的封闭性,俄勒冈州立大学的网站上出现了一封联署信:关于《自然-机器智能》的声明 机器学习这个领域一直处在免费开放获取研究成果的前列。还记得在2001年,《MachineLearningJournal》编委会集体辞职,组建了免费开放期刊《JournalofMachineLearningResearch(JMLR)》。 他们的辞职信里说: “期刊应当完全服务于学界的需求,尤其应当为现代科技的发展提供及时且大范围的期刊文章,并且不应该把任何人排除在外。” 除了JMLR,几乎所有主要的机器学习会议,包括NIPS,ICML,

  • 区块链记账原理

    区块链(1.0)是一个基于密码学安全的分布式账本,是一个方便验证,不可篡改的账本。 通常认为与智能合约相结合的区块链为区块链2.0,如以太坊是典型的区块链2.0 很多人只了解过比特币,不知道区块链,比特币实际是一个使用了区块链技术的应用,只是比特币当前太热,把区块链技术的光芒给掩盖了。区块链才是未来,期望各位开发人员少关心币价,多关心技术。 本文将讲解区块链1.0技术是如何实现的。哈希函数在讲区块链记账之前,先说明一下哈希函数。 哈希函数:Hash(原始信息)=摘要信息 原始信息可以是任意的信息,hash之后会得到一个简短的摘要信息哈希函数有几个特点:同样的原始信息用同一个哈希函数总能得到相同的摘要信息原始信息任何微小的变化都会哈希出面目全非的摘要信息从摘要信息无法逆向推算出原始信息举例说明: Hash(张三借给李四100万,利息1%,1年后还本息…..)=AC4635D34DEF 账本上记录了AC4635D34DEF这样一条记录。可以看出哈希函数有4个作用:简化信息 很好理解,哈希后的信息变短了。标识信息 可以使用AC4635D34DEF来标识原始信息,摘要信息也称为原始信息的id。

  • “优雅”的Linux漏洞:用罕见方式绕过ASLR和DEP保护机制

    最近国外研究人员公布的一段exp代码能够在打完补丁的Fedora等Linux系统上进行drive-by攻击,从而安装键盘记录器、后门和其他恶意软件。这次的exp针对的是GStreamer框架中的一个内存损坏漏洞,GStreamer是个开源多媒体框架,存在于主流的Linux发行版中。我们都知道,地址空间布局随机化(ASLR)和数据执行保护(DEP)是linux系统中两个安全措施,目的是为了让软件exp更难执行。但新公布的exp通过一种罕见的办法绕过了这两种安全措施——国外媒体还专门强调了这个漏洞的“优雅”特色。研究人员写了个flac多媒体文件,就能达成漏洞利用!ASLR是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。而DEP则能够在内存上执行额外检查以帮助防止在系统上运行恶意代码。无脚本exp与传统的ASLR和DEP绕过方法不同的是,这个exp没有通过代码来篡改内存布局和其他的环境变量。而是通过更难的字节码排序彻底关闭保护。由于不需要JavaScript也

  • 电商域名.shop受全球用户青睐 续费率高达74.3%

    2017年12月,技慕科技(北京)有限公司宣布由其运营管理的新顶级域名.shop自2016年9月面向全球开放以来,续费率高达74.3%,成为全球续费率最高的新顶级域名之一。.shop域名后缀以其简短、直观、易记的特点和自带的电商性质,一年以来吸引了来自全球182个国家的注册用户,注册量突破34万,在新顶级域名当中排名第12。.shop域名尤其受到欧洲地区用户的欢迎,技慕科技透露超过半数的.shop域名注册用户来自欧洲国家,德国和荷兰分别有超过2.3万和1.1万的注册量。.shop域名在欧洲地区整体的续费率高达81%,南美地区以76%紧跟其后,北美地区的以74%占据第三位。在过去一年中,1149个溢价.shop域名被注册,为注册局带来120万美元的溢价域名销售额,其中71.4%的溢价域名用户为注册域名续费。另外一个更高的续费率体现在.shop域名日升期注册的域名。2016年.shop创造了最高纪录获得1182份来自全球品牌的域名申请,97.4%在日升期注册的.shop域名被续费。技慕科技首席执行官HiroTsukahara表示:“.shop首年续费率反映了市场对.shop域名的认何和对电

  • Pytorch中tensor和numpy互相转换[通俗易懂]

    从numpy中导入tensor torch.from_numpy(data)或torch.from_numpy(data).to(a.device) 也可以用torch.tensor(data),但torch.from_numpy更加安全,使用tensor.Tensor在非float类型下会与预期不符 以前是整型,导入就是整型。以前是浮点型,导入就是浮点型 注意,torch.from_numpy()这种方法互相转的Tensor和numpy对象共享内存,所以它们之间的转换很快,而且几乎不会消耗资源。这也意味着,如果其中一个变了,另外一个也会随之改变。 图片的numpy转tensor 注意,读取图片成numpyarray的范围是[0,255]是uint8 而转成tensor的范围就是[0,1.0],是float 所以图片的numpy转tensor有些不一样 如果是直接按照上面的方法x=torch.from_array(x),得到的tensor值是0-255的 得到0-1.0的话 importtorchvision.transformsastransformsimportmatpl

  • 腾讯云私有网络释放IPv6子网段api接口

    1.接口描述接口请求域名:vpc.tencentcloudapi.com。 本接口(UnassignIpv6SubnetCidrBlock)用于释放IPv6子网段。子网段如果还有IP占用且未回收,则子网段无法释放。 默认接口请求频率限制:20次/秒。 APIExplorer提供了在线调用、签名验证、SDK代码生成和快速检索接口等能力。您可查看每次调用的请求内容和返回结果以及自动生成SDK调用示例。 2.输入参数以下请求参数列表仅列出了接口请求参数和部分公共参数,完整公共参数列表见公共请求参数。 参数名称 必选 类型 描述 Action 是 String 公共参数,本接口取值:UnassignIpv6SubnetCidrBlock。 Version 是 String 公共参数,本接口取值:2017-03-12。 Region 是 String 公共参数,详见产品支持的地域列表。 VpcId 是 String 子网所在私有网络ID。形如:vpc-f49l6u0z。 Ipv6SubnetCidrBlocks.N 是 ArrayofIpv6Sub

  • 腾讯云安全凭证服务请求结构调用方式

    1.服务地址 API支持就近地域接入,本产品就近地域接入域名为sts.tencentcloudapi.com,也支持指定地域域名访问,例如广州地域的域名为sts.ap-guangzhou.tencentcloudapi.com。 推荐使用就近地域接入域名。根据调用接口时客户端所在位置,会自动解析到最近的某个具体地域的服务器。例如在广州发起请求,会自动解析到广州的服务器,效果和指定sts.ap-guangzhou.tencentcloudapi.com是一致的。 注意:对时延敏感的业务,建议指定带地域的域名。 注意:域名是API的接入点,并不代表产品或者接口实际提供服务的地域。产品支持的地域列表请在调用方式/公共参数文档中查阅,接口支持的地域请在接口文档输入参数中查阅。 目前支持的域名列表为: 接入地域 域名 就近地域接入(推荐,只支持非金融区) sts.tencentcloudapi.com 华南地区(广州) sts.ap-guangzhou.tencentcloudapi.com 华东地区(上海) sts.ap-shanghai.tencentcloudapi.

  • Light oj 1134 - Be Efficient (前缀和)

    题目链接:http://www.lightoj.com/volume_showproblem.php?problem=1134 题意:     给你n个数,问你多少个连续的数的和是m的倍数。 思路:     前缀和取模一下就好了。 1#include<iostream> 2#include<cstring> 3#include<cstdio> 4usingnamespacestd; 5constintN=1e5+5; 6inta[N]; 7intsum[N],cnt[N]; 8 9intmain() 10{ 11intt,n,m; 12scanf("%d",&t); 13for(intca=1;ca<=t;++ca){ 14scanf("%d%d",&n,&m); 15memset(cnt,0,sizeof(cnt)); 16cnt[0]++; 17for(inti=1;i<=n;++i){ 18scanf("%d",a+i); 19

  • UE4 引擎剖析 - 渲染线程

    转自:https://zhuanlan.zhihu.com/p/78347351 一、渲染线程的初始化: ////////////////////////////////////////////////////////////////////////////////////////////// //渲染线程执行体 classFRenderingThread:publicFRunnable {   //执行函数   virtualuint32Run(void)override   {     RenderingThreadMain(TaskGraphBoundSyncEvent);   } }复制 ////////////////////////////////////////////////////////////////////////////////////////////// //渲染线程初始化流程: 1.FEngineLoop::PreInit() { StartRenderingThread() }复制 2.StartRenderingThread() { GRen

  • [算法]LeetCode 120:三角形最小路径和

    题目描述: 给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。 例如,给定三角形: [[2],[3,4],[6,5,7],[4,1,8,3]]自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 =11)。 说明: 如果你可以只使用O(n) 的额外空间(n为三角形的总行数)来解决这个问题,那么你的算法会很加分。 原题地址:https://leetcode-cn.com/problems/triangle/submissions/ 题目思路: 这道题可以用递归(回溯)解答,但是时间复杂度是O(a^n),若是采用动态递归的话,时间复杂度只需要O(m*n),空间复杂度由O(m*n)还可以优化成O(n)。 dp[][]设置成2维的,和triangle维度一样,刚开始先初始化最后一行,和triangle最后一行一样。 转移方程: dp[i][j]=min{dp[i+1][j],dp[i+1][j+1]}+ triangle[i][j]   这个其实也

  • 一站式解决方案 :OFD电子证照生成

    前言 证照的电子化是一个趋势;可以预计,未来几年内,绝大部分证照都会电子化。电子证照的种类越来越多,应用场景也复杂多样;这就给电子证照规范的制定、电子证照的生成提出了更高的要求。电子证照采用的格式有两种:pdf、ofd。pdf为国际标准,生态比较好;ofd为国家标准,具有后发优势,拥有完整自主知识产权,可根据需求灵活修改规范。综合考虑,电子证照采用ofd更合适,也符合国家政策导向。   每一类电子证照外观格式是完全一致的,好像“同一个模子刻出来的”。显然,证照生成系统也要根据“模子“生成,这个模子就是证照模板。市面上的电子证照的生成系统因此大同小异,就是根据模板生成。我们的生成系统也不例外;但是,我们在生成系统上深耕细作,为用户的每个细节着想,形成了完善的一站式解决方案。目前,市面还没有出现与我们方案类似的系统,我们的系统具有很强的市场竞争力。本文简要描述我们的ofd证照生成系统处理逻辑。 好的电子证照生成系统评判标准 生成的文件符合ofd标准; 模板设计工具:方便灵活、可视化。能满足特殊需求:标引、元数据、附件、模板等元素。 模板的设计、测试、管理一站式处理。 接口调用简

  • ICMP:报文控制协议

      ICMP经常被认为是IP层的一个组成部分。它传递差错报文以及其他需要注意的信息。ICMP报文通常被IP层或更高层协议(TCP或UDP)使用。一些ICMP报文把差错报文返用户进程。ICMP报文是在IP内部传输的。   所有报文的前4个字节都是一样的,但是剩下的其他字节则互不相同。 ICMP报文类型   不同类型由报文中的类型字段和代码字段来共同决定。   当发送一份ICMP差错报文时,报文始终包含IP的首部和产生ICMP差错报文的IP数据报的前8个字节。这样接收ICMP差错报文的模块就会把它与某个特定的协议(IP首部协议字段类型)和用户进程(根据包含在IP数据报前8个字节中的TCP或UDP报文首部中的TCP或UDP端口号来判断)联系起来。 下面各种情况都不会导致产生ICMP差错报文: ICMP差错报文(但是,ICMP查询报文可能会产生ICM差错报文)。 目的地址是广播地址或多播地址(D类地址)的IP数据报。 作为链路层广播的数据报。 不是IP分片的第一片 源地址不是单个主机的数据报。这就是说,源地址不能为零地址、环回地址、广播地址或多播地址。 ICMP端口不可达差错 UDP

相关推荐

推荐阅读