分布式一致性算法与开源分布式应用具体实现

Paxos算法:

Paxos是目前最广泛流行的分布式一致性算法之一,也是目前被各大开源分布式框架使用较多的算法之一(例如zookeeper的核心算法就是基于Paxos算法优化实现的),它的核心思想就是少数服从多数原则,即对于任意的value的产生都需要进行半数以上通过。

在Paxos算法中存在3个角色:Proposer (提案者-用于生成各提案-其中提案由-提案编号(一般为全局唯一且递增的)+实际值value),Acceptor(接受者:接受Proposer提出的提案并选举最终value),Learner(学习者最终拿到Accpetor选取到的value进行)

对于Paxos算法在选举最终value的整体的过程可以分为两个阶段,prepare与acceptor,在这两个阶段都是Proposer与Accpetor之间进行通信。

在任意的节点下,每个副本同时具有Proposer、Acceptor、Learner三种角色。

第一阶段:proposer生成一个提案编号N(一般都是自增提案编号),携带该编号发送一个prepare请求到超过半数的Acceptor,Acceptor接受该提案有两种选择:

  1. 如果当前Acceptor没有接受过任何提案,那么认为此处是第一次提案,记录当前编号N,并向当前proposer保证不再接受比当前编号N小的提案(这里是小于等于),并且返回null值。
  2. 若当前Acceptor已经存在提案,对比当前提案编号与该Acceptor的所存储的旧的最大提案编号,若是大于则接受该新提案,更改新编号并返回 旧值,否者拒绝该提案不做任何响应。

第二阶段:Proposer接收来自各Acceptor,若接受到超过半数以上的Acceptor响应,就进行第二阶段,否者自增proposerId重新进行第一阶段执行。若当前所有Acceptor响应携带的值为null则该Proposer可提交任何值否则只能使用当前响应的最大编号对应的value,此时Proposer将发送请求到超过半数的Acceptor上(注:第一阶段和第二阶段发送的请求可能落到不同的Acceptor),若Acceptor没有接受到其他Proposer超过N的编号,则该Acceptor接受提案,否则不回复或回复error。但Proposer接受到了超过了半数以上Acceptor的AOK响应 (确定响应)代表提案被最终确定,否则重新开始第一阶段。

经过第一阶段和第二阶段,Proposer肯定会选举产生一个最终value,该值一旦产生可以通过不同的方式直接通知Learner,由Learner具体做不同

Paxos算法与二阶段和三阶段最大不同就是可以保证分布式数据的绝对一致性。

Raft选举机制:

Paxos算法虽然可以保证绝对的一致性,但因为它的难以理解以及工程上的难以实现,所以Raft选举算法应运而生。

Raft选举算法并由Leader节点负责管理日志复制来实现多副本的一致性。

Raft中存在以下3个角色,任意一个分布式节点都可以是以下角色:

领导人(Leader):负责接收客户端的请求,将日志复制到其他节点并告知其他节点何时应用这些日志是安全的

候选者(Candidate):用于选举Leader的一种角色,集群中任意一个节点都可以成为Candidate

跟随者(follower):负责响应来自Leader(进行日志复制)或者Candidate(进行投票选举)的请求

任期的概念:

  1. 每一个server内部都会维护一个任期该任期随着时间自增,并且该值会持久化,任期越大代表这个服务的数据越新。
  2. 每一段任期从一次选举开始,一个或者多个 candidate 尝试成为 leader 。如果一个 candidate 赢得选举,然后他就在该任期剩下的时间里充当 leader 。在某些情况下,一次选举无法选出 leader 。在这种情况下,这一任期会以没有 leader 结束;一个新的任期(包含一次新的选举)会很快重新开始。
  3. 每一个leader都有自己的任期(Raft把时间切割为任意长度的任期,每个任期都有一个任期号,采用连续的整数)

  4. 服务器之间的通信都会携带任期号,follower会根据对比Candidate与本地的任期号的大小决定是否对其投票,Candidate 或者 leader 发现自己的任期号过期了,它会立即回到 follower 状态。

对于raft算法来说,所有服务都可以成为leader,但是我们需要在这些server上尽量选举出更适合的leader,这个塞选条件就是服务器中的数据是不是最新的,用越新的数据的server成为leader之后与各follower之间数据同步所消耗的时间越少,数据的一致性的保障也最高。那么如何来评论数据的新或者不新就从任期(自增)+logid(自增)来判断。

