一、为什么出现Zookeeper?Zookeeper是什么?
- 背景:传统的集中式已经没有办法满足大型互联网系统的快速发展带来的性能需求,所以分布式的处理方式越来越受到业界肯定,随之而来的就是部署在多台计算机上的相关服务如何进行协调的问题,因为分布式的系统之间仅仅通过消息传递进行通信和协调,而网络的加入给系统带来了诸多不稳定的因素 ,如通信异常,网络三态,网络分区,服务节点死亡等,这时候就急需一个框架或中间件来解决分布式节点之间的协调问题;
- Zookeeper出现之前的尝试
2PC,3PC,Paxos:因为分布式的节点需要引入一个协调者(Coordinator)来统一调度所有分布式节点(参与者)的执行逻辑,如一个最典型的分布式场景就是跨银行转账,我们既需要调用转账银行的服务从自己的银行账户扣钱,也需要调用所转账到的银行的存钱服务,而这两个服务具有明显的先后顺序,并且应该支持回滚来保证安全性(分布式事务),而服务之间并不知道彼此的状态,所以需要抽象出一个协调者来调度服务,并保证分布式系统的数据一致性(限于篇幅,没有办法详细讲这些历史算法,有兴趣的可以看 《从Paxos到Zookeeper》这本书 - Zookeeper没有直接采用Paxos算法,而是采用ZAB(Zookeeper Atomic Broadcast)这个一致性协议,之后会详细介绍;
- Zookeeper是什么?Zookeeper是Google Chubby的开源实现,其是一个典型的分布式数据一致性的解决方案。
二、Zookeeper是如何协调分布式节点的?
- 因为分布式节点间的通信本质上是服务器节点之间的跨进程通信,所以我觉得首先要想明白操作系统中定义的进程间通信的方式: (1)通过管道通信(还有很多类似的,如队列,共享内存,信号量,其实本质上都是通过内存中每个进程可以共享的资源来实现同一台计算机上两个进程间的通信);(2) 通过Socket进行通信(RPC,可以跨计算机实现两个进程的通信);
- 而Zookerper是通过一个共享的、树型结构的名字空间,从而实现对分布式程序中多个进程的协调;
2.1 这个名字空间是什么样子?
- 这个名字空间是什么?是指Zookeeper服务器内存中的一个数据模型,其由一系列数据节点组成(也称为ZNode)
而每个ZNode中存储着一个stat对象,childern引用,data域;
- data域存储着这个ZNode存储的数据值;
- childern引用,即子节点;
- stat对象:用于存储ZNode当前的状态;
[zk: localhost:2181(CONNECTED) 0] get /app1/p_1
127.0.0.1 // 节点数据data域
<!--下面这些参数即为stat对象中存储的ZNode的相关信息-->
cZxid = 0xdd59 //Created ZXID,表示该ZNode被创建时的事务ID
ctime = Thu Apr 18 15:17:11 CST 2019 //Created Time,表示该ZNode被创建的时间
mZxid = 0xdd59 //Modified ZXID,表示该ZNode最后一次被更新时的事务ID
mtime = Thu Apr 18 15:17:11 CST 2019 //Modified Time,表示该节点最后一次被更新的时间
pZxid = 0xdd62 //表示该节点的子节点列表最后一次被修改时的事务ID。注意,只有子节点列表变更了才会变更pZxid,子节点内容变更不会影响pZxid。
cversion = 4 //子节点的版本号
dataVersion = 0 //数据节点版本号
aclVersion = 0 //ACL版本号
ephemeralOwner = 0x0 //创建该节点的会话的sessionID。如果该节点是持久节点,那么这个属性值为0。
dataLength = 9 // data域内容长度
numChildren = 4 // 子节点个数
可能暂时不理解这些参数的含义,但只要先了解到每个ZNode都会对应一个stat对象存储着该数据节点的所有状态信息即可。
- ZNode的类型分为持久节点,临时节点,顺序节点,组合使用后 有四种组合的节点类型:持久节点(PERSISTENT),持久顺序节点(PERSISTENT_SEQUENTIAL),临时节点(EPHEMERAL),临时顺序节点(EPHEMERAL_SEQUENTIAL)
持久节点是指被创建后,就会一直在Zookeeper服务器上,知道有删除操作来主动清除这个节点; 持久顺序节点与持久节点大致一样,只是每个父节点会通过给节点名后加上一个数字后缀来为它的第一级子节点维护一个创建顺序; 至于临时节点,其生命周期是和客户端的会话绑定在一起,所以会在stat对象中的ephemeralOwner参数记录下其对应的SessionID ,所以当客户端会话失效时,该临时节点就会被自动清理掉,并且临时节点只能用作叶子节点,即其不可以拥有子ZNode;
- Zookeeper是如何通过ZNode来进行分布式节点之间的协调的呢?以Kafka为例:
了解kafka推荐朱忠华的
深入理解Kafka,这里就跳过基础概念了。 我们都知道Kafka是通过Zookeeper去维护Broker集群的,为什么kafka要通过Zookeeper去维护,是因为Kafka做为一个分布式的消息中间件,其具有多个Broker节点,而这些节点可能并不在一台服务器上,所以kafka需要Zookeeper负责每个Broker的注册以及通过Zookeeper了解到每个Broker的数据与负载状态来进行负载均衡等操作;
(1)Broker的注册,当一个新的Broker启动时,我们需要将其注册到Zookeeper中,并在/broker/ids 路径上的父节点下新建一个子节点用来对应相应的broker,路径为/broker/ids/2,2即为Broker的id,然后在新创建的ZNode的data域填上该Broker节点对应的IP地址和端口,并且创建的为临时节点,这样当Broker不可以和Zookeeper保持连接时,就会自动删除该节点 ,这样Kafka就可以通过Zookeeper维护的/broker/ids路径下的子节点状态得到当前Broker集群上已注册的Broker;
(2)获取Broker存储的分区情况:我们知道Kafka中的Topic是一个抽象概念,是分区数据的逻辑组织形式,而如何知道这些Topic所对应的分区以及分区的副本分散在哪些Broker中呢?(ps:kafka的分区副本机制使得副本会存在与Leader副本不一样的Broker中),所以可以知道Kafka中每个主题的分区可能分布在不同的Broker上,所以kafka每创建一个主题,就会在Zookeeper中的/brokers/topics路径下新建一个对应Topic名称的子节点,而每个具有该主题分区的Broker就会在该子节点下注册自己的BrokerID和拥有的分区数,如/brokers/topics/(主题名)login/(BrokerID)3,然后再3这个ZNode的data域里面写上其拥有的分区数,如2;而kafka就可以根据这个信息去定位到对应主题的信息。
(3)获取Broker状态并进行Broker集群的负载均衡
- 生产者负载均衡:这种动态的负载均衡需要生产者可以感知到Zookeeper上
Broker节点的增加与减少,Topic的增加与减少,Broker和Topic之间联系的变化这些信息才能够进行正确的负载均衡,而这些信息主要是通过Zookeeper的Watcher机制来让生产者动态获得的。
2.2 Watcher机制(通过上文Kafka中的使用抛出的watcher的概念)
2.2.1 首先来看一个Watcher实例(实例来自 《从Paxos到Zookeeper》 ):
//可以看到我们新建了一个实现了Watcher接口的类
public class ZooKeeper_Constructor_Usage_Simple implements Watcher {
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
public static void main(String[] args) throws Exception{
//新建一个Zookeeper连接,分别用到要连接的Zookeeper服务器的信息以及建立会话的超时时间以及一个Watcher实例(就是这个类)
ZooKeeper zookeeper = new ZooKeeper("127.0.0.1:2181",
5000, //
new ZooKeeper_Constructor_Usage_Simple());
System.out.println(zookeeper.getState());
try {
connectedSemaphore.await();
} catch (InterruptedException e) {}
System.out.println("ZooKeeper session established.");
}
public void process(WatchedEvent event) {
System.out.println("Receive watched event:" + event);
if (KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
}
}
这段代码我们首先看main()方法,首先我们新建一个Zookeeper对象实例尝试和Zookeeper服务器建立一个连接,并且该Watcher监听Zookeeper服务器返回的WatchedEvent事件,并通过process()方法进行处理,而当服务端成功建立与客户端建立连接时,会发送一个SyncConnected事件给客户端,客户端便可以通过对应Watcher的process()方法进行适当的事件响应;而Kafka中的Watcher也是如此,当Zookeeper相应节点上的值变化后,就会产生事件并交给相应的Watcher进行处理。
//path为Zookeeper上的节点路径,watcher是要注册的Watcher
public byte[] getData(String path, Watcher watcher, Stat stat)
我们可以通过这个API实现对某个路径下的节点进行动态监测,如在Kafka中我们需要监视Broker集群的状态,那我们可以注册Watcher到/broker/ids 这个路径上,这样当其子节点个数减少(或增加)时,Zookeeper服务端会返回一个KeepState:SyncConnected, EventType:NodeDeleted 事件,而我们就可以在Watcher的process 方法中去处理;
2.2.2 如果同时有多个Watcher,客户端如何知道服务器返回的事件应该由哪一个Watcher进行处理呢?
那首先来分析一下Watcher的工作机制,即为客户端注册Watcher,服务端处理Watcher(主要是传递事件给客户端),客户端回调Watcher(主要是调用process()方法进行处理事件);
- 首先在客户端注册Watcher时,会对客户端该次对服务器端的请求request进行标记,将其设置为“使用Watcher监听”,并且在客户端本地生成一个WatchRegistration对象用于存储这次请求的节点路径与Watcher的映射关系,如果服务器端响应成功则会提取其中的信息至客户端本地的ZKWatcherManger中用于客户端回调的步骤 而此时request请求至服务端就会生成一个
Map<String(节点路径),Set<Watcher>>这样的Map,如果有则进行put操作; - 服务端是如何生成事件的呢?比如说
NodeDataChanged事件,什么操作会改变ZNode的Data域呢?服务端的setData()便可以,所以当客户端调用服务端的setData()方法并传入要设置值的节点的path时,会根据服务端的Map去找到注册到这个path上的所有监听器,并生成一个NodeDataChanged事件,然后服务端负责把这个事件通过网络传递给客户端; - 客户端接受到相应的事件后便会查看ZKWatcherManager中存储的所有Watcher,将其放入到一个等待队列中去,并通过EventThread的线程通过死循环串行执行所有在等待队列中watcher的process()方法。
三、总结:
- Zookeeper是通过维护一个有多个不同类型的ZNode的树型结构来维护分布式环境中多个需要协调的进程;
- Zookeeper是通过
Leader-follower-observer机制来维护一个Zookeeper集群,并通过ZAB协议来保证Zookeeper集群的可以忍受的数据一致性,可用性。