JAVA面试题分享二百七十七:Eureka怎么AP?Nacos既CP又AP,怎么实现的?

目录

注册中心集群的数据一致性问题

CAP定理

微服务注册中心是AP还是CP

Eureka 的数据同步方式

多个副本之间的 复制方式

Eureka  的Peer to Peer 模式同步过程

循环复制问题

总结一下,Eureka 的数据同步方式

Nacos 满足AP,又满足CP

快速了解Distro 协议

Distro 节点新加入集群场景

心跳场景

写操作场景

读操作场景

总结一下,Distro 的数据同步

快速了解Raft协议

Raft算法选主流程

1.Term

2.RPC

3.选举流程

4.日志复制

如何实现Raft算法

启动选举

选举流程

心跳机制


注册中心集群的数据一致性问题

服务注册中心必然是高可用的,这意味着它不能是单点的,而必须是一个注册中心集群。

接下来的问题是:

在一个微服务注册中心集群中,如何确保微服务 Provider 提供者的注册信息或元数据信息保持一致性?

首先,回顾一下CAP定理。

CAP定理

分布式系统中有一个重要理论:CAP。

  1. C:一致性(Consistency)

    在分布式系统中,数据会在多个副本中存在,一些问题可能导致在写入数据时,部分副本成功,部分副本失败,从而导致数据不一致。一致性 C 的要求是,数据更新操作成功后,多个副本的数据必须保持一致。

  2. A:可用性(Availability)

    无论何时,客户端对集群进行读写操作,请求都应能得到正常的响应。

  3. P:分区容错性(Partition Tolerance)

    当发生通信故障,集群被分割成多个无法通信的分区时,集群仍应能正常运行。

微服务注册中心是AP还是CP

回到微服务注册中心的场景。

微服务注册中心的中间件非常多,比如传统的分布式协调组件 Zookeeper, 比如 传统的微服务注册中心 Eureka,比如 阿里的微服务注册中心Nacos,还有 google的 分布式协调组件 etcd,等等。

微服务注册中心是AP还是CP?

首先要明确的是 Eureka 是AP 高并发类型,不是CP强一致类型,而是弱数据一致性的。

ZooKeeper 是CP类型的注册中心,就是尽可能的保证强数据一致性,ZooKeeper首先牺牲A,另外,在某些情况下可以牺牲可用性P。

所以, Eureka 与ZooKeeper 完全是两个极端。

Eureka 则选择了 A,ZooKeeper 优先选择了 C。

Eureka 具有高可用性,在任何时候,服务消费者都能正常获取服务列表,但不保证数据的强一致性,消费者可能会拿到过期的服务列表

Nacos 则做了兼容,既能支持AP模式,也能支持CP模式。

Spring Cloud Alibaba Nacos 在 1.0.0 正式支持 AP 和 CP 两种一致性协议,其中,CP 一致性协议的实现是基于简化的 Raft 协议的强一致性实现。

Eureka 的数据同步方式

多个副本之间的 复制方式

首先看看 数据同步的方式,或者说多个副本的 复制方式。通常,分布式系统中的数据在多个副本间的复制方式,大体上可以分为以下两种:

  • 主从复制

这种是 Master-Slave 模式,存在一个 master 主副本,其他则是 Slave 从副本,所有的写操作都会被提交到主副本,然后由主副本更新到其他从副本。

因此,写压力都会聚集在主副本上,这成为了系统的瓶颈,而从副本则可以分担读请求。

  • 对等复制

这种是 Peer to Peer 模式,副本之间不存在主从之分,任何一个副本都可以接收写操作,然后各个副本之间会相互进行数据更新。

Peer to Peer 对等复制模式的优势:

任何一个副本都可以接收写请求,不存在写压力的瓶颈,但是在各个副本间进行数据同步时可能会出现数据冲突。

Eureka 就是采用了 Peer to Peer 模式。

Eureka  的Peer to Peer 模式同步过程