Raft算法的整体实现过程:

  1. Raft 使用一种心跳机制来触发 leader 选举,在server启动的之后每个节点都成为一个follower角色,在定时时间(一般是150ms-300ms之间,并且该值的设定是随机确定)内没有收到leader的心跳检测,那么该follower就变更为Candidate,自增当前的任期(默认情况一个新的节点加入的任期都为0),开始广播信息进行选举,此时Candidate都会投于自己一票
  2. 每一个follower都能进行投票,follower收到Candidate的信息,会比较两这的任期是否相同,如果follower任期大于Candidate会拒绝投票,如果follower任期小于等于当前Candidate,更新自己服务中的任期号并且投出手中同一任期的唯一票(对于同一个任期,每个服务器节点只会投给一个 candidate ,按照先来先服务的顺序),如果该follower收到了后续的大于该任期号的其它请求,更新自己服务中的任期号并且投票
  3. 经过一段时间Candidate会有以下的情况:1.获得过半数以上的投票,则当前Candidate成为Leader,并广播各Follower停止投票,2,未收到过半数请求,自增任期号开始下轮选举,3:收到了来自其它的自称为leader的Candidate的信息,这个时候就需要比较两者的任期号来确定谁更适合成为leader,如果低于leader则该Candidate降为follower,如果高于则仍然保持Candidate 4,可能存在多个Candidate瓜分了选票导致过半这一个条件无法达到,等待选举超时,重置定时器时间等待一下轮的选举。(若出现多个Candidate选举的情况,会存在多个Candidate获取到相同的选票那么此时就会出现选举失败,此时可以通过Candidate随机睡眠一定时间,重新打撒选票,重新开始进行选举。)
  4. leader在没过一小段时间后,就给所有的 Follower 发送一个 Heartbeat 以保持所有节点的状态,Follower 收到 Leader 的 Heartbeat 后重设 Timeout。

一旦选取到领导者,那么后续与客户端的操作都由Leader操作,Leader写入完成同时发送同步数据请求到所有follower(此时的leader中数据是Uncomit状态),follower完成数据预写入(这个时候数据的状态为Uncomit状态)并进行ACK应答,leader收到了过半数的follower的响应,数据Commit直接返回客户端,同时会发送AppendEntries(就是commit请求)请求给follower,follower收到请求进行commit(这里也有个两阶段提交的概念,第一次预提交第二次是真正的落地。这里就会存在一定的数据不一致的情况。例如leader在返回客户端之后就挂机了,那么此时就会导致follower未收到commit请求,那条数据仍处于Uncommit中。这个时候重新进行leader选举,若leader在拥有此条数据的follower中产生,那么仍然可以commit,若被选举的leader中未包含此数据,就会出现丢数据情况),commit成功之后,follower会进行一个ack确认,这里不能保证第二次AppendEntries一次成功,所以,leader如果没有收到某个follower的ACK通知,那么leader会不断给它AppendEntries直到该Follower回应。如果这时候客户端又有新的请求,那么leader给滞后的Follower的AppendEntries里就会包含上一次的第二阶段和新请求的第一阶段leader来调度这些并发请求的顺序,并且保证leader与followers状态的一致性。raft中的做法是,将这些请求以及执行顺序告知followers。leader和followers以相同的顺序来执行这些请求,保证状态一致。若该follower长时间不进行响应,leader会认为该节点挂掉,将其节点从集群中剔除。

开源实现:服务注册与发现中心:nacos中CP(基于Raft算法实现的Nacos CP 也并不是严格的,只是能保证一半所见一致,以及数据的丢失概率较小)

Nacos中Raft算法源码分析(基于Nacos-1.3.0版本,与Raft算法还是有一些不同的区别的):

Nacos是阿里开源的一款集注册中心与配置中心于一体的一个分布式应用组件。官网:http://dubbo.apache.org/zh-cn/docs/user/references/registry/nacos.html。nacos针对于应用环境的不同提供了不同的分布式CAP的实现(Raft选举的CP与参考Eureka Server对等节点的AP)。这里主要讲解的就是raft算法的实现。

nacos 1.3.0是一个springboot项目(所以本质代码走读较其它组件的应用来说来的简单),如下图所示是nacos源码:

 

核心选举代码在RaftCore中(核心代码):


public class RaftCore {

    /**
     * 提供对外访问api路径
     */
    public static final String API_VOTE = UtilsAndCommons.NACOS_NAMING_CONTEXT + "/raft/vote";

