zookeeper-笔记-大纲

一、什么是zookeeper

zookeeper是一个分布式协同服务

什么是分布式协调服务:主要解决分布式环境当中多个进程之间的同步控制,让他们有序的去访问某种临界资源,防止造成脏数据的后果。
什么是多个进程,不是多个线程,进程就是涉及到多个应用程序,比如一个docker就是一个进程。一个服务多个实例,就是多个进程。
例如:三个实例,去抢购库存服务,当三个实例分别去抢购5个库存,一共需要15件,那么库存不够,这个需要保证同步进行,不然就会超卖。

那么需要保障同步进行,就是协调服务,分布式协调服务的本质就是分布式锁。让他们有序的去访问某种临界资源。像上面的库存5就是临界资源,让实例1先取访问,完了之后实例2再去访问。
单体应用就没有这样的问题,因为可以使用同步代码块,这种就是控制线程。

哪些技术栈用了Zookeeper
1、HBase:保障集群只有一个Master
2、Kafka:集群成员管理,Controller节点选举

Zookeeper常用功能
1、配置管理
2、分布式锁
3、DNS服务
4、成员管理,比如人员分派
缺点:不适合存储大量数据

Zookeeper数据模型

层次模型,比如文件系统,层次模型和key-value模型是两种主流的数据模型。 层次模型也叫:data tree
每个节点叫Znode\n节点可以保存数据,每个节点都有一个版本。(文件系统,如果是文件夹是不能带数据的)
data tree提供了一套完整的API接口,用来增删改查各个节点。

为什么使用层次模型:
1、便以表达数据之间层次关系
2、方便给不同的应用分配独立的命名空间

Znode分类
分为持久性和临时性
1、持久性的znode:Zookeeper集群发生宕机也不会丢失
2、临时性的Znode:client在指定的时间内没有给Zookeeper集群发消息,节点就会消失

Znode也可以是顺序性的,这个单调递增整数是Znode名字的后缀。
3、持久顺序性
4、临时顺序性

平时用的比较多的也就以上四种类型。

ZK加锁的原理
就是通过创建一个临时的节点,只要不退出,那么就会一直占有这个锁,
其他节点如果也创建这个同名的临时节点就会失败,其他节点可以选择监听这个名字一样的零时节点状态,如果释放了就会返回一个状态,就表示锁持有者释放了,其他节点就可以共同去竞争这个锁。

Master-worker

一个Master,多个worker。master监听worker的状态,并为worker分配任务。

Master

1、master除了处于active状态, 还有多个bakcup master,如果active master失败,backup master很快进入active状态
2、master实时监控worker状态,能够及时收到worker成员变化的通知,如果有变化,通常进行任务的重新分配

Master-slave和Leader-Follower的区别:

1、Master-slave:主从式架构,也称之为Master-worker,若Master发生故障,则机台会暂时停止运转,并在确认新的Master后重新自行启动,一般由Master更多的是承担一个资源管理的功能,而slave承担具体的job,而且这里的job一般是可以分解的,划分到多个slave上并行执行,master节点提供write和提供read,而slaver仅提供read的能力。当master宕机的时候,一个slave可以接管成为master,也是为了提升高可用性

2、Leader-Follower:Leader和Follower一般可以承担相同的工作,但一个时刻真正服务的只有Leader,Follower只是用来提升系统的高可用性,在Leader宕机的时候,从follower中竞选出新的leader。例如Zookeeper。

3、其实Leader-Follower和Master-slave架构是一样的,只是另一种叫法而已

Zookeeper实现master-worker

1、创建一个临时Znode节点,表示master,创建成功表示在行使master的职能,进入active状态,也就是active-master。失败就否则进入backup-master状态,为什么会失败,一定是有其他实例已经创建了这个临时节点,抢占了锁,进入backup状态的实例,就可以通过watch机制监控active-master。如果active-master被删除,就会通知backup-master,backup-master需要在重行创建master去重新抢占master

2、worker的创建:通过在/workers下面创建临时节点来加入集群,注意是在/workers下面创建临时znode节点,注意:/workers是永久的znode

3、处在active状态的master会通过watch机制监控/workers下面的znode列表,来实时获取worker成员的变化

zookeeper总体架构

zookeeper客户端库负责和zookeeper集群的交互