在 Eureka Server 启动之后,它会利用本地的 Eureka Client 向其他 Eureka Server 节点中的一个节点发起请求,以获取注册的服务信息,并将这些信息复制到其他 peer 节点。

每当 Eureka Server 的自身信息发生变化,例如微服务的客户端向它发起注册、续约或注销请求时,它会将最新的信息推送给其他 Eureka Server,以保持数据的同步性。

循环复制问题

当然,这里有一个问题:循环复制问题。

具体来说,如果自身的信息变更是由另一个 Eureka Server 同步过来的,那么如果再将这些信息同步回去,就会出现数据同步的死循环。

在 Eureka Server 执行复制操作时,它会使用一个名为 HEADER_REPLICATION 的 http header 来区分复制操作。

如果一个请求携带了 HEADER_REPLICATION 这个 header,那么这个请求就不再是普通应用实例微服务的客户端的正常请求,而是来自其他 server 的复制请求。这样,当 Eureka Server 收到复制请求时,它就不会再执行复制操作,从而避免了死循环。

还有一个问题,就是数据冲突。

比如 server A 向 server B 发起同步请求,如果 A 的数据比 B 的还旧,那么 B 不可能接受 A 的数据。在这种情况下,B 如何知道 A 的数据是旧的呢?这时 A 又应该怎么办呢?

数据的新旧通常是通过版本号来定义的,Eureka 使用 lastDirtyTimestamp 这个类似版本号的属性来实现。

lastDirtyTimestamp 是注册中心中服务实例的一个属性,它表示此服务实例最近一次变更时间。

节点间的复制,可能会出错,如何进行错误的检测和弥补呢?

此外,Eureka 集群中,还有一个重要的机制:hearbeat 心跳,即续约操作,用于完成数据的最终修复。由于节点间复制可能出现错误,我们可以通过心 beat 机制来发现并修复这些错误。

总结一下,Eureka 的数据同步方式

  • Eureka 使用 Peer to Peer 模式进行数据复制。

  • Eureka 通过 http header就是 HEADER_REPLICATION  解决循环复制问题。

  • Eureka 通过 lastDirtyTimestamp 解决复制冲突。

  • Eureka 通过心跳机制实现数据修复。

Nacos 满足AP,又满足CP

与Eureka 、Zookeeper集群不同Nacos 既能支持AP,又能支持 CP。

Nacos 支持 CP+AP 模式,这意味着 Nacos 可以根据配置识别为 CP 模式或 AP 模式,默认情况下为 AP 模式。

  • 如果注册Nacos的client节点注册时ephemeral=true,那么Nacos集群对这个client节点的效果就是AP,采用distro协议实现;

  • 而注册Nacos的client节点注册时ephemeral=false,那么Nacos集群对这个节点的效果就是CP的,采用raft协议实现。

根据client注册时的属性,AP,CP同时混合存在,只是对不同的client节点效果不同。

因此,Nacos 能够很好地满足不同场景的业务需求。

快速了解Distro 协议

Distro 协议是 Nacos 自主研发的一种 AP 分布式协议,专为临时实例设计,确保在部分 Nacos 节点宕机时,整个临时实例仍可正常运行。

作为一款具有状态的中间件应用的内置协议,Distro 确保了各 Nacos 节点在处理大量注册请求时的统一协调和存储。

Distro 协议 与Eureka Peer to Peer 模式同步过程, 大致是类似的。

Distro 协议的同步过程,大致如下:

  • 每个节点是平等的都可以处理写请求,同时将新数据同步至其他节点。

  • 每个节点只负责部分数据,定时发送自己负责数据的校验值,到其他节点来保持数据⼀致性。

  • 每个节点独立处理读请求,并及时从本地发出响应。

接下来的几节将通过不同的场景介绍 Distro 协议的工作原理。

Distro 节点新加入集群场景

新加入的 Distro 节点,会进行全量数据拉取。

