赞
踩
Dubbo 官方推荐使用 ZooKeeper 作为注册中心,它是在实际生产中最常用的注册中心实现,这也是我们本课时要介绍 ZooKeeper 核心原理的原因。
要与 ZooKeeper 集群进行交互,我们可以使用 ZooKeeper 原生客户端或是 ZkClient、Apache Curator 等第三方开源客户端。在后面介绍 dubbo-registry-zookeeper 模块的具体实现时你会看到,Dubbo 底层使用的是 Apache Curator。Apache Curator 是实践中最常用的 ZooKeeper 客户端。
如果你对zooper 不是很了解可以仔细阅读这一篇文章也可以阅读zookeeper 专栏的文章
Apache ZooKeeper 是一个针对分布式系统的、可靠的、可扩展的协调服务,它通常作为统一命名服务、统一配置管理、注册中心(分布式集群管理)、分布式锁服务、Leader 选举服务等角色出现。很多分布式系统都依赖与 ZooKeeper 集群实现分布式系统间的协调调度,例如:Dubbo、HDFS 2.x、HBase、Kafka 等。ZooKeeper 已经成为现代分布式系统的标配。
ZooKeeper 本身也是一个分布式应用程序,下图展示了 ZooKeeper 集群的核心架构。
ZooKeeper 集群的核心架构图
了解了 ZooKeeper 整体的架构之后,我们再来了解一下 ZooKeeper 集群存储数据的逻辑结构。ZooKeeper 逻辑上是按照树型结构进行数据存储的(如下图),其中的节点称为 ZNode。每个 ZNode 有一个名称标识,即树根到该节点的路径(用 “/” 分隔),ZooKeeper 树中的每个节点都可以拥有子节点,这与文件系统的目录树类似。
ZNode 节点类型有如下四种:
在每个 ZNode 中都维护着一个 stat 结构,记录了该 ZNode 的元数据,其中包括版本号、操作控制列表(ACL)、时间戳和数据长度等信息,如下表所示:
我们除了可以通过 ZooKeeper Client 对 ZNode 进行增删改查等基本操作,还可以注册 Watcher 监听 ZNode 节点、其中的数据以及子节点的变化。一旦监听到变化,则相应的 Watcher 即被触发,相应的 ZooKeeper Client 会立即得到通知。Watcher 有如下特点:
ZooKeeper 集群中三种角色的节点(Leader、Follower 和 Observer)都可以处理 Client 的读请求,因为每个节点都保存了相同的数据副本,直接进行读取即可返回给 Client。
对于写请求,如果 Client 连接的是 Follower 节点(或 Observer 节点),则在 Follower 节点(或 Observer 节点)收到写请求将会被转发到 Leader 节点。下面是 Leader 处理写请求的核心流程:
上面写请求处理流程中,如果发生 Leader 节点宕机,整个 ZooKeeper 集群可能处于如下两种状态:
在 Leader 宕机后,ZooKeeper 会进入崩溃恢复模式,重新进行 Leader 节点的选举。
ZooKeeper 对新 Leader 有如下两个要求:
ZooKeeper 选主使用的是 ZAB 协议,如果展开介绍的话内容会非常多,这里我们就通过一个示例简单介绍 ZooKeeper 选主的大致流程。
比如,当前集群中有 5 个 ZooKeeper 节点构成,sid 分别为 1、2、3、4 和 5,zxid 分别为 10、10、9、9 和 8,此时,sid 为 1 的节点是 Leader 节点。实际上,zxid 包含了 epoch(高 32 位)和自增计数器(低 32 位) 两部分。其中,epoch 是“纪元”的意思,标识当前 Leader 周期,每次选举时 epoch 部分都会递增,这就防止了网络隔离之后,上一周期的旧 Leader 重新连入集群造成不必要的重新选举。该示例中我们假设各个节点的 epoch 都相同。
某一时刻,节点 1 的服务器宕机了,ZooKeeper 集群开始进行选主。由于无法检测到集群中其他节点的状态信息(处于 Looking 状态),因此每个节点都将自己作为被选举的对象来进行投票。于是 sid 为 2、3、4、5 的节点,投票情况分别为(2,10)、(3,9)、(4,9)、(5,8),同时各个节点也会接收到来自其他节点的投票(这里以(sid, zxid)的形式来标识一次投票信息)。
对于节点 2 来说,接收到(3,9)、(4,9)、(5,8)的投票,对比后发现自己的 zxid 最大,因此不需要做任何投票变更。
对于节点 3 来说,接收到(2,10)、(4,9)、(5,8)的投票,对比后由于 2 的 zxid 比自己的 zxid 要大,因此需要更改投票,改投(2,10),并将改投后的票发给其他节点。
对于节点 4 来说,接收到(2,10)、(3,9)、(5,8)的投票,对比后由于 2 的 zxid 比自己的 zxid 要大,因此需要更改投票,改投(2,10),并将改投后的票发给其他节点。
对于节点 5 来说,也是一样,最终改投(2,10)。
经过第二轮投票后,集群中的每个节点都会再次收到其他机器的投票,然后开始统计投票,如果有过半的节点投了同一个节点,则该节点成为新的 Leader,这里显然节点 2 成了新 Leader节点。
Leader 节点此时会将 epoch 值加 1,并将新生成的 epoch 分发给各个 Follower 节点。各个 Follower 节点收到全新的 epoch 后,返回 ACK 给 Leader 节点,并带上各自最大的 zxid 和历史事务日志信息。Leader 选出最大的 zxid,并更新自身历史事务日志,示例中的节点 2 无须更新。Leader 节点紧接着会将最新的事务日志同步给集群中所有的 Follower 节点,只有当半数 Follower 同步成功,这个准 Leader 节点才能成为正式的 Leader 节点并开始工作。
重点介绍了 ZooKeeper 的核心概念以及 ZooKeeper 集群的基本工作原理:
首先介绍了 ZooKeeper 集群中各个节点的角色以及职能;
然后介绍了 ZooKeeper 中存储数据的逻辑结构以及 ZNode 节点的相关特性;
紧接着又讲解了 ZooKeeper 集群读写数据的核心流程;
最后我们通过示例分析了 ZooKeeper 集群的崩溃恢复流程。
ZooKeeper 官方提供的客户端支持了一些基本操作,例如,创建会话、创建节点、读取节点、更新数据、删除节点和检查节点是否存在等,但在实际开发中只有这些简单功能是根本不够的。而且,ZooKeeper 本身的一些 API 也存在不足,例如:
ZkClient 是在 ZooKeeper 原生 API 接口的基础上进行了包装,虽然 ZkClient 解决了 ZooKeeper 原生 API 接口的很多问题,提供了非常简洁的 API 接口,实现了会话超时自动重连的机制,解决了 Watcher 反复注册等问题,但其缺陷也非常明显。例如,文档不全、重试机制难用、异常全部转换成了 RuntimeException、没有足够的参考示例等。可见,一个简单易用、高效可靠的 ZooKeeper 客户端是多么重要。
客户端介绍这个不赘述了,参考 zookeeper基础学习之六: zookeeper java客户端curator
为了避免 curator-framework 包过于膨胀,Curator 将很多其他解决方案都拆出来了,作为单独的一个包,例如:curator-recipes、curator-x-discovery、curator-x-rpc 等。
在后面我们会使用到 curator-x-discovery 来完成一个简易 RPC 框架的注册中心模块。curator-x-discovery 扩展包是一个服务发现的解决方案。在 ZooKeeper 中,我们可以使用临时节点实现一个服务注册机制。当服务启动后在 ZooKeeper 的指定 Path 下创建临时节点,服务断掉与 ZooKeeper 的会话之后,其相应的临时节点就会被删除。这个 curator-x-discovery 扩展包抽象了这种功能,并提供了一套简单的 API 来实现服务发现机制。curator-x-discovery 扩展包的核心概念如下:
ServiceInstance。 这是 curator-x-discovery 扩展包对服务实例的抽象,由 name、id、address、port 以及一个可选的 payload 属性构成。其存储在 ZooKeeper 中的方式如下图展示的这样。
下面通过一个简单示例来说明一下 curator-x-discovery 包的使用,该示例中的 ServerInfo 记录了一个服务的 host、port 以及描述信息。
public class CuratorDiscoveryApi {
private ServiceDiscovery<ServerInfo> serviceDiscovery;
private ServiceCache<ServerInfo> serviceCache;
private CuratorFramework client;
private String root;
// 这里的JsonInstanceSerializer是将ServerInfo序列化成Json
private InstanceSerializer serializer =
new JsonInstanceSerializer<>(ServerInfo.class);
void ZookeeperCoordinator(Config config) throws Exception {
this.root = config.getPath();
// 创建Curator客户端
client = CuratorFrameworkFactory.newClient(
config.getHostPort(), new ExponentialBackoffRetry(1000,3));
client.start(); // 启动Curator客户端
client.blockUntilConnected(); // 阻塞当前线程,等待连接成功
// 创建ServiceDiscovery
serviceDiscovery = ServiceDiscoveryBuilder
.builder(ServerInfo.class)
.client(client) // 依赖Curator客户端
.basePath(root) // 管理的Zk路径
.watchInstances(true) // 当ServiceInstance加载
.serializer(serializer)
.build();
serviceDiscovery.start(); // 启动ServiceDiscovery
// 创建ServiceCache,监Zookeeper相应节点的变化,也方便后续的读取
serviceCache = serviceDiscovery.serviceCacheBuilder()
.name(root)
.build();
serviceCache.start(); // 启动ServiceCache
}
public void registerRemote(ServerInfo serverInfo)throws Exception{
// 将ServerInfo对象转换成ServiceInstance对象
ServiceInstance<ServerInfo> thisInstance =
ServiceInstance.<ServerInfo>builder()
.name(root)
.id(UUID.randomUUID().toString()) // 随机生成的UUID
.address(serverInfo.getHost()) // host
.port(serverInfo.getPort()) // port
.payload(serverInfo) // payload
.build();
// 将ServiceInstance写入到Zookeeper中
serviceDiscovery.registerService(thisInstance);
}
public List<ServerInfo> queryRemoteNodes() {
List<ServerInfo> ServerInfoDetails = new ArrayList<>();
// 查询 ServiceCache 获取全部的 ServiceInstance 对象
List<ServiceInstance<ServerInfo>> serviceInstances =
serviceCache.getInstances();
serviceInstances.forEach(serviceInstance -> {
// 从每个ServiceInstance对象的playload字段中反序列化得
// 到ServerInfo实例
ServerInfo instance = serviceInstance.getPayload();
ServerInfoDetails.add(instance);
});
return ServerInfoDetails;
}
}
Recipes 是 Curator 对常见分布式场景的解决方案,这里我们只是简单介绍一下,具体的使用和原理,就先不做深入分析了。
总结
本课时我们重点介绍了 Apache Curator 相关的内容:
首先将 Apache Curator 与其他 ZooKeeper 客户端进行了对比,Apache Curator 的易用性是选择 Apache Curator 的重要原因。
接下来,我们通过示例介绍了 Apache Curator 的基本使用方式以及实际使用过程中的一些注意点。
然后,介绍了 curator-x-discovery 扩展库的基本概念和使用。
最后,简单介绍了 curator-recipes 提供的强大功能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。