【第二弹】详解分布式中间件Dubbo
四、Dubbo的注册中心Zookeeper
官方推荐Dubbo的注册中心使用Zookeeper,本部分将对Zookeeper的相关知识进行讲解,包括Zookeeper基本概念、基本使用、工作原理、集群相关和主要应用场景。
1. Zookeeper基本概念
1.1 会话 Session
Session指客户端会话,一个客户端连接是指客户端和服务端之间的一个TCP长连接,Zookeeper对外的服务端口默认为2181,客户端启动的时候,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来自服务器的Watch事件通知。
1.2 数据节点 Znode
Zookeeper提供一个多层级的节点命名空间即节点称为Znode,在Zookeeper中,节点可以分为两类,第一类指构成集群的机器,称为机器节点;第二类指数据模型中的数据单元即数据节点。Zookeeper中数据节点分为以下四种:
(1)PERSISTENT-持久节点
除非手动删除,否则节点一直存在于 Zookeeper 上。
(2)EPHEMERAL-临时节点
临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与Zookeeper连接断开不一定会话失效),那么这个客户端创建的所有临时节点都会被移除。
(3)PERSISTENT_SEQUENTIAL-持久顺序节点
基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
(4)EPHEMERAL_SEQUENTIAL-临时顺序节点
基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
1.3 版本 Version
由上可知,Zookeeper的每个Znode上都会存储数据,对于每个Znode,Zookeeper都会为其维护一个叫作Stat的数据结构,Stat记录了这个Znode的三个数据版本,分别是version(当前Znode的版本)、cversion(当前Znode子节点的版本)、aversion(当前Znode的ACL版本)。
1.4 事件监听器 Watcher
事件监听器(Watcher)是Zookeeper中一个很重要的特性,Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端,该机制是Zookeeper实现分布式协调服务的重要特性。
1.5 权限控制策略 ACL
Zookeeper采用ACL(Access Control Lists)策略来进行权限控制,其定义了如下五种权限:
(1)CREATE:创建子节点的权限;
(2)READ:获取节点数据和子节点列表的权限;
(3)WRITE:更新节点数据的权限;
(4)DELETE:删除子节点的权限;
(5)ADMIN:设置节点ACL的权限。
其中需要注意的是,CREATE和DELETE这两种权限都是针对子节点的权限控制。
2. Zookeeper基本使用
2.1 Zookeeper的部署模式
部署模式:单机模式、伪集群模式、集群模式。
2.2 Zookeeper基本使用方式
在客户端如何对Zookeeper的节点数据进行操作,Zookeeper为我们提供了以下四种方式:
(1)命令行操作
(2)API操作
(3)开源客户端ZkClient操作
(4)开源客户端Curator操作
3. Zookeeper工作原理
3.1 Zookeeper Watcher 机制(数据变更通知)
Zookeeper允许客户端向服务端的某个Znode注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher监听,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据Watcher通知状态和事件类型做出业务上的改变。
Watcher特性:
(1)一次性
无论是服务端还是客户端,一旦一个Watcher被 触 发 ,Zookeeper都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。
(2)客户端串行执行
客户端Watcher回调的过程是一个串行同步的过程。
(3)轻量
Watcher通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容,客户端向服务端注册Watcher的时候,并不会把客户端真实的Watcher对象实体传递到服务端,仅仅是在客户端请求中使用Boolean类型属性进行了标记。
(4)Watcher event异步发送Watcher的通知事件从server发送到client是异步的,这就存在一个问题,不同的客户端和服务器之间通过socket进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于Zookeeper本身提供了ordering guarantee,即客户端监听事件后,才会感知它所监视的Znode发生了变化。所以我们使用Zookeeper不能期望能够监控到节点每次的变化。Zookeeper只能保证最终的一致性,而无法保证强一致性。
(5)注册Watcher getData、exists、getChildren
(6)触发Watcher create、delete、setData
(7)当一个客户端连接到一个新的服务器上时,Watcher将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到Watcher的。而当client重新连接时,如果需要的话,所有先前注册过的Watcher,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,Watcher可能会丢失:对于一个未创建的Znode的exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个Watcher事件可能会被丢失。
工作机制:
(1)客户端注册Watcher
(2)服务端处理Watcher
(3)客户端回调Watcher
客户端注册Watcher实现说明:
(1)调用getData()/getChildren()/exist()三个API,传入Watcher对象
(2)标记请求request,封装Watcher到WatchRegistration
(3)封装成Packet对象,服务端发送request
(4)收到服务端响应后,将Watcher注册到ZKWatcherManager中进行管理
(5)请求返回,完成注册。
服务端处理Watcher实现:
(1)服务端接收Watcher并存储
接收到客户端请求,处理请求判断是否需要注册Watcher,需要的话将数据节点的节点路径和ServerCnxn(ServerCnxn 代表一个客户端和服务端的连接,实现了Watcher的process接口,此时可以看成一个Watcher对象)存储在WatcherManager的WatchTable和watch2Paths 中去。
(2)Watcher触发
以服务端接收到setData() 事务请求触发NodeDataChanged事件为例:
先封装WatchedEvent,将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点路径封装成一个WatchedEvent对象;后查询Watcher,即从WatchTable中根据节点路径查找 Watcher,若没找到说明没有客户端在该数据节点上注册过Watcher,若找到则提取并从WatchTable 和Watch2Paths中删除对应Watcher(从这里可以看出Watcher在服务端是一次性的,触发一次就失效了);最后调用process方法来触发Watcher,即通过ServerCnxn对应的TCP连接发送Watcher事件通知。
客户端回调 Watcher:
客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调 Watcher。客户端的 Watcher 机制同样是一次性的,一旦被触发后,该 Watcher 就失效了。
3.2 ZAB协议(保证主从节点的状态同步)
ZAB协议是为分布式协调服务(保证各个服务之间的数据状态同步)Zookeeper专门设计的一种支持崩溃恢复的原子广播协议。
ZAB 协议包括两种基本的模式:崩溃恢复和消息广播。
当整个Zookeeper集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与Leader服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式,首先选举产生新的Leader 服务器,然后集群中Follower服务器开始与新的Leader服务器进行数据同步,当集群中超过半数机器与该Leader服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。
恢复模式:
当服务启动或者在领导者崩溃后,Zookeeper集群服务就进入了恢复模式,当领导者被选举出来,且大多数服务Server完成了和Leader的状态同步以后,恢复模式就结束了。状态同步保证了Leader和 Server具有相同的系统数据状态。
广播模式:
一旦Leader已经和多数的Follower进行了状态同步后,它就可以开始广播消息了,即进入广播模式。这时候当一个Server加入到ZooKeeper集群服务中,它会在恢复模式下启动,发现Leader,并和 Leader进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper服务一直维持在Broadcast状态,直到Leader崩溃了或者Leader失去了大部分的Followers支持。
4. Zookeeper集群相关
4.1 Zookeeper集群规则
如果保证Zookeeper集群可用,必须满足过半原则;
集群规则为 2N+1 台,N>0,即Zookeeper集群数量为奇数,最少为3台。
官方推荐集群数必须为奇数的原因:
如果Zookeeper集群总数为5的话,能够保证Zookeeper可用性,最多只能宕机几台?
因为可用必须满足5-2>5/2,所以最多只能宕机2台;
如果Zookeeper集群总数为6的话,能够保证Zookeeper可用性,最多只能宕机几台?
因为可用必须满足6-2>6/2,所以最多只能宕机2台;
也就是说,如果有2个Zookeeper节点,那么只要有1个Zookeeper节点死了,那么Zookeeper服务就不能用了,因为1没有过半,所以2个Zookeeper的死亡容忍度为0;同理,要是有3个Zookeeper,一个死了,还剩下2个正常的,过半了,所以3个Zookeeper的容忍度为1;同理也可以多列举几个:2->0; 3->1; 4->1; 5->2; 6->2 就会发现一个规律,2n和2n-1的容忍度是一样的,都是n-1,所以为了更加高效,不必增加那一个不必要的Zookeeper节点,所以从资源节省的角度来考虑,Zookeeper集群的节点最好要部署成奇数个。
4.2 集群角色
通常在分布式系统中,构成集群的每一台机器(服务)都有自己的角色,Zookeeper中服务角色分为Leader、Follower、Observer,主要功能如下:
Leader:
(1)事务请求的唯一调度和处理者,保证集群事务处理的顺序性
(2)集群内部各服务的调度者
Follower:
(1)处理客户端的非事务请求,转发事务请求给Leader服务器
(2)参与事务请求Proposal的投票
(3)参与Leader选举投票
Observer:
(1)3.0 版本以后引入的一个服务器角色,在不影响集群事务处理能力的基础上提升集群的非事务处理能力
(2)处理客户端的非事务请求,转发事务请求给Leader服务器
(3)不参与任何形式的投票
4.3 集群服务的工作状态
集群中服务器具有四种状态,分别是 LOOKING、FOLLOWING、LEADING、OBSERVING。
(1)LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态。
(2)FOLLOWING:跟随者状态。表明当前服务器角色是Follower。
(3)LEADING:领导者状态。表明当前服务器角色是Leader。
(4)OBSERVING:观察者状态。表明当前服务器角色是Observer。
4.4 Zookeeper集群的脑裂问题
(1)什么是脑裂
脑裂(split-brain)就是“大脑分裂”,也就是本来一个“大脑”被拆分了两个或多个“大脑”,对于人来说,如果一个人有多个大脑,并且相互独立的话,那么会导致人体“手舞足蹈”,“不听使唤”。
在"双机热备"高可用(HA)系统中,当联系两个节点的"心跳线"断开时(即两个节点断开联系时),本来为一个整体、动作协调的HA系统,就分裂成为两个独立的节点(即两个独立的个体)。由于相互失去了联系,都以为是对方出了故障,两个节点上的HA软件像"裂脑人"一样,“本能"地争抢"共享资源”、争起"应用服务"。
对于Zookeeper集群来说,在Zookeeper集群中有Leader节点,Zookeeper集群的脑裂就是存在多个Leader节点。
(2)Zookeeper集群的脑裂问题
对于一个集群,想要提高这个集群的可用性,通常会采用多机房部署,比如现在有一个由6台zkServer所组成的一个集群,部署在了两个机房,每个机房有3台zkServer。正常情况下,此集群只会有一个Leader,那么如果机房之间的网络断了之后,两个机房内的zkServer还是可以相互通信的,如果不考虑过半机制,那么就会出现每个机房内部都将选出一个Leader。这就相当于原本一个集群,被分成了两个集群,出现了两个“大脑”,这就是脑裂。对于这种情况,原本应该是统一的一个集群对外提供服务的,现在变成了两个集群同时对外提供服务,如果过了一会,断了的网络突然联通了,那么此时就会出现问题了,两个集群刚刚都对外提供服务了,数据该怎么合并,数据冲突怎么解决等等问题。
(3)Zookeeper集群脑裂问题的解决
Zookeeper集群的脑裂问题在Zookeeper中已经通过过半选举机制解决了。所谓过半选举机制,即在领导者选举的过程中,如果某台zkServer获得了超过半数的选票,则此zkServer就可以成为Leader了。
接下来,先看下过半选举机制的源码:
public class QuorumMaj implements QuorumVerifier {
private static final Logger LOG = LoggerFactory.getLogger(QuorumMaj.class);
int half;
// n表示集群中zkServer的个数(准确的说是参与者的个数,参与者不包括观察者节点)
public QuorumMaj(int n){
this.half = n/2;
}
// 验证是否符合过半机制
public boolean containsQuorum(Set<Long> set){
// half是在构造方法里赋值的
// set.size()表示某台zkServer获得的票数
return (set.size() > half);
}
}
可以看出代码中过半选举出Leader的条件是,获得投票数必须大于服务个数的一半,注意不是大于等于!
解决过程分析:
情况1:如果现在集群中有5台zkServer,机房1有3台,机房2有2台,如下:
那么half=5/2=2,此时过半机制的条件是set.size() > 2,也就是说,领导者选举的过程中至少要有三台zkServer投了同一个zkServer,才会符合过半机制,才能选出来一个Leader。此时机房间的网络断开了,对于机房1来说是没有影响的,Leader依然还是Leader,对于机房2来说是选不出来Leader的,此时整个集群中只有一个Leader。
情况2:如果现在集群中有6台zkServer,机房1有3台,机房2有3台,如下:
当机房间的网络断掉之后,机房1内的三台服务器会进行领导者选举,但是此时过半机制的条件是set.size() > 3,也就是说至少要4台zkServer才能选出来一个Leader,所以对于机房1来说它不能选出一个Leader,同样机房2也不能选出一个Leader,这种情况下整个集群当机房间的网络断掉后,整个集群将没有Leader。
而如果过半机制的条件是set.size() >= 3,那么机房1和机房2都会选出一个Leader,这样就出现了脑裂。所以我们就知道了,为什么过半机制中是大于,而不是大于等于。就是为了防止脑裂。
综上所述,可以得出结论,有了过半机制,对于一个Zookeeper集群,要么没有Leader,要没只有1个Leader,这样就避免了脑裂问题。
5. Zookeeper主要应用场景
5.1 数据发布/订阅(配置中心)
原理解说:
数据发布/订阅,即配置中心管理,就是发布者发布数据供订阅者进行数据订阅,实现动态获取配置数据。常见的设计模式有Push模式和Pull模式。
Zookeeper作为配置中心适合以下数据(配置信息)特性:
(1)数据量通常比较小
(2)数据内容在运行时会发生动态更新
(3)集群中各机器共享,配置一致
比如机器列表信息、运行时开关配置、数据库配置信息等
案例:基于Zookeeper实现动态获取数据源配置信息,即切换数据源时,不需要重启应用服务即可获得最新的数据源信息。
实现图解:
5.2 命名服务
5.3 分布式锁
在分布式环境中,经常需要对某些操作进行加锁,这时就需要分布式锁来完成。
实现分布式锁的方案:
(1)基于数据库实现分布式锁
(2)基于Redis实现分布式锁
(3)基于Zookeeper实现分布式锁
(4)基于Redission框架实现分布式锁
基于Zookeeper实现分布式锁实现思路:
这里是基于模板模式来实现分布式锁,主要步骤如下:
(1)定义模板,即锁的实现骨架,包括:定义锁、等待锁、释放锁;
(2)实现模块,即Zookeeper定义锁实现模板,包括创建临时节点来定义锁,通过事件监听来等待锁,关闭客户端连接(Session会话)来释放锁;
(3)测试:通过多个线程来创建订单序号模拟在分布式环境下独占锁使用情况。
5.4 集群管理(注册中心)
Zookeeper在集群管理方面作为注册中心的应用非常广泛,即Zookeeper利用其数据节点模型和Watcher机制提供了服务的注册和发现功能,这里主要通过Zookeeper自身实现服务的注册为例来熟悉Zookeeper作为注册中心的工作逻辑。特别Zookeeper作为Dubbo框架的注册中心将在后面学习到。(具体可以阅读下面的Dubbo中的注册中心)
基于Zookeeper实现简单的服务注册中心思路:
主要步骤如下:
(1)启动服务端时,将服务端注册到Zookeeper中,以节点形式进行数据存储,节点为/zookeeper/server;
(2)客户端启动时,从Zookeeper中获取节点信息,并进行连接;
(3)下线一个服务端,利用Zookeeper的监听机制,客户端与它断开连接;
(4)某服务端上线后,利用Zookeeper的监听机制,客户端感知与它重新建立连接;
(5)测试
先分别启动两个服务端,然后启动客户端,查看连接情况;再停掉一个服务端,查看连接情况;最后再启动停掉的服务端,查看连接情况;
入骨相思知不知
玲珑骰子安红豆
入我相思门,知我相思苦,长相思兮长相忆,短相思兮无穷极。