具体操作是依次访问所有 Distro 节点,通过向其他机器发送请求,来拉取全量数据

在完成全量拉取操作后,Nacos 的每台机器都维护了当前所有注册的非持久化实例数据。

心跳场景

在 Distro 集群启动后,各台机器之间会定期发送心跳。

心跳信息主要包括各机器上的所有数据的元信息(使用元信息是为了确保网络中数据传输量维持在较低水平)。这种数据校验以心跳形式进行,即每台机器在固定时间间隔内向其他机器发起一次数据校验请求。

如果在数据校验过程中,某台机器发现其他机器上的数据与本地数据不一致,会发起一次全量拉取请求,将数据补全。

写操作场景

对于⼀个已经启动完成的 Distro 集群,在⼀次客户端发起写操作的流程中,当注册非持久化的实例的写请求打到某台 Nacos 服务器时,Distro 集群处理的流程图如下。

整个步骤包括几个部分(图中从上到下顺序):

  • 前置的 Filter 拦截请求,并根据请求中包含的 IP 和 port 信息计算其所属的 Distro 责任节点,并将该请求转发到所属的 Distro 责任节点上。

  • 责任节点上的 Controller 对写请求进行解析。

  • Distro 协议定期执行 Sync 任务,将本机所负责的所有实例信息同步到其他节点上。

读操作场景

由于每台机器上都存储了全量数据,因此在每次读操作中,Distro 机器会直接从本地获取数据,实现快速响应。

这种机制确保了 Distro 协议可以作为 AP 协议,对读操作进行及时响应。

  • 在网络分区状况下,所有读操作仍可正常返回结果;

  • 当网络恢复时,各 Distro 节点会将各数据片段进行合并恢复。

总结一下,Distro 的数据同步

Distro 协议是 Nacos 针对临时实例数据开发的⼀致性协议。

数据存储在缓存中,并在启动时进行全量数据同步,定期执行数据校验

遵循 Distro 协议的设计理念,每个 Distro 节点均能接收读写请求。Distro 协议的请求场景主要分为以下三种情况:

  1. 当该节点接收到属于该节点负责的实例的写请求时,直接写入。

  2. 当该节点接收到不属于该节点负责的实例的写请求时,将在集群内部路由,转发给对应的节点,从而完成读写。

  3. 当该节点接收到任何读请求时,都直接在本机查询并返回(因为所有实例都被同步到了每台机器上)。

作为 Nacos 的内置临时实例一致性协议,Distro 协议确保了在分布式环境中,每个节点上的服务信息状态能够及时通知其他节点,支持数十万量级服务实例的存储和一致性维护。

快速了解Raft协议

Spring Cloud Alibaba Nacos 在 1.0.0 正式支持 AP 和 CP 两种一致性协议,其中的CP一致性协议实现,是基于简化的 Raft 的 CP 一致性。

Raft 适用于一个管理日志一致性的协议,相比于 Paxos 协议, Raft 更易于理解和去实现它。

为了提高理解性,Raft 将一致性算法分为了几个部分,包括领导选取(leader selection)、日志复制(log replication)、安全(safety),并且使用了更强的一致性来减少了必须需要考虑的状态。

相比Paxos,Raft算法理解起来更加直观。

Raft算法将Server划分为3种状态,或者也可以称作角色:

  • Leader:负责Client交互和log复制,同一时刻系统中最多存在1个。

  • Follower:被动响应请求RPC,从不主动发起请求RPC。

  • Candidate:一种临时的角色,只存在于leader的选举阶段,某个节点想要变成leader,那么就发起投票请求,同时自己变成candidate。如果选举成功,则变为candidate,否则退回为follower

状态或者说角色的流转如下:

在Raft中,问题被分解为:领导选取、日志复制、安全和成员变化。

通过复制日志来实现状态机的复制:

日志:每台机器都保存一份日志,日志来源于客户端的请求,包含一系列的命令。

状态机:状态机会按顺序执行这些命令。

