Java技术问答分布式&高可用Java技术升华

ZooKeeper原理、应用与面经总结

2018-11-16  本文已影响519人  千淘萬漉

一、原理篇

为什么需要ZooKeeper?
现在分布式集群已经成为后台系统的主流部署方式,而集群部署和分布式的环境提供了强大服务的同事也增加了问题的复杂性,比如集群节点在相互通信时,A节点向B节点发送消息。A节点如果想知道消息是否发送成功,只能由B节点告诉A节点。那么如果B节点关机或者由于其他的原因脱离集群网络,问题就出现了。A节点不断的向B发送消息,并且无法获得B的响应。B也没有办法通知A节点已经离线或者关机。集群中其他的节点完全不知道B发生了什么情况,还在不断的向B发送消息。这时,你的整个集群就发生了部分失败的故障。Zookeeper不能让部分失败的问题彻底消失,但是它提供了一些工具能够让你的分布式应用安全合理的处理部分失败的问题。
引自——【Zookeeper 入门】

1.1分布式系统理论

Zookeeper是Hadoop分布式调度服务,用来构建分布式应用系统。构建一个分布式应用是一个很复杂的事情,一个分布式系统会有以下几大特性和需要解决的问题:

分布式系统中绕不开最重要CAP原则,又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼,最多只能同时满足其中的两项。

之前在博客《分布式系统的事务探讨》中介绍过,简而言之就是:一致性是多个副本的结果一致性(广义上则可以延伸到分布式事务的转账问题),可用性则是请求在有限的时间内得到响应,分区容错性就是在网络分区故障的时候、仍可对外提供满足Consistency和Availability的服务。

在CAP理论的基础上,又进化出了BASE理论,P是一定需要的,剩下要做的就是在C和A之间进行平衡。BASE理论:即使无法做到强一致性,但分布式系统可以根据自己的业务特点,采用适当的方式来使系统达到最终的一致性

1.2 ZooKeeper理论

ZooKeeper是一种分布式协调服务,用于管理大型主机。ZooKeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集。在分布式环境中协调和管理服务是一个复杂的过程,而ZooKeeper通过其简单的架构和API解决了这个问题,例如ZK提供了数据发布订阅、负载均衡、命名服务、Master选举、集群管理、配置管理、分布式队列和分布式锁等功能。

Zookeeper的数据保持在内存中,这就意味着它可以实现高吞吐量和低延迟的数据。另外,Zookeeper自己在处理协调的时候要复制多个主机,Zookeeper服务的组成部分必须彼此都知道彼此,它们维持了一个内存状态影像,连同事务日志和快照在一个持久化的存储中。只要大多数的服务器是可用的,Zookeeper服务就是可用的。

ZK服务

1.2.1 ZK的重要概念总结

特点总结:1、集群部署,容错性高。2、内存设计,高吞吐低延迟。3、特别适合读场景,性能好。4、临时节点设计,提供监听服务。

(1).会话

在 ZooKeeper 中,一个客户端连接是指客户端和服务器之间的一个 TCP 长连接。客户端启动的时候,首先会与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了。通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知。ZK会话在整个运行期间的生命周期中,会在不同的会话状态之间切换,这些状态包括:CONNECTING、CONNECTED、RECONNECTING、RECONNECTED、CLOSE

(2).ZK数据模型

ZooKeeper包含一个树形的数据模型——Znode。一个znode中包含了存储的数据和 ACL(Access Control List)。ZooKeeper的设计适合存储少量的数据(因为保存在内存中),并不适合存储大量数据,所以znode的存储限制最大不超过1M。

ZK数据的访问被定义成原子性的。即一个客户端访问一个znode时,不会只得到一部分数据;客户端访问数据要么获得全部数据,要么读取失败,什么也得不到。相似的,写操作时,要么写入全部数据,要么写入失败,什么也写不进去。ZooKeeper能够保证写操作只有两个结果,成功和失败。绝对不会出现只写入了一部分数据的情况。(区别于数据流式读取的HDFS)