1、zookeeper集群可以有2种模式:standalone模式和quorum模式
standalone模式就是单节点模式、quorum就是多节点模式

2、Quorum模式
Zookeeper集群有三个及以上节点,其中只有一个leader节点,其他都是follower节点。

注意:
1、leader-follower就是zookeeper的active-bakcup表现
2、leader节点处理读写请求,follower只处理读请求,follwer节点在收到写请求时,会把写请求转发给leader来处理。

3、Session
zookeeper客户端和zookeeper集群中的某一个节点,建立会话,就会创建一个session,session是有效时间的

1、服务端关闭会话:如果在timeout时间内没有收到客户端的消息,zookeeper服务端节点会关闭session
2、客户端关闭会话:客户端主动关闭session

注意:如果zookeeper客户端连接zookeeper出错,会自动和zookeeper服务端的其他节点建立连接。

二、zookeeper-java相关的类

构造方法

new Zookeeper(connectString, sessionTimeout, watcher)

说下参数:

1、connectString:使用逗号分隔的列表,每个ZooKeeper节点是一个host:port对,host是机器名或者IP地址,port是ZooKeeper节点使用的端口号。客户端连接zookeeper集群时,会任意选取
2、connectString:中的一个节点建立连接 2、sessionTimeout:session timeout 时间
3、watcher:用于接收到来自ZooKeeper集群的所有事件,zookeeper集群会有哪些事件,zookeeper客户端连接进来的事件、客户端断开的事件、zookeeper服务节点新增和端口的事件

2、常用的API

节点的增删

1、create(path, data, flags): 创建一个给定路径的znode,并在znode保存data中的数据,flags指定znode的类型,是临时还是永久
2、delete(path, version):如果给定path上的znode的版本和给定的version匹配,删除znode,只有和参数的版本号一致才会执行删除,有点类似于cas方法。
3、exists(path, watch):判断给定 path 上的 znode 是否存在,并在znode设置一个watch

节点数据的查改

4、getData(path, watch):返回给定 path 上的 znode数据,并在znode设置一个watch,设置一个watch,就是监听这个znode
5、setData(path, data, version):如果给定 path 上的 znode的版本和给定的version匹配,设置znode数据
6、getChildren(path, watch):返回给定path上的znode的孩子znode名字,并在znode设置一个watch

同步和异步

7、sync(path):把客户端session连接节点和leader节点进行同步,客户端会随机连接集群中的一个数据,这个和客户端连接的节点如果不是leader节点,那么就会存在数据不一致的问题。

注意:上面只是部分API,还有很多没有列出来,下面对API进行一个简单的分类,读取数据的API、更新数据的API、同步异步API

1、读取znode数据的API,都可以设置一个watch用来监听znode的变化
2、更新数据的API,可以通过version版本号来作为更新条件,也可以不传版本号更新。
3、所有的方法都有同步和异步两个版本,如果是异步的,会把请求放在客户端的请求队列,然后马上返回,可以通过callback来接受服务端的响应

条件更行的流程:

异常

1、所有同步API 有两个异常:
KeeperException: 表示ZooKeeper服务端出错。

注意:KeeperException异常有个子类ConnectionLossException:表示客户端和当前连接的ZooKeeper节点断开了连接,那么就会有两种情况,客户端的请求还没zookeeper服务端,网络断开了,或者是请求已经到了服务端,且已经执行了,然后节点挂了,这个时候,客户端会自动连接到其他的服务端节点上,所以在新的节点上,要判断一下,上一个请求是否已经被执行了。

2、InterruptedException:表示方法被中断了。我们可以使用 Thread.interrupt()来中断API的执行。

watch

watch的作用,当我们监听一个节点时,我们就可以不需要轮训查询结果

比如下面图

三、使用zookeeper设计一个队列

设计思路:

1、先创建一个/queue永久节点
2、在/queue节点下面创建n个节点,这n个永久节点都是queue的直接子节点,并且是有序
3、名字都是有相同前缀+数字n依次递增,数字越小,表示位置越靠前,数字越大表示位置越靠后

实践:

1、offer方法:在/queue下面创建一个顺序znode。因为znode的后缀数字是/queue里面,现有znode最大后缀数字加1,所以该znode对应的队列元素处于队尾
2、element方法:返回队列的队头元素,如果为空则抛出NoSuchElementException异常。
3、remove方法:删除队头的元素,并且返回。remove和element代码和类似,只是多了一个delete操作。