一致性模型:在分布式环境中,确保多台机器的日志保持一致,从而使状态机回放时的状态保持一致。

Raft算法选主流程

Raft中使用心跳机制来出发leader选举。当服务器启动的时候,服务器成为follower。只要follower从leader或者candidate收到有效的RPCs就会保持follower状态。如果follower在一段时间内(该段时间被称为election timeout)没有收到消息,则它会假设当前没有可用的leader,然后开启选举新leader的流程。

1.Term

Term的概念类比中国历史上的朝代更替,Raft 算法将时间划分成为任意不同长度的任期(term)。

任期用连续的数字进行表示。每一个任期的开始都是一次选举(election),一个或多个候选人尝试成为领导者。如果一个候选人赢得选举,它将在该任期的剩余时间内担任领导者。在某些情况下,选票可能会被平分,导致没有选出领导者,此时将开始新的任期并立即进行下一次选举。Raft 算法确保在给定的任期中只有一个领导者。

2.RPC

Raft 算法中服务器节点之间通信使用远程过程调用(RPCs),并且基本的一致性算法只需要两种类型的 RPCs,为了在服务器之间传输快照增加了第三种 RPC。

RPC有三种:

  • RequestVote RPC:候选人在选举期间发起

  • AppendEntries RPC:领导人发起的一种心跳机制,复制日志也在该命令中完成

  • InstallSnapshot RPC:领导者使用该RPC来发送快照给太落后的追随者

3.选举流程

(1)follower增加当前的term,转变为candidate。

(2)candidate投票给自己,并发送RequestVote RPC给集群中的其他服务器。

(3)收到RequestVote的服务器,在同一term中只会按照先到先得投票给至多一个candidate。且只会投票给log至少和自身一样新的candidate。

初始节点

Node1 转为 Candidate 发起选举

Node 确认选举

Node1 成为 leader,发送 Heartbeat

candidate节点保持(2)的状态,直到下面三种情况中的一种发生。

  • 该节点赢得选举,即收到大多数节点的投票,然后转变为 leader 状态。

  • 另一个服务器成为 leader,即收到合法心跳包(term 值大于或等于当前自身 term 值),然后转变为 follower 状态。

  • 一段时间后仍未确定胜者,此时会启动新一轮的选举。

为了解决当票数相同时无法确定 leader 的问题,Raft 使用随机选举超时时间。

4.日志复制

日志复制(Log Replication)的主要目的是确保节点的一致性,在此阶段执行的操作都是为了确保一致性和高可用性。

当 Leader 选举产生后,它开始负责处理客户端的请求。所有的事务(更新操作)请求都必须先由 Leader 处理。日志复制(Log Replication)就是为了确保执行相同的操作序列所做的工作。

在 Raft 中,当接收到客户端的日志(事务请求)后,先把该日志追加到本地的Log中,然后通过heartbeat把该Entry同步给其他Follower,Follower接收到日志后记录日志然后向Leader发送ACK,当Leader收到大多数(n/2+1)Follower的ACK信息后将该日志设置为已提交并追加到本地磁盘中,通知客户端并在下个heartbeat中Leader将通知所有的Follower将该日志存储在自己的本地磁盘中。

如何实现Raft算法

Nacos server在启动时,会通过RunningConfig.onApplicationEvent()方法调用RaftCore.init()方法。