ZooKeeper通过path来定位znode,使用斜杠'/'来表示路径,这个和Unix系统定位文件一样。但是,znode的路径只能使用绝对路径,而不能像Unix系统一样使用相对路径,即Zookeeper不能识别../和./这样的路径。

(3).节点Znode
(4).Watcher

Watcher(事件监听器),是Zookeeper中的一个很重要的特性。Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是Zookeeper实现分布式协调服务的重要特性。

但是请注意watch机制官方说明:一个Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端,以便通知它们。不支持用持久Watcher的原因很简单,如果Watcher的注册是持久的,那么必然导致服务端的每次数据更新都会通知到客户端——这在数据变更非常频繁且监听客户端特别多的场景下,ZooKeeper无法保证性能。

(5).ACL权限控制

ACL机制,表示为scheme:id:permissions,第一个字段表示授权策略,第二个id表示用户,permissions表示相关权限(如只读,读写,管理等)。这个在实际种可能用到较少,了解一下即可。

1.2.3 来自ZK的保证

1.3 ZooKeeper集群和Zab协议

生产中ZK是以集群的形式进行部署的,分为三类角色,Leader是集群工作机制中的核心,事务请求的唯一调度和处理者,保证集群事务处理的顺序性,集群内部个服务器的调度者(管理follower,数据同步)。Follower是集群工作机制中的跟随者处理非事务请求,转发事务请求给Leader参与事务请求proposal投票,参与leader选举投票。还有Observer 观察者,和follower功能相同,但不参与任何形式投票。

ZK集群

集群工作流程:Zookeeper 客户端会随机连接到 Zookeeper 集群的一个节点,如果是读请求,就直接从当前节点中读取数据;如果是写请求,那么节点就会向 leader 提交事务,leader 会广播事务,只要有超过半数节点写入成功,该写请求就会被提交(类 2PC 协议)。但是考虑以下特殊情况,ZK集群该如何工作呢?

这个时候需要Zab协议出场了:

ZAB 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。

Zab协议核心是在整个zookeeper集群中只有一个节点——leader将所有客户端的写操作转化为事务( 提议proposal )。leader节点在数据写完之后,将向所有的follower节点发送数据广播请求(数据复制),等所有的follower节点的反馈,在zab协议中,只要超过半数follower节点反馈ok,leader节点会向所有follower服务器发送commit消息,将leader节点上的数据同步到follower节点之上。

Zab周期:正常情况下当客户端对zk有写的数据请求时,leader节点会把数据同步到follower节点,这个过程其实就是消息的广播模式;在新启动的时候,或者leader节点奔溃的时候会要选举新的leader,选好新的leader之后会进行一次数据同步操作,整个过程就是奔溃恢复。整个过程其实可以分成两部分。第一的消息广播模式,第二是崩溃恢复模式。

Zab周期

因此上面两个问题中,Leader挂掉以后的数据一致性的问题,答案可以这么来总结:

——【Zookeeper ZAB 协议分析】

二、ZooKeeper应用

Zookeeper客户端提供了基本的操作,比如,创建会话、创建节点、读取节点、更新数据、删除节点和检查节点是否存在等。但对于开发人员来说,Zookeeper提供的基本操纵还是有一些不足之处,比如Session超时之后没有实现重连机制、异常处理繁琐、Watcher是一次性等等。但是好在社区提供了两款十分强大的ZK客户端:ZKClientCurator

这两个客户端,熟练掌握其中的一种即可!下面以ZKClient为例,创建会话、创建节点、读取数据、更新数据、删除节点、检测节点等方面介绍ZkClient的使用。