offer方法

element方法

remove方法:

四、使用zookeeper的队列实现分布式锁

1、在/lock下面的znode数字最小的表示是锁的持有者。

2、队列中的其他节点,通过watch监听【锁的持有者节点】,收到通知后,各自判断自己是队列中最小的节点,如果是那就表示抢占锁成功。

但是通过watch监听,会存在一个问题:就是羊群效应

所有的锁请求者都 watch锁持有者,当代表锁请求者的 znode 被删除以后,所有的锁请求者都会通知到,但是只有一个锁请求者能拿到锁。这就是羊群效应。为了避免羊群效应,每个锁请求者 watch它前面的锁请求者。每次锁被释放,只会有一个锁请求者会被通知到。这样做还让锁的分配具有公平性,锁定的分配遵循先到先得的原则。

五、zookeeper集群leader的选举

选举逻辑:最靠前的节点就是leader

和分布式的锁的实现很类似,都是通过队列,最先进来的,成为leader

六、Zookeeper Observer实现跨区域部署

1、在没有Observer时,zookeeper是如何处理写请求的

有几个点:

  1. follower收到写请求,会转给leader来处理
  2. leader节点通知所有follower节点需要更新数据
  3. leader收到follower节点的accept后,向其他follower节点发送commit请求

2、在有Observer时,zookeeper是如何处理写请求的

先说下什么是Observer

1、Observer和ZooKeeper 机器其他节点唯一的交互是接收来自 leader 的 inform 消息,更新自己的本地存储,不参与提交和选举的投票过程。
2、Observer 应用场景 - 读性能提升 Observer 和 ZooKeeper 机器其他节点唯一的交互是接收来自 leader 的
3、inform 消息,更新自己的本地存储,不参与提交和选举的投票过程。因此可以通过往集群里面添加 Observer 节点来提高整个集群的读性能。

所以再看上面图,省去了leader给follower节点的propose、follower给leader的accept的2个操作

3、Observer 应用场景 - 跨数据中心部署

我们需要部署一个北京和香港两地都可以使用的 ZooKeeper 服务。我们要求北京和香港的客户端的读请求的延迟都低。因此,我们需要在北京和香港都部署 ZooKeeper 节点。我们假设 leader 节点在北京。那么每个写请求要涉及leader 和每个香港 follower 节点之间的 propose 、ack 和 commit三个跨区域消息。解决的方案是把香港的节点都设置成 observer 。 上面提的 propose 、ack 和 commit 消息三个消息就变成了 inform 一个跨区域消息消息。

4、如何配置Observer

在节点的后面添加一个:observer

5、小结

  1. 和读操作相比,写操作更耗时。而且对容错的要求越高,越要有更多的节点,写操作越耗时(因为写操作必须由leader来执行,还需要收到大多数节点的响应)。
  2. 但是想要保证linearizable的写操作数据一致性和高容错,据我所知也没有其他更好的办法。
  3. ZooKeeper最佳的应用场景是写少读多的场景。

七、服务注册与发现API

服务注册与发现要提供的功能有以下几点
1、服务注册
2、服务实例的获取
3、服务变化的通知机制

Curator有一个扩展叫作curator-x-discovery ,它就是基于ZooKeeper实现了服务发现

curator-x-discovery设计

1、使用一个 basePath 作为整个服务发现的根目录。在这个根目录下是各个服务的的目录。服务目录下面是服务实例,也就“两层” 的层级结构
2、服务实例对应的znode节点可以根据需要设置成持久性、临时性和顺序性。如果服务断掉了不能用,那么就要设计成临时节点

下面介绍三个重要的接口

1、ServiceDiscovery : 服务注册,其实他包含注册和发现功能。
2、ServiceProvider : 在服务cache之上支持服务发现操作,封装了一些服务发现策略
3、ServiceCache : 服务cache,本地保存服务实例

通过ServiceDiscovery可以得到ServiceProvider和ServiceCache

ServiceInstance

用来表示服务的具体实例,包含这个实例具体的IP+端口+ID等信息,还提供一个payload成员让用户存自定义的信息。

疑问:这是表示具体某个服务的所有实例,还是某个服务的某个具体实例?答案:是后者

