怎样使用Zookeeper实现服务发现?
典型回答
服务发现是ZK的重要用途之一,当我们想要基于zk实现服务发现时,一般可以参考以下步骤:
1. 向Zookeeper注册服务
服务提供者需要在Zookeeper上创建一个临时节点 来注册自己的服务。节点的名称通常是服务名称和版本号等信息的组合,节点的数据可以包含服务的地址、端口、协议等信息。因为是 临时节点 ,所以当服务提供者关闭或崩溃时,该节点将自动从Zookeeper中删除。
2. 客户端订阅服务
服务消费者需要在Zookeeper上订阅自己所需的服务。它可以监听服务节点的变化,一旦节点发生变化,就会接收到通知。服务消费者可以根据需要选择自己感兴趣的服务节点。
服务消费者还可以通过Zookeeper提供的API获取当前可用的服务列表。它可以从服务节点的子节点中获取服务的地址和端口等信息,然后根据自己的业务逻辑选择一个合适的服务节点。还可以在服务节点上设置监听器,一旦节点发生变化,就会收到通知。这样可以保证服务消费者随时获取到最新的服务列表 。
扩展知识
Zookeeper和Nacos、Eureka、Consul区别?
我们知道,服务发现有很多框架,比如Nacos、Eureka、Consul等,那这些工具和Zookeeper有什么不同呢,已经有了ZK为什么还需要这些工具呢?
虽然Zookeeper也可以用于实现服务发现,但是它并不是专门为服务发现而设计的,因此在某些场景下可能存在一些不足,
首先,Zookeeper的配置和使用相对较为复杂 ,需要熟悉它的底层原理和API,对于开发人员和运维人员都有一定的学习成本。
第二,Zookeeper主要是用于分布式协调和同步 ,对于高频率的服务注册和查询请求,其性能可能会受到影响。
第三,Zookeeper虽然支持服务注册和发现,但在某些方面可能不够灵活 ,比如对于服务的多种状态、元数据等的管理可能不够完善。
而Nacos、Eureka和Consul等专门的服务注册中心和发现组件,可以在做到简单易用,并且高性能的基础上,还能提供一些特色的功能,比如健康检查、负载均衡、流量控制等等。
为什么 RocketMQ 不使用 Zookeeper 作为注册中心呢?
Kafka 我们都知道采用 Zookeeper 作为注册中心------当然也开始逐渐去 Zookeeper,RocketMQ 不使用 Zookeeper 其实主要可能从这几方面来考虑:
- 基于可用性的考虑,根据 CAP 理论,同时最多只能满足两个点,而 Zookeeper 满足的是 CP ,也就是说 Zookeeper 并不能保证服务的可用性,Zookeeper 在进行选举的时候,整个选举的时间太长,期间整个集群都处于不可用的状态,而这对于一个注册中心来说肯定是不能接受的,作为服务发现来说就应该是为可用性而设计。
- 基于性能的考虑,NameServer 本身的实现非常轻量,而且可以通过增加机器的方式水平扩展,增加集群的抗压能力,而 Zookeeper 的写是不可扩展的,Zookeeper 要解决这个问题只能通过划分领域,划分多个 Zookeeper 集群来解决,首先操作起来太复杂,其次这样还是又违反了 CAP 中的 A 的设计,导致服务之间是不连通的。
- 持久化的机制来带的问题 ,ZooKeeper 的 ZAB 协议对每一个写请求,会在每个 ZooKeeper 节点上保持写一个事务日志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的一致性和持久性,而对于一个简单的服务发现的场景来说,这其实没有太大的必要,这个实现方案太重了。而且本身存储的数据应该是高度定制化的。
- 消息发送应该弱依赖注册中心,而 RocketMQ 的设计理念也正是基于此,生产者在第一次发送消息的时候从 NameServer 获取到 Broker 地址后缓存到本地,如果 NameServer 整个集群不可用,短时间内对于生产者和消费者并不会产生太大影响。
如何用Zookeeper实现分布式锁?
基于zookeeper临时有序节点可以实现的分布式锁。
大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的临时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个临时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
来看下Zookeeper能不能解决以下问题。
●锁无法释放 ?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
●非阻塞锁 ?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。
●不可重入 ?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中 ,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
●单点问题 ?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。
锁无法释放(临时节点 客户端断开 会自动删除)、非阻塞锁(临时有序节点,绑定监听器,节点有变化就会通知客户端,客户端检查自己的节点是不是当前节点序号最小的,如果是就可以获取到锁,执行业务逻辑)、不可重入(客户端的主机信息和线程信息写入节点,下次获取可以进行对比)、单点问题(集群)
使用ZK实现的分布式锁好像完全符合了我们对一个分布式锁的所有期望。但是,其实并不是,Zookeeper实现的分布式锁其实存在一个缺点,那就是 性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上。
其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端和ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。(所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)
使用zookeeper实现分布式锁的优缺点
使用Zookeeper实现分布式锁的优点
有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。
使用Zookeeper实现分布式锁的缺点
性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。
Zookeeper是CP的还是AP的? CP
Zookeeper是一个CP的分布式系统。所以他会牺牲可用性,也就是在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果。
如果 ZooKeeper下所有节点都断开了,或者集群中出现了网络分割的故障(注:由于交换机故障导致交换机底下的子网间不能互访);那么ZooKeeper 会将它们都从自己管理范围中剔除出去,外界就不能访问到这些节点了,即便这些节点本身是"健康"的,可以正常提供服务的;所以导致到达这些节点的服务请求被丢失了
Zookeeper是保证的顺序一致性,也就是说,ZooKeeper不保证在每个时间点,两个不同的客户端将具有相同的ZooKeeper数据视图。但是他能保证我们在每个节点上读取到的一定是他最后一次更新的内容。
具体的案例就是,当Zookeeper在进行数据同步的过程中,如果半数节点同步成功,它就提交当前事务,但此时集群内还有可能有节点没有同步到数据,如果此时读请求发送到没有同步到数据的节点,那么就会读到旧的数据。
但是Zookeeper是会保证这个节点最终也会按照顺序执行成功的。
Zookeeper是如何保证创建的节点是唯一的?
典型回答
Zookeeper通过两个手段来保证节点创建的唯一性:
1、所有的写请求都会由Leader进行,即使是请求到Follower节点,也会被转发到Leader节点上执行。
2、在Leader上写入数据的时候,通过加锁(synchronized)和CAS(ConcurrentHashMap)操作,保证了并发情况下之后一个线程可以添加成功。
先是通过synchronized锁,将父节点锁住,然后再在锁里面判断是否已经存在节点,如果已存在,直接抛异常,如果不存在,则向维护了节点的map------NodeHashMap中添加当前节点。
在 Zookeeper 中,节点(也称为 ZNode)是分层结构的,可以有父节点和子节点。Zookeeper 的节点结构类似于文件系统中的目录结构。
父节点和子节点的概念
- 父节点:是一个节点的上一级节点。每个节点(除根节点)都有一个唯一的父节点。父节点可以包含多个子节点。
- 子节点:是一个节点的下一级节点。子节点可以是叶子节点(没有进一步的子节点),也可以是非叶子节点(可以有自己的子节点)。每个子节点有唯一的父节点。
Zookeeper是选举机制是怎样的?
典型回答
ZooKeeper的选举机制是其实现分布式协调一致性的核心部分,它确保在ZooKeeper集群中选择一个Leader节点来协调和处理客户端请求。
一次完整的选举大概要经历以下几个步骤:
初始化阶段 : 在一个ZooKeeper集群中,每个Follower节点都可以成为Leader。初始状态下,所有Follower节点都是处于"LOOKING"状态,即寻找Leader。每个节点都会监视集群中的其他节点,以侦听Leader选举消息。
提名和投票 : 当一个节点启动时,它会向其他节点发送投票请求,称为提名。节点收到提名后可以选择投票支持这个提名节点,也可以不投票。每个节点只能在一个选举周期内投出一票。
在提名过程中,所有的投票者都遵守一个原则,那就是遇强投强。
怎么算强?
在Zookeeper中,通过数据是否足够新来判断这个节点是不是够强。在 Zookeeper 中以事务id(zxid)来标识数据的新旧程度,节点的zxid越大代表这个节点的数据越新,也就代表这个节点能力越强。
那么在投票过程中,节点首先会认为自己是最强的,所以他会在投票时先投自己一票,然后把自己的投票信息广播出去,这里面包含了zxid和sid,zxid就是自己的事务ID,sid就是标识出自己是谁的唯一标识。
这样集群中的节点们就会不断受到别人发过来的投票结果,然后这个节点就会拿别人的zxid和自己的zxid进行比较,如果别人的zxid更大, 说明他的数据更新,那么就会重新投票,把zxid和sid都换成别人的信息再发出去。
选举过程: 选举过程分为多个轮次,每个轮次被称为一个"选举周期"。在每个选举周期中,节点根据投票数来选择新的Leader候选者。如果一个候选者获得了大多数节点(超过半数)的投票,那么它就会成为新的Leader。否则,没有候选者能够获得足够的投票,那么这个选举周期失败,所有节点会继续下一个选举周期。
Leader确认: 一旦一个候选者获得了大多数节点的投票,它就会成为新的Leader。这个Leader会向其他节点发送Leader就绪消息,告知它们自己已经成为Leader,并且开始处理客户端的请求。
集群同步: 一旦新的Leader选举完成,其他节点会与新Leader同步数据,确保所有节点在一个一致的状态下运行。这个同步过程也包括了未完成的客户端请求,以保证数据的一致性。
Zookeeper的watch机制是如何工作的?
典型回答
在Zookeeper中,watch机制是一种非常重要的特性,它能够让应用程序监听Zookeeper上节点的变化,从而及时做出响应
Zookeeper的watch机制实现中,涉及到多个概念,首先是客户端和服务端,这个好理解,Zookeeper的集群就是服务端,调用ZK服务的机器就是客户端。
还有两个模块,分别叫做WatchManager和ZkWatcherManager。
WatchManager是Zookeeper服务端内部的一个模块,用于管理所有watcher的相关操作,包括watcher的注册、注销、触发等。
而ZkWatcherManager是Zookeeper客户端中的一个模块,用于管理客户端中watcher的相关操作,包括创建watcher、注册watcher、处理watcher事件等。
了解了这几个概念之后,再来说一下ZK的watch机制是如何工作的:
1. 客户端连接到Zookeeper服务端,客户端创建一个ZkWatcherManager实例,用于管理客户端中所有的watcher。
2. 当客户端想要监控某个znode节点时,它可以调用ZkWatcherManager中的方法创建watcher并将其注册到客户端中。客户端将watcher的信息发送到Zookeeper服务端。
3. Zookeeper服务端接收到客户端发送的watcher信息后,会将该watcher信息交给WatchManager处理。WatchManager会将该watcher注册到相应的znode节点上,并将watcher相关的信息保存在内存中。
4. 当znode节点发生变化时,WatchManager会通知Zookeeper Server
5. Zookeeper Server会根据变化类型通知相应的客户端,告知它们发生了哪些变化。
6. 当客户端接收到Zookeeper Server的通知后,ZkWatcherManager会根据watcher的类型(data watcher或child watcher)来触发相应的事件处理方法,例如data watcher会触发processDataChanged()方法,child watcher会触发processChildChanged()方法等。
Zookeeper集群中的角色有哪些?有什么区别?
ZK中主要有以下角色:
**领导者(leader):**负责进行投票的发起和决议,更新系统状态。为客户端提供读和写服务。
**跟随者(follower):**用于接受客户端请求并响应客户端返回结果,在选主过程中参与投票。为客户端提供读服务。
**观察者(observer):**可以接受客户端连接,将写请求转发给leader,但observer不参加投票过程,只同步leader的状态,observer的目的是为了扩展系统,提高读取速度。
**客户端(client):**请求发起方
Zookeeper的自动恢复机制
当 ZooKeeper 集群中出现脑裂问题时,ZooKeeper 会自动检测到这种情况,并尝试进行自动恢复。那么过程是怎么样的呢?
识别集群分裂
当 ZooKeeper 集群中的某些节点与其他节点失去了联系,无法互相通信时,就会形成网络分区,导致集群分裂。这时候ZooKeeper就会发现集群分裂,会尝试解决这个问题。
选举新的主节点
在 ZooKeeper 集群中,所有的节点都有可能成为主节点,当旧的主节点无法与其他节点通信时,集群将选举一个新的主节点。在选举过程中,各个节点会向其他节点发送信息,包括选票和心跳包。如果有足够多的节点认为某个节点是主节点,该节点就会成为主节点。
数据同步
在选举新的主节点后,集群中的所有节点将尝试同步数据。ZooKeeper使用"原子广播"机制,确保数据在所有节点上是一致的。新的主节点将在其他节点上复制所有最新的数据,并在后续的操作中与其他节点同步。
恢复正常状态
一旦集群恢复到正常状态,ZooKeeper将更新集群状态,并将最新的状态同步给所有节点。此时,集群中只有一个主节点,并且所有节点都具有相同的数据,从而保证了集群的一致性。
Tomcat处理请求的过程是怎么样的?
典型回答
Tomcat是一个基于Servlet规范实现的Java Web容器,所以,在接收并处理请求的过程中,Servlet是必不可少的。
主要大致流程可以分为以下几步:
1、接收请求 2、请求解析 3、Servlet查找 4、Servlet请求处理 5、请求返回
接收请求:Tomcat通过连接器监听指定的端口和协议,接收来自客户端的HTTP请求
请求解析:接收到请求之后,Tomcat首先会解析请求信息,包括请求方法、URL、请求头参数等
Servlet查找:根据解析出来的URL,找到对应的Servlet,并把请求交给他进行处理
Servlet处理:这个过程就把请求交给Servlet进行处理,主要是执行其中的service方法进行请求处理
请求返回:在Servlet处理结束后,把请求的响应在发送给客户端
过滤器和拦截器的区别是什么?
一般这个问题,主要是因为在SpringMVC的应用中,过滤器和拦截器都是用来对请求进行预处理、过滤、拦截的,所以经常会放在一起比较。不过他们其实还有一些区别的。
他们的主要区别在于作用和生效的位置不同,
过滤器是在请求进入Servlet容器之前拦截请求并对请求进行处理,而拦截器是在请求进入Servlet容器之后,但在进入Controller之前拦截请求并对请求进行处理,也可以在响应返回客户端之前,拦截响应并对响应进行处理。
在Tomcat中,一次请求会先进入到Tomcat容器,然后经过Filter的处理,处理通过之后才会进入到Servlet容器,进入到Servlet容器之后,才会在Servlet执行的前后执行Intercepter。
过滤器在请求进入Servlet容器之前拦截请求并对请求进行处理,比如对请求进行安全验证、日志记录等,之后将请求转发给对应的Servlet进行处理。过滤器是基于Java Servlet规范实现的,可以通过配置web.xml文件进行实现。
拦截器是在请求进入Servlet容器之后,拦截请求并对请求进行处理,也可以在响应返回客户端之前,拦截响应并对响应进行处理 。拦截器可以对请求进行更加精细的控制,例如进行AOP、权限控制、事务管理等操作。拦截器是基于Spring框架实现的,可以通过定义拦截器类实现。
Tomcat的类加载机制是怎么样的?
Tomcat的类加载机制,在默认情况下,是先把当前要加载的类委托给BootstrapClassLoader尝试加载 ,为了避免JRE中的核心类被我们应用自己给覆盖(如String等),Bootstrap如果无法加载,那么就由WebAppClassLoader尝试加载,如果无法加载,那么再委托通过双亲委派的方式向上委派给Common、System等类加载进行加载,即顺序为:Bootstrap->WebApp->System->Common
上面的是默认情况,tomcat中有一个配置**delegate**,他的默认值是false,如果设置成true了,那么他就会严格遵守双亲委派,按照Bootstrap->System->Common->WebApp的顺序进行加载。