会话创建、节点创建、获取子节点、删除节点
public class ZkClientTest {
    public static void main(String[] args) throws IOException,InterruptedException {
        //创建会话
        ZkClient zkClient = new ZkClient("127.0.0.1:2181", 5000);
        String path = "/zk-book";

        //监测子节点变化
        zkClient.subscribeChildChanges(path, new IZkChildListener() {   
            @Override
            public void handleChildChange(String parentPath, List<String> currentChild)
                    throws Exception {
                System.out.println(parentPath + " 's child changed ,currentChilds: " + currentChild);
            }
        });

        //创建节点
        zkClient.createPersistent(path);
        Thread.sleep(1000);
        System.out.println(zkClient.getChildren(path));
        Thread.sleep(1000);
        zkClient.createPersistent(path + "/c1");
        Thread.sleep(1000);
        System.out.println(zkClient.getChildren(path));
        Thread.sleep(1000);
        zkClient.createPersistent(path + "/c2","123");
        Thread.sleep(1000);
        System.out.println(zkClient.getChildren(path));

        //删除节点
        Thread.sleep(1000);
        zkClient.delete(path + "/c1");
        Thread.sleep(1000);
        System.out.println(zkClient.getChildren(path));
        zkClient.delete(path + "/c2");
        System.out.println(zkClient.getChildren(path)); 
        Thread.sleep(1000);     
    }
}   
改变节点数据、检测节点是否存在
public class ZkClientTest {

    public static void main(String[] args) throws InterruptedException   {

        //创建会话
        ZkClient zkClient = new ZkClient("127.0.0.1:2181", 5000);
        //创建节点
        String path = "/zk-book";
        zkClient.createPersistent(path,"123");
        //监测节点数据变化
        zkClient.subscribeDataChanges(path, new IZkDataListener() {

            @Override
            public void handleDataDeleted(String dataPath) throws Exception {
                System.out.println("Node " + dataPath + " deleted.");
            }

            @Override
            public void handleDataChange(String dataPath, Object data) throws Exception {
                System.out.println("Node " + dataPath + " changed, new data: " +data);
            }
        });
        //读取节点数据
        System.out.println(zkClient.readData(path));
        zkClient.writeData(path, "456");
        Thread.sleep(1000);
        System.out.println("Node exists :" + zkClient.exists(path));
        zkClient.delete(path);
        System.out.println("Node exists :" + zkClient.exists(path));
    }
}

Demo引自——【ZooKeeper学习笔记-6---ZkClient使用】,该博客中有系统总结ZK应用,可以学习。

2.1集群部署

zk集群搭建可参考该文章:https://blog.csdn.net/liu_zhaoming/article/details/79332232

注意!!!在zookeeper的选举过程中,为了保证选举过程最后能选出leader,就一定不能出现两台机器得票相同的僵局,所以一般的,要求zk集群的server数量一定要是奇数,也就是2n+1台,并且,如果集群出现问题,其中存活的机器必须大于n+1台,否则leader无法获得多数server的支持,系统就自动挂掉。所以一般是3个或者3个以上节点。因此有些面试考察的时候会问各种挂掉的情况,只要记住:过半存活就能保证工作,否则系统就挂掉了。另外ZK集群不支持动态扩容(其实就是水平扩容),Zookeeper在这方面不太好,扩容只能采用以下两种方式:

三、面经

Zookeeper是什么框架?
应用场景
Paxos算法& Zookeeper使用协议
选举算法和流程
Zookeeper有哪几种节点类型
Zookeeper对节点的watch监听通知是永久的吗?
部署方式?集群中的机器角色都有哪些?集群最少要几台机器?
集群如果有3台机器,挂掉一台集群还能工作吗?挂掉两台呢?
集群支持动态添加机器吗?


参考引用

Zookeeper 入门
Zookeeper ZAB 协议分析
ZK系列专栏,尤其推荐 Zookeeper 3、Zookeeper工作原理(详细)
可能是全网把 ZooKeeper 概念讲的最清楚的一篇文章

上一篇下一篇

猜你喜欢

热点阅读