ServiceDiscovery

服务注册与发现的接口,可以创建多个ServiceProvider和多个ServiceCache

ServiceProvider

服务发现的接口,它封装了ProviderStraegy和InstanceProvider,后面会继续介绍该接口的实现类

ProviderStraegy

获取服务的的某个实例的策略,提供了三种策略:轮询、随机和sticky

InstanceProvider的数据来自一个服务Cache 。服务cache是ZooKeeper数据的一个本地cache ,服务cache里面的数据可能会比ZooKeeper里面的数据旧一些。

疑问:InstanceProvider的缓存什么时候更新

noteError提供了一个让服务使用者把服务使用情况反馈给 ServiceProvider 的机制。

服务发现和注册交互流程:

1、ServiceDiscovery 提供了服务注册和发现,其实是对znode的增删改查的操作,比如服务发现方法其实是znode的读取操作。同时它也是最核心的类,所有的服务发现操作都要从这个类开始。

2、服务Cache会接受来自ZooKeeper的更新通知,也就会watch事件,然后在读取服务信息(也就是读取znode信息)

八、ZooKeeper 实现服务发现代码解读

NodeCache

NodeCache是curator包下的类,该类的作用如下

  1. 通过本地内存,来缓存服务上的某个实例数据。NodeCache通过Zookeeper的watch事件,来监听服务上面的变动,比如服务的znode里面有update、create、delete,在通过watch事件来更新本地的缓存数据。
  2. 用户如果想监听NodeCache缓存的变动,可以在Node Cache上面注册一个listener来获取cache更新的通知。

场景:因为znode有watch事件,一个zookeeper客户端最多只会收到一次通知,如果多个业务方需要监听这个事件,又不想开启多个zookeeper客户端,其实可以通过NodeCache来实现,向里面添加listener

PathCache

PathCache和NodeCache一样,原理其实和NodeCache差不多,不同之处是在于PathCache缓存一个znode目录下所有子节点。

container节点

1、这是一种新引入的znode,也可以下挂子节点。当一个container节点的所有子节点被删除之后,ZooKeeper会删除掉这个container节点。
2、前面我们说到的 《服务发现的base path节点》和《服务节点》就是containe类型的节点。

疑问:

containe节点是不是也分临时节点和永久节点?

ServiceDiscoveryImpl

该类包含服务注册与发现的实现逻辑,代码太多,挑下面2个功能重点看下
1、服务注册
2、服务发现
3、类包含哪些属性

1、先看属性

private final ConcurrentMap<String, Entry<T>> services = Maps.newConcurrentMap();
//注意属性services,他的key是服务实例的Id,value是一个Entry,下面看下Entry的数据结构
private static class Entry<T>{
    private volatile ServiceInstance<T> service;
    private volatile NodeCache cache;

    private Entry(ServiceInstance<T> service){
        this.service = service;
    }
}

ServiceInstance前面说了,是一个服务的某一个实例的对象,只能表示一个具体服务实例。
所以需要注意:services里面的Map存的是当前basePath下面的所有服务的所有实例

2、服务注册

@Override
public void registerService(ServiceInstance<T> service) throws Exception {

    Entry<T> newEntry = new Entry<T>(service);
    //1、判断当前service map是否包含这个实例,注意key是实例的Id
    Entry<T> oldEntry = services.putIfAbsent(service.getId(), newEntry);
    Entry<T> useEntry = (oldEntry != null) ? oldEntry : newEntry;
    synchronized(useEntry){
        if ( useEntry == newEntry ){
        	//2、如果是一个Service map里面没有的实例,那就给这个实例创建一个NodeCache
            useEntry.cache = makeNodeCache(service);
        }
        //3、添加实例到znode里面去
        internalRegisterService(service);
        //4、看下internalRegisterService的具体实现逻辑
    }
}

3、发现服务:

//获取所有实例名称
@Override
public Collection<String> queryForNames() throws Exception {
    List<String> names = client.getChildren().forPath(basePath);
    return ImmutableList.copyOf(names);
}
//下面是获取某个服务的所有实例

ServiceProviderImpl

主要是用于服务的发现,里面最重要的方法就是getInstance() 和 getAllInstances()
前者获取某一个实例,后者是获取全部实例

属性
private final InstanceProvider<T> instanceProvider;
private final ProviderStrategy<T> providerStrategy;