    public static final String API_BEAT = UtilsAndCommons.NACOS_NAMING_CONTEXT + "/raft/beat";

    public static final String API_PUB = UtilsAndCommons.NACOS_NAMING_CONTEXT + "/raft/datum";

    public static final String API_DEL = UtilsAndCommons.NACOS_NAMING_CONTEXT + "/raft/datum";

    public static final String API_GET = UtilsAndCommons.NACOS_NAMING_CONTEXT + "/raft/datum";

    public static final String API_ON_PUB = UtilsAndCommons.NACOS_NAMING_CONTEXT + "/raft/datum/commit";

    public static final String API_ON_DEL = UtilsAndCommons.NACOS_NAMING_CONTEXT + "/raft/datum/commit";

    public static final String API_GET_PEER = UtilsAndCommons.NACOS_NAMING_CONTEXT + "/raft/peer";


    private ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);

            t.setDaemon(true);
            t.setName("com.alibaba.nacos.naming.raft.notifier");

            return t;
        }
    });

    //
    public static final Lock OPERATE_LOCK = new ReentrantLock();

    public static final int PUBLISH_TERM_INCREASE_COUNT = 100;
    //记录Listener
    private volatile Map<String, List<RecordListener>> listeners = new ConcurrentHashMap<>();

    private volatile ConcurrentMap<String, Datum> datums = new ConcurrentHashMap<>();

    @Autowired
    //对等节点集合 在选举阶段节点都是定位一致
    private RaftPeerSet peers;

    @Autowired
    private SwitchDomain switchDomain;

    @Autowired
    private GlobalConfig globalConfig;

    @Autowired
    private RaftProxy raftProxy;

    @Autowired

    private RaftStore raftStore;

    //通知者 用于发送信息给其它节点
    public volatile Notifier notifier = new Notifier();

    private boolean initialized = false;

    @PostConstruct
    //在RaftCore初始化之后执行
    public void init() throws Exception {
        //初始化
        Loggers.RAFT.info("initializing Raft sub-system");
        //启动Notifier,轮询Datums,通知RaftListener
        executor.submit(notifier);

        long start = System.currentTimeMillis();

        //用于从磁盘加载Datum和term数据进行数据恢复 并添加到notifier
        raftStore.loadDatums(notifier, datums);
        //初始化当前 term选票 默认为0
        setTerm(NumberUtils.toLong(raftStore.loadMeta().getProperty("term"), 0L));
        //打印日志
        Loggers.RAFT.info("cache loaded, datum count: {}, current term: {}", datums.size(), peers.getTerm());

        while (true) {
            if (notifier.tasks.size() <= 0) {
                break;
            }
            Thread.sleep(1000L);
        }

        initialized = true;

        Loggers.RAFT.info("finish to load data from disk, cost: {} ms.", (System.currentTimeMillis() - start));
        //注册master选举任务 选举超时时间0-15s之间 每TICK_PERIOD_MS=500ms执行一次
        GlobalExecutor.registerMasterElection(new MasterElection());
        //注册一个心跳检测事件 0-5s执行每TICK_PERIOD_MS=500ms执行一次
        GlobalExecutor.registerHeartbeat(new HeartBeat());

        Loggers.RAFT.info("timer started: leader timeout ms: {}, heart-beat timeout ms: {}",
            GlobalExecutor.LEADER_TIMEOUT_MS, GlobalExecutor.HEARTBEAT_INTERVAL_MS);
    }

    public Map<String, List<RecordListener>> getListeners() {
        return listeners;
    }

    public void signalPublish(String key, Record value) throws Exception {

        if (!isLeader()) {
            ObjectNode params = JacksonUtils.createEmptyJsonNode();
            params.put("key", key);
            params.replace("value", JacksonUtils.transferToJsonNode(value));
            Map<String, String> parameters = new HashMap<>(1);
            parameters.put("key", key);

            final RaftPeer leader = getLeader();

            raftProxy.proxyPostLarge(leader.ip, API_PUB, params.toString(), parameters);
            return;
        }

        try {
            OPERATE_LOCK.lock();
            long start = System.currentTimeMillis();
            final Datum datum = new Datum();
            datum.key = key;
            datum.value = value;
            if (getDatum(key) == null) {
                datum.timestamp.set(1L);
            } else {
                datum.timestamp.set(getDatum(key).timestamp.incrementAndGet());
            }

            ObjectNode json = JacksonUtils.createEmptyJsonNode();
            json.replace("datum", JacksonUtils.transferToJsonNode(datum));
            json.replace("source", JacksonUtils.transferToJsonNode(peers.local()));

            onPublish(datum, peers.local());

            final String content = json.toString();

            final CountDownLatch latch = new CountDownLatch(peers.majorityCount());
            for (final String server : peers.allServersIncludeMyself()) {
                if (isLeader(server)) {
                    latch.countDown();
                    continue;
                }
                final String url = buildURL(server, API_ON_PUB);
                HttpClient.asyncHttpPostLarge(url, Arrays.asList("key=" + key), content, new AsyncCompletionHandler<Integer>() {
                    @Override
                    public Integer onCompleted(Response response) throws Exception {
                        if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
                            Loggers.RAFT.warn("[RAFT] failed to publish data to peer, datumId={}, peer={}, http code={}",
                                datum.key, server, response.getStatusCode());
                            return 1;
                        }
                        latch.countDown();
                        return 0;
                    }

                    @Override
                    public STATE onContentWriteCompleted() {
                        return STATE.CONTINUE;
                    }
                });

            }

            if (!latch.await(UtilsAndCommons.RAFT_PUBLISH_TIMEOUT, TimeUnit.MILLISECONDS)) {
                // only majority servers return success can we consider this update success
                Loggers.RAFT.error("data publish failed, caused failed to notify majority, key={}", key);
                throw new IllegalStateException("data publish failed, caused failed to notify majority, key=" + key);
            }

            long end = System.currentTimeMillis();
            Loggers.RAFT.info("signalPublish cost {} ms, key: {}", (end - start), key);
        } finally {
            OPERATE_LOCK.unlock();
        }
    }

    public void signalDelete(final String key) throws Exception {

        OPERATE_LOCK.lock();
        try {

            if (!isLeader()) {
                Map<String, String> params = new HashMap<>(1);
                params.put("key", URLEncoder.encode(key, "UTF-8"));
                raftProxy.proxy(getLeader().ip, API_DEL, params, HttpMethod.DELETE);
                return;
            }

            // construct datum:
            Datum datum = new Datum();
            datum.key = key;
            ObjectNode json = JacksonUtils.createEmptyJsonNode();
            json.replace("datum", JacksonUtils.transferToJsonNode(datum));
            json.replace("source", JacksonUtils.transferToJsonNode(peers.local()));

            onDelete(datum.key, peers.local());

            for (final String server : peers.allServersWithoutMySelf()) {
                String url = buildURL(server, API_ON_DEL);
                HttpClient.asyncHttpDeleteLarge(url, null, json.toString()
                    , new AsyncCompletionHandler<Integer>() {
                        @Override
                        public Integer onCompleted(Response response) throws Exception {
                            if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
                                Loggers.RAFT.warn("[RAFT] failed to delete data from peer, datumId={}, peer={}, http code={}", key, server, response.getStatusCode());
                                return 1;
                            }

                            RaftPeer local = peers.local();

                            local.resetLeaderDue();

                            return 0;
                        }
                    });
            }
        } finally {
            OPERATE_LOCK.unlock();
        }
    }

    public void onPublish(Datum datum, RaftPeer source) throws Exception {
        RaftPeer local = peers.local();
        if (datum.value == null) {
            Loggers.RAFT.warn("received empty datum");
            throw new IllegalStateException("received empty datum");
        }

        if (!peers.isLeader(source.ip)) {
            Loggers.RAFT.warn("peer {} tried to publish data but wasn't leader, leader: {}",
                JacksonUtils.toJson(source), JacksonUtils.toJson(getLeader()));
            throw new IllegalStateException("peer(" + source.ip + ") tried to publish " +
                "data but wasn't leader");
        }

        if (source.term.get() < local.term.get()) {
            Loggers.RAFT.warn("out of date publish, pub-term: {}, cur-term: {}",
                JacksonUtils.toJson(source), JacksonUtils.toJson(local));
            throw new IllegalStateException("out of date publish, pub-term:"
                + source.term.get() + ", cur-term: " + local.term.get());
        }

        local.resetLeaderDue();

        // if data should be persisted, usually this is true:
        if (KeyBuilder.matchPersistentKey(datum.key)) {
            raftStore.write(datum);
        }

        datums.put(datum.key, datum);

        if (isLeader()) {
            local.term.addAndGet(PUBLISH_TERM_INCREASE_COUNT);
        } else {
            if (local.term.get() + PUBLISH_TERM_INCREASE_COUNT > source.term.get()) {
                //set leader term:
                getLeader().term.set(source.term.get());
                local.term.set(getLeader().term.get());
            } else {
                local.term.addAndGet(PUBLISH_TERM_INCREASE_COUNT);
            }
        }
        raftStore.updateTerm(local.term.get());

        notifier.addTask(datum.key, ApplyAction.CHANGE);

        Loggers.RAFT.info("data added/updated, key={}, term={}", datum.key, local.term);
    }

    public void onDelete(String datumKey, RaftPeer source) throws Exception {

        RaftPeer local = peers.local();

        if (!peers.isLeader(source.ip)) {
            Loggers.RAFT.warn("peer {} tried to publish data but wasn't leader, leader: {}",
                JacksonUtils.toJson(source), JacksonUtils.toJson(getLeader()));
            throw new IllegalStateException("peer(" + source.ip + ") tried to publish data but wasn't leader");
        }

        if (source.term.get() < local.term.get()) {
            Loggers.RAFT.warn("out of date publish, pub-term: {}, cur-term: {}",
                JacksonUtils.toJson(source), JacksonUtils.toJson(local));
            throw new IllegalStateException("out of date publish, pub-term:"
                + source.term + ", cur-term: " + local.term);
        }

        local.resetLeaderDue();

        // do apply
        String key = datumKey;
        deleteDatum(key);

        if (KeyBuilder.matchServiceMetaKey(key)) {

            if (local.term.get() + PUBLISH_TERM_INCREASE_COUNT > source.term.get()) {
                //set leader term:
                getLeader().term.set(source.term.get());
                local.term.set(getLeader().term.get());
            } else {
                local.term.addAndGet(PUBLISH_TERM_INCREASE_COUNT);
            }

            raftStore.updateTerm(local.term.get());
        }

        Loggers.RAFT.info("data removed, key={}, term={}", datumKey, local.term);

    }
    //执行选举任务
    public class MasterElection implements Runnable {
        @Override
        public void run() {
            try {

                if (!peers.isReady()) {
                    return;
                }

                RaftPeer local = peers.local();
                //一旦leaderDueMs=0说明选举定时任务已经超期 要进行一轮选举
                local.leaderDueMs -= GlobalExecutor.TICK_PERIOD_MS;

                if (local.leaderDueMs > 0) {
                    return;
                }

                // reset timeout
                //重置选举触发超时时间
                local.resetLeaderDue();
                //重置本地心跳时间 避免触发无效的心跳检测
                local.resetHeartbeatDue();
                //集群广播发送选票
                sendVote();
            } catch (Exception e) {
                Loggers.RAFT.warn("[RAFT] error while master election {}", e);
            }

        }

        public void sendVote() {

            //
            RaftPeer local = peers.get(NetUtils.localServer());
            Loggers.RAFT.info("leader timeout, start voting,leader: {}, term: {}",
                JacksonUtils.toJson(getLeader()), local.term);

            //将所有peer节点的leader置为null
            peers.reset();
            //自增选举周期
            local.term.incrementAndGet();
            //将自身作为候选者 相当于投自己一票
            local.voteFor = local.ip;
            //修改当前节点状态为CANDIDATE候选者
            local.state = RaftPeer.State.CANDIDATE;

            Map<String, String> params = new HashMap<>(1);
            params.put("vote", JacksonUtils.toJson(local));
            //将本地vote 发送除自己以外的其它节点
            for (final String server : peers.allServersWithoutMySelf()) {
                final String url = buildURL(server, API_VOTE);
                try {
                    //异步发送http请求 这里跳转到对应的response接口
                    HttpClient.asyncHttpPost(url, null, params, new AsyncCompletionHandler<Integer>() {
                        @Override
                        public Integer onCompleted(Response response) throws Exception {
                            if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
                                Loggers.RAFT.error("NACOS-RAFT vote failed: {}, url: {}", response.getResponseBody(), url);
                                return 1;
                            }

                            //收到发送节点的response响应 这里接受者投自己认为最合适的leader一票 不一定是当前发送投票的候选者
                            RaftPeer peer = JacksonUtils.toObj(response.getResponseBody(), RaftPeer.class);

                            Loggers.RAFT.info("received approve from peer: {}", JacksonUtils.toJson(peer));
                            //收到请求之后马上 归档票数是否满足过半要求
                            peers.decideLeader(peer);

                            return 0;
                        }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值