作为注册中心对比
| 方面 | ZooKeeper作为注册中心 | Nacos(服务注册发现,配合负载均衡) |
|---|---|---|
| 数据一致性角色 | 内部有Leader/Follower,用于保证注册数据的一致性。 | 内部有Raft协议选举Leader,或使用Distro协议,目的相同。 |
| 服务节点角色 | 注册的服务实例是对等的。 | 注册的服务实例是对等的。 |
| 服务发现读请求 | 客户端可从任何ZK节点(通常是Follower)读取服务列表。 | 客户端可从任何Nacos节点读取服务列表。 |
| 流量路由决策 | 在客户端通过负载均衡器做出。 | 在客户端通过负载均衡器(如Ribbon)做出。 |
| 业务流量路径 | 直接 从调用者到被调用的服务实例,不经过ZK。 | 直接 从调用者到被调用的服务实例,不经过Nacos。 |
Zookeeper数据一致性(CP)
实现机制:
-
ZAB协议 :ZooKeeper使用ZAB(ZooKeeper Atomic Broadcast)协议来保证集群中各个节点状态的强一致性。这个协议与Paxos算法类似,能确保 写操作在所有节点上以相同的顺序被提交。
-
Leader写 :所有写请求都必须由Leader节点处理,并由Leader将更新广播给Follower节点。只有当超过半数的节点(Quorum)都确认写入后,这次写操作才会被提交,并向客户端返回成功。(Follower节点可以提供读服务)
-
健康检查:基于临时节点(Ephemeral Nodes)。客户端与ZooKeeper服务器维持一个会话(Session)。如果会话超时,则该客户端创建的所有临时节点会被自动删除。
在ZooKeeper的CP模型中,当发生网络分区时,为了保证数据的一致性,它会让少数派分区(无法形成Quorum)的节点不可用。对于服务注册中心来说,这意味着部分服务调用者可能会暂时无法获取服务列表,但获取到的列表一定是与主分区一致的。
问题:多数节点同意为啥保证了强一致性
多数节点同意并不代表所有节点同意,所以为啥ZAB保证了强一致性?
1. ZAB协议
ZAB协议的核心:两阶段提交 + 事务ID
ZAB协议的写操作(如你提到的set命令)并不是简单地"收到多数派确认就返回",它包含一个严谨的两阶段过程:
阶段一:Leader发起提案(Broadcast)
-
Leader为这个写请求分配一个全局单调递增的事务ID。
-
Leader将这个提案(包含新的数据内容和ZXID)持久化到自己的本地磁盘。
-
Leader将提案发送给所有的Follower(包括你例子中的A, B, C)。
阶段二:Leader提交提案(Commit)
-
Leader等待超过半数的Follower (包括自己)返回ACK确认。在你的三节点集群中,多数派是2,所以当Leader收到A的ACK时,加上自己,就已经满足了多数派(A + Leader = 2)。
-
关键点来了 :此时,Leader虽然满足了多数派,但它不会立即向客户端返回成功 。它还需要做一件事:向所有已经ACK的Follower (在这个例子里是A)发送一个
COMMIT消息 。这个COMMIT消息的意思是:"我之前发给你的那个提案,现在可以正式生效了,把它应用到你的内存状态机里。" -
只有当Leader自己也应用了这个提案,并且(在某些实现中)发送了
COMMIT之后,它才会向客户端返回写操作成功的响应。
2.读请求保证一致性
现在,如果一个读请求发到了C节点,会发生什么?
-
Follower的自我认知 :在ZooKeeper中,Follower不能独立处理写请求,同时对于读请求,它也必须确保自己的数据不是过时的。
-
Sync机制 :为了处理读请求,Follower C首先需要与Leader进行一次
sync操作。这个操作的目的就是将自己的状态与Leader同步到最新。 -
C节点的行为:
-
当读请求到达C时,C会发现自己的ZXID(99)落后于它所知的Leader的ZXID。
-
C会拒绝直接提供这个可能过期的数据。
-
C会尝试与Leader同步。由于网络分区,这个同步请求会失败。
-
因为同步失败,C会认为自己的数据不是最新的,进而会拒绝客户端的这个读请求 ,或者让客户端超时。最终,客户端的ZooKeeper客户端库会将连接重定向到一个健康的、数据最新的节点(比如A或B)。
-
问题:读操作Follower节点的同步是怎样的?
Follower节点在提供读服务前,会通过一种机制(显式的sync或隐式的连接健康度检查)来评估自己的数据是否足够新。如果评估失败,它宁可拒绝服务也不会返回可能过期的数据,以此坚决捍卫CP特性。
Follower节点提供读服务时,需要判断其与Leader节点是否同步。这个"判断"过程有显式和隐式两种方式,并且根据ZooKeeper的版本和配置,其严格程度也有所不同。下面来详细拆解一下:
1. 显式同步:sync 操作
-
流程:
-
客户端向Follower发起一个读请求。
-
在Follower返回数据之前 ,它会先向Leader发送一个特殊的
sync请求。 -
Leader会阻塞这个
sync请求,Leader 主动检查并推送 Follower 缺失的所有数据,主动帮助 Follower 的 ZXID 与自己保持一致。 -
Leader回复Follower
sync完成。 -
Follower确认自己的数据是最新的,然后才执行本地读操作,将结果返回给客户端。
-
-
优点 :提供了线性一致性的读保证。客户端读到的数据一定是最新的,并且能看到之前所有已完成的写操作。
-
缺点 :延迟高。每次读操作都可能需要一次与Leader的网络往返,增加了延迟,失去了在Follower上读操作降低负载的意义。
2. 隐式检查:会话与连接状态
这是更常见和默认的行为,它依赖于Follower与Leader之间的心跳和通信状态。
-
流程:
-
Follower与Leader维持着一个心跳连接。通过这个连接,Follower不断地知道Leader的最新ZXID。
-
当客户端向Follower发起读请求时,Follower会进行一个本地的、快速的检查:
-
检查1:连接状态。我(Follower)和Leader的连接是否健康?如果我最近刚和Leader有过成功的心跳,说明我同步得很好。
-
检查2:ZXID差距(逻辑上的) 。虽然Follower不会在每次读请求时都去精确比对ZXID,但它通过心跳和来自Leader的数据包,大致知道自己的数据是否滞后。如果它发现自己已经有一段时间没有收到Leader的消息,或者明确知道自己的ZXID远落后于Leader,它就会认为自己状态可疑。
-
-
-
当Follower认为自己"落后"或"失联"时:
-
它会拒绝客户端的读请求。
-
它会关闭与客户端的会话,或者让客户端的请求超时。
-
客户端的ZooKeeper库在收到错误或超时后,会自动重连到集群中的另一个节点(很可能是一个数据更新的Follower或Leader本身)。
-
Nacos的高可用(AP)
作为服务注册中心,服务的可用性 和注册中心的自身可用性至关重要。它优先保证在绝大多数情况下(包括网络分区时)服务都能被注册和发现,即使读到的信息可能不是最新的。
实现机制:
-
自研协议(AP模式) :Nacos默认使用一种自研的、基于异步复制 的协议(
Distro协议)。节点之间相互对等,服务实例向某个Nacos节点注册后,该节点会异步地将信息传播给其他节点。 -
客户端心跳 :服务实例定期向Nacos服务器发送心跳来维持注册状态。如果一个Nacos节点长时间收不到某服务实例的心跳,它会将该实例标记为不健康,但不会立即删除。这个信息也会异步地传播。
-
可切换的CP模式 :对于配置信息等需要强一致性的场景,Nacos支持切换到CP模式,使用Raft协议 来选举Leader并实现强一致性。但服务发现功能默认是AP的。
- 服务发现AP, 意思是说服务发现时,请求的当前Nacos节点并不会像Zookeeper中那样判断与主节点ZXID的同步性。
在网络分区下的表现 :
假设一个由3个节点(N1, N2, N3)组成的Nacos集群。
-
发生网络分区,将N1和N2与N3隔开。
-
此时,两个分区都无法感知到对方的存在 ,但它们都仍然认为自己可以提供服务。
-
服务实例A注册在N1上,服务实例B注册在N3上。
-
连接到N1或N2的客户端,能看到服务A,但可能看不到服务B(或者看到B但标记为不健康)。
-
连接到N3的客户端,能看到服务B,但可能看不到服务A。
-
双方都仍然可以工作 ,都可以进行服务注册和发现,但看到的数据视图是不一致的。
总结:在Nacos的AP模型中,当发生网络分区时,为了保证服务的可用性,它允许各个分区继续独立工作,但牺牲了数据的强一致性。你可能会读到旧数据或不完整的数据,但你的服务调用不会因为注册中心本身的问题而完全失败。