@Override
public Collection<ServiceInstance<T>> getAllInstances() throws Exception {
    return instanceProvider.getInstances();
}

@Override
public ServiceInstance<T> getInstance() throws Exception {
	//通过providerStrategy来获取某一个实例
    return providerStrategy.getInstance(instanceProvider);
}

疑问:

getInstance方法用来获取某一个实例,那到底获取到是哪个实例呢?

解答:

主要是通过providerStrategy来获取,providerStrategy是一个接口,它有三个实现类,也就是三种策略,每个策略实现了从多个实例中获取一个实例的逻辑,实现逻辑都不一样的。

1、随机获取
public class RandomStrategy<T> implements ProviderStrategy<T>{
	ServiceInstance<T>  getInstance(InstanceProvider<T> instanceProvider) throws Exception{
		int thisIndex = random.nextInt(instances.size());//产生一个随机数
        return instances.get(thisIndex);
	}
}

2public class RoundRobinStrategy<T> implements ProviderStrategy<T>{
	ServiceInstance<T>  getInstance(InstanceProvider<T> instanceProvider) throws Exception{
		int thisIndex = Math.abs(index.getAndIncrement());//返回一个数的绝对值
        return instances.get(thisIndex % instances.size());//取模
	}
}

3、返回上一次返回的实例
public class StickyStrategy<T> implements ProviderStrategy<T>{
	ServiceInstance<T> getInstance(InstanceProvider<T> instanceProvider) throws Exception{
		....这个有点复杂,大致就是返回上一次返回的那一个实例
	}
}

ZooKeeper是如何存储数据的

1、一部分是在内存中存储:用的数据结构是data tree
2、事务日志文件,这部分数据是在磁盘中存储

进行 ZooKeeper API 开发,我个人建议以下的 SDK 使用优先顺序:

curator recipes -> curator framework -> ZooKeeper API

九、kafka如何使用zookeeper的

Kafka使用ZooKeeper实现了大量的协同服务。如果我们检查一个Kafka使用的ZooKeeper,会发现大量的znode:Broker Node Registry是用来保存Kafka集群的Kafka节点,是典型的"组成员"管理协同服务

首先说下kafka是如何通过zookeeper实现分布式功能的,

1、首先是启动一个zookeeper服务端,作为kafka服务端
2、启动一个客户端,作为kafka节点1:然后我们在kafka的配置上,配置zookeeper服务端的ip和端口。在启动kafka节点1,节点1会吧自身的信息注册到服务端中。
3、在启动一个客户端,作为kafka节点2:同样的在配置上配置zookeeper服务端的ip和端口,启动kafka节点2,节点2会吧自身的信息注册到服务端中。这样我们就搭建了一个kafka集群。

kafuka里面用了大量的multi API,下面开始介绍multi API

multi API

ZooKeeper 的 multi 方法提供了一次执行多个 ZooKeeper 操作的机制。多个 ZooKeeper 操作作为一个整体执行,要么全部成功,要么全部失败。有点类似于mysql的事务、

下面展示同步操作创建2个znode,要不都成功要不都失败

//创建2个znodes
Op createOp1 = Op.create(path1, data1, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
Op createOp2 = Op.create(path2, data2, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//把这个znode转成list传给multi,要吗都成功要吗都失败,这个是同步的操作
zk.multi(ImmutableList.of(createOp1, createOp2));

下面删除2个znode

// Delete two znodes
Op deleteOp1 = Op.delete(path1, -1);
Op deleteOp2 = Op.delete(path2, -1);

// Asynchronous API
zk.multi(ImmutableList.of(deleteOp1, deleteOp2), callback, null);
//异步创建znode,并且设置回调
AsyncCallback.MultiCallback callback =
    (int rc, String path, Object ctx, List<OpResult> opResults) -> {
      assertThat(rc).isEqualTo(KeeperException.Code.OK.intValue());
      System.out.printf("delete multi executed");
    };
zk = new ZooKeeper("localhost", 2181, new DefaultWatcher());

设置事务创建2个znode

//手动事务的创建2个节点
Transaction tx = zk.transaction();
tx.create(path1, data1, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
tx.create(path2, data2, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

tx.commit();提交
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

信仰_273993243

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

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

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

打赏作者

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

抵扣说明:

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

余额充值