启动选举
public static void init() throws Exception {
 
    Loggers.RAFT.info("initializing Raft sub-system");
 
    // 启动Notifier,轮询Datums,通知RaftListener
    executor.submit(notifier);
     
    // 获取Raft集群节点,更新到PeerSet中
    peers.add(NamingProxy.getServers());
 
    long start = System.currentTimeMillis();
 
    // 从磁盘加载Datum和term数据进行数据恢复
    RaftStore.load();
 
    Loggers.RAFT.info("cache loaded, peer count: {}, datum count: {}, current term: {}",
        peers.size(), datums.size(), peers.getTerm());
 
    while (true) {
        if (notifier.tasks.size() <= 0) {
            break;
        }
        Thread.sleep(1000L);
        System.out.println(notifier.tasks.size());
    }
 
    Loggers.RAFT.info("finish to load data from disk, cost: {} ms.", (System.currentTimeMillis() - start));
 
    GlobalExecutor.register(new MasterElection()); // Leader选举
    GlobalExecutor.register1(new HeartBeat()); // Raft心跳
    GlobalExecutor.register(new AddressServerUpdater(), GlobalExecutor.ADDRESS_SERVER_UPDATE_INTERVAL_MS);
 
    if (peers.size() > 0) {
        if (lock.tryLock(INIT_LOCK_TIME_SECONDS, TimeUnit.SECONDS)) {
            initialized = true;
            lock.unlock();
        }
    } else {
        throw new Exception("peers is empty.");
    }
 
    Loggers.RAFT.info("timer started: leader timeout ms: {}, heart-beat timeout ms: {}",
        GlobalExecutor.LEADER_TIMEOUT_MS, GlobalExecutor.HEARTBEAT_INTERVAL_MS);
}

在init方法主要做了如下几件事:

  1. 获取Raft集群节点 peers.add(NamingProxy.getServers());

  2. Raft集群数据恢复 RaftStore.load();

  3. Raft选举 GlobalExecutor.register(new MasterElection());

  4. Raft心跳 GlobalExecutor.register(new HeartBeat());

  5. Raft发布内容

  6. Raft保证内容一致性

选举流程

其中,raft集群内部节点间是通过暴露的Restful接口,代码在 RaftController 中。RaftController控制器是raft集群内部节点间通信使用的,具体的信息如下

POST HTTP://{ip:port}/v1/ns/raft/vote : 进行投票请求

POST HTTP://{ip:port}/v1/ns/raft/beat : Leader向Follower发送心跳信息

GET HTTP://{ip:port}/v1/ns/raft/peer : 获取该节点的RaftPeer信息

PUT HTTP://{ip:port}/v1/ns/raft/datum/reload : 重新加载某日志信息

POST HTTP://{ip:port}/v1/ns/raft/datum : Leader接收传来的数据并存入

DELETE HTTP://{ip:port}/v1/ns/raft/datum : Leader接收传来的数据删除操作

GET HTTP://{ip:port}/v1/ns/raft/datum : 获取该节点存储的数据信息

GET HTTP://{ip:port}/v1/ns/raft/state : 获取该节点的状态信息{UP or DOWN}

POST HTTP://{ip:port}/v1/ns/raft/datum/commit : Follower节点接收Leader传来得到数据存入操作

DELETE HTTP://{ip:port}/v1/ns/raft/datum : Follower节点接收Leader传来的数据删除操作

GET HTTP://{ip:port}/v1/ns/raft/leader : 获取当前集群的Leader节点信息

GET HTTP://{ip:port}/v1/ns/raft/listeners : 获取当前Raft集群的所有事件监听者
RaftPeerSet

心跳机制

Raft中使用心跳机制来触发leader选举。

心跳定时任务是在GlobalExecutor 中,通过 GlobalExecutor.register(new HeartBeat())注册心跳定时任务,具体操作包括:

  • 重置Leader节点的heart timeout、election timeout;

  • sendBeat()发送心跳包

public class HeartBeat implements Runnable {
    @Override
    public void run() {
        try {

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

            RaftPeer local = peers.local();
            local.heartbeatDueMs -= GlobalExecutor.TICK_PERIOD_MS;
            if (local.heartbeatDueMs > 0) {
                return;
            }

            local.resetHeartbeatDue();

            sendBeat();
        } catch (Exception e) {
            Loggers.RAFT.warn("[RAFT] error while sending beat {}", e);
        }
    }
}

简单说明了下Nacos中的Raft一致性实现,更详细的流程,可以下载源码,查看 RaftCore 进行了解。

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值