zookeeper

1.研究下数据持久化机制
2.研究下dubbo中如何封装并使用zk的
3.脑裂问题
4.多中心部署数据同步问题

https://luyiisme.github.io/2017/04/22/spring-cloud-service-discovery-products/?utm_source=tuicool&utm_medium=referral

1.zk概述

1.1zk是什么

Zookeeper是一个分布式协调服务,可用于服务发现,分布式锁,分布式领导选举,配置管理等。

这一切的基础,都是Zookeeper提供了一个类似于Linux文件系统的树形结构,
可认为是轻量级的内存文件系统,但只适合存少量信息,
完全不适合存储大量文件或者大文件, 同时提供了对于每个节点的监控与通知机制。

Zookeeper 和Redis一样全量数据存储在内存中,以此来提高服务器吞吐、减少延迟的目的。
100%读请求压测QPS 12-13W。

// 三大特性
>> Znode数据节点
>> Watcher机制
>> ACL权限控制

1.2相关概念

#1.zk集群 (实则是主从复制模式)
Zookeeper 是一个由多个 server 组成的集群,一个 leader,多个 follower。
这个不同于我们常见的Master/Slave模式, leader 为客户端服务器提供读写服务,除了leader外其他的机器只能提供读服务。
每个 server 保存一份数据副本全数据一致,分布式读 follower,
写由 leader 实施更新请求转发,由 leader 实施更新请求顺序进行,
来自同一个 client 的更新请求按其发送顺序依次执行数据更新原子性,
一次数据更新要么成功,要么失败。
全局唯一数据视图,client 无论连接到哪个 server,数据视图都是一致的实时性,
在一定事件范围内,client 能读到最新数据。

#2.zk集群中的角色
Zookeeper集群是一个基于主从复制的高可用集群,每个服务器承担如下三种角色中的一种:
1) Leader (有投票权, 可处理读 & 写请求)
一个Zookeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各Follwer及Observer间的心跳。
是整个 Zookeeper 集群工作机制中的核心 。负责响应所有对 ZooKeeper 状态变更的请求。
所有的写操作必须要通过Leader完成再由Leader将写操作广播给其它服务器。
>> 主要工作:
事务请求的唯一调度和处理,保障集群处理事务的顺序性。
集群内各服务器的调度者。

2) Follower (有投票权, 可处理读请求) 
一个Zookeeper集群可能同时存在多个Follower,它会响应Leader的心跳。
Follower可直接处理并返回客户端的读请求,除了响应本服务器上的读请求外,
follower 还要处理leader 的提议,并在 leader 提交该提议时在本地也进行提交。
另外需要注意的是,leader 和 follower 构成ZooKeeper 集群的法定人数,
也就是说,只有他们才参与新 leader的选举、响应 leader 的提议。

3) Observer (无投票权, 可处理读请求)
服务器充当一个观察者的角色。如果 ZooKeeper 集群的读取负载很高,
或者客户端多到跨机房,可以设置一些 observer 服务器,以提高读取的吞吐量。
Observer 和 Follower 比较相似,只有一些小区别:
首先 observer 不属于法定人数,即不参加选举也不响应提议,也不参与写操作的“过半写成功”策略;
其次是 observer 不需要将事务持久化到磁盘,一旦 observer 被重启,需要从 leader 重新同步整个namespace。


#3.会话(Session)
Session 指的是 ZooKeeper  服务器与客户端会话。
在 ZooKeeper 中,一个客户端连接是指客户端和服务器之间的一个 TCP 长连接。
客户端启动的时候,首先会与服务器建立一个 TCP 连接,
从第一次连接建立开始,客户端会话的生命周期也开始了。
通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,
也能够向Zookeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知。 
Session 的 sessionTimeout 值用来设置一个客户端会话的超时时间。
当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,
只要在sessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。
在为客户端创建会话之前,服务端首先会为每个客户端都分配一个sessionID。
由于 sessionID 是 Zookeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,
因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。
##3.1会话状态
在Zookeeper客户端与服务端成功完成连接创建后,就创建了一个会话,
Zookeeper会话在整个运行期间的生命周期中,会在不同的会话状态中之间进行切换,
这些状态可以分为:
>> CONNECTING
>> CONNECTED
>> RECONNECTING
>> RECONNECTED
>> CLOSE
一旦客户端开始创建Zookeeper对象,那么客户端状态就会变成CONNECTING状态,
同时客户端开始尝试连接服务端,连接成功后,客户端状态变为CONNECTED,
通常情况下,由于断网或其他原因,客户端与服务端之间会出现断开情况,
一旦碰到这种情况,Zookeeper客户端会自动进行重连服务,同时客户端状态再次变成CONNCTING,
直到重新连上服务端后,状态又变为CONNECTED,
在通常情况下,客户端的状态总是介于CONNECTING 和CONNECTED 之间。
但是,如果出现诸如会话超时、权限检查或是客户端主动退出程序等情况,
客户端的状态就会直接变更为CLOSE状态。
##3.2会话创建
Session是Zookeeper中的会话实体,代表了一个客户端会话,其包含了如下四个属性:
1) sessionID
会话ID,唯一标识一个会话,每次客户端创建新的会话时,
Zookeeper都会为其分配一个全局唯一的sessionID。
2) TimeOut
会话超时时间,客户端在构造Zookeeper实例时,会配置sessionTimeout参数用于指定会话的超时时间,
Zookeeper客户端向服务端发送这个超时时间后,服务端会根据自己的超时时间限制最终确定会话的超时时间。
3) TickTime
下次会话超时时间点,为了便于Zookeeper对会话实行”分桶策略”管理,
同时为了高效低耗地实现会话的超时检查与清理,
Zookeeper会为每个会话标记一个下次会话超时时间点,其值大致等于当前时间加上TimeOut。
4) isClosing
标记一个会话是否已经被关闭,当服务端检测到会话已经超时失效时,
会将该会话的isClosing标记为”已关闭”,这样就能确保不再处理来自该会话的新请求了。
Zookeeper为了保证请求会话的全局唯一性,在SessionTracker初始化时,
调用initializeNextSession方法生成一个sessionID,之后在Zookeeper运行过程中,
会在该sessionID的基础上为每个会话进行分配,初始化算法如下:

public static long initializeNextSession(long id) {
  long nextSid = 0;
  // 无符号右移8位使为了避免左移24后,再右移8位出现负数而无法通过高8位确定sid值
  nextSid = (System.currentTimeMillis() << 24) >>> 8;
  nextSid = nextSid | (id << 56);
  return nextSid;
}

##3.3会话管理
Zookeeper的会话管理主要是通过SessionTracker来负责,
其采用了分桶策略(将类似的会话放在同一区块中进行管理)进行管理,
以便Zookeeper对会话进行不同区块的隔离处理以及同一区块的统一处理。

##3.4数据节点 Znode
在Zookeeper中,“节点"分为两类:
第一类同样是指构成集群的机器,我们称之为机器节点;
第二类则是指数据模型中的数据单元,我们称之为数据节点一一ZNode。
Zookeeper将所有数据存储在内存中,数据模型是一棵树(Znode Tree), 
由斜杠(/)的进行分割的路径,就是一个Znode,例如/foo/path1。
每个ZNode上都会保存自己的数据内容,同时还会保存一系列属性信息。
>>>>>>> 节点类型 <<<<<<<
在Zookeeper中,node可以分为持久节点和临时节点和顺序节点三大类。
可以通过组合生成如下四种类型节点:
1) PERSISTENT
持久节点,节点创建后便一直存在于Zookeeper服务器上,直到有删除操作来主动清除该节点。
2) PERSISTENT_SEQUENTIAL
持久顺序节点,相比持久节点,其新增了顺序特性,
每个父节点都会为它的第一级子节点维护一份顺序,用于记录每个子节点创建的先后顺序。
在创建节点时,会自动添加一个数字后缀,作为新的节点名,该数字后缀的上限是整形的最大值。
3) EPEMERAL
临时节点,临时节点的生命周期与客户端会话绑定,客户端失效,节点会被自动清理。
同时,Zookeeper规定不能基于临时节点来创建子节点,即临时节点只能作为叶子节点。
4) EPEMERAL_SEQUENTIAL
临时顺序节点,在临时节点的基础添加了顺序特性。

##3.5版本——保证分布式数据原子性操作
每个数据节点都具有三种类型的版本信息,对数据节点的任何更新操作都会引起版本号的变化。
>> version– 当前数据节点数据内容的版本号
>> cversion– 当前数据子节点的版本号
>> aversion– 当前数据节点ACL变更版本号
上述各版本号都是表示修改次数,如version为1表示对数据节点的内容变更了一次。
即使前后两次变更并没有改变数据内容,version的值仍然会改变。version可以用于写入验证,类似于CAS。

##3.6watcher事件监听器
ZooKeeper允许用户在指定节点上注册一些Watcher,当数据节点发生变化的时候,
ZooKeeper服务器会把这个变化的通知发送给感兴趣的客户端。

##3.7ACL 权限控制——保障数据的安全
ACL是Access Control Lists 的简写, ZooKeeper采用ACL策略来进行权限控制,有以下权限:
CREATE:创建子节点的权限
READ:获取节点数据和子节点列表的权限
WRITE:更新节点数据的权限
DELETE:删除子节点的权限
ADMIN:设置节点ACL的权限

##3.8Ensemble
ZooKeeper服务器组。形成ensemble所需的最小节点数为3。

##3.9层次命名空间(namespace)
下图描述了用于内存表示的ZooKeeper文件系统的树结构。ZooKeeper节点称为 znode 。
每个znode有一个名称标识,并用路径(/)序列分隔。
在图中,首先有一个由“/”分隔的znode。在根目录下,你有两个逻辑命名空间 config 和 workers 。
>> config 命名空间用于集中式配置管理,workers 命名空间用于命名。
>> 在 config 命名空间下,每个znode最多可存储1MB的数据。
这与UNIX文件系统相类似,除了父znode也可以存储数据。
这种结构的主要目的是存储同步数据并描述znode的元数据。
此结构称为 ZooKeeper数据模型。

##3.10zk的数据模型
ZooKeeper数据模型中的每个znode都维护着一个 stat 结构。
一个stat仅提供一个znode的元数据。它由版本号,操作控制列表(ACL),时间戳和数据长度组成。
1) 版本号
每个znode都有版本号,这意味着每当与znode相关联的数据发生变化时,其对应的版本号也会增加。
当多个zookeeper客户端尝试在同一znode上执行操作时,版本号的使用就很重要。
2) 操作控制列表(ACL) 
ACL基本上是访问znode的认证机制。它管理所有znode读取和写入操作。
3) 时间戳
时间戳表示创建和修改znode所经过的时间。它通常以毫秒为单位。
ZooKeeper从“事务ID"(zxid)标识znode的每个更改。
Zxid 是唯一的,并且为每个事务保留时间,以便你可以轻松地确定从一个请求到另一个请求所经过的时间。
4) 数据长度
存储在znode中的数据总量是数据长度。最多可以存储1MB的数据。
zk-写Leader.png
zk-写Follower-Observer.png
zk-Leader-Follower-Observer读操作.png
namespace.png

1.3 ZAB(ZooKeeper Atomic Broadcast)

ZAB协议规定了两种模式:崩溃恢复和消息广播。
基于该协议,Zookeeper实现了一种主从模式的系统架构来保持集群中各个副本之间的数据一致性。

1) 恢复模式
下述情况ZAB都会进入恢复模式
>> 当整个服务框架在启动过程中。
>> 当Leader服务器出现网络中断崩溃退出与重启等异常情况。
>> 当有新的服务器加入到集群中且集群处于正常状态(广播模式),新服会与leader进行数据同步,然后进入消息广播模式。
主要功能:
选举产生新的Leader服务器,同时集群中已有的过半的机器会与该Leader完成状态同步,
这些工作完成后,ZAB协议就会退出崩溃恢复模式。

2) 广播模式
集群状态稳定,有了leader且过半机器状态同步完成 or 退出崩溃恢复模式后进入消息广播模式
主要功能:
正常的消息同步,把日常产生数据从leader同步到learner的过程
ZAB协议的两种模式.png
1.崩溃恢复状态 – 即选主过程
进入崩溃恢复模式说明集群目前是存在问题的了,那么此时就需要开始一个选主的过程。
zookeeper使用的默认选主算法是FastLeaderElection,它是标准的Fast Paxos算法实现,
可解决LeaderElection选举算法收敛速度慢的问题。

// 特点:
>> Commit过的数据不丢失
commit过的数据半数以上参加选举的follwer都有,
而且成为leader的条件是要有最高事务id即数据是最新的。
>> 未commit过的数据丢弃
未commit过的数据只存在于leader,但是leader宕机无法参加首轮选举,
epoch会小一轮,最终数据会丢弃。

// 可通过electionAlg配置项设置Zookeeper用于领导选举的算法。
到3.4.10版本为止,可选项有
0 基于UDP的LeaderElection
1 基于UDP的FastLeaderElection
2 基于UDP和认证的FastLeaderElection
3 基于TCP的FastLeaderElection
在3.4.10版本中,默认值为3,也即基于TCP的FastLeaderElection。
另外三种算法已经被弃用,并且有计划在之后的版本中将它们彻底删除而不再支持。

// 服务器状态
>> LOOKING 不确定Leader状态。
该状态下的服务器认为当前集群中没有Leader,会发起Leader选举。
>> FOLLOWING 跟随者状态。
表明当前服务器角色是Follower,并且它知道Leader是谁。
>> LEADING 领导者状态。
表明当前服务器角色是Leader,它会维护与Follower间的心跳。
>> OBSERVING 观察者状态。
表明当前服务器角色是Observer,与Folower唯一的不同在于不参与选举,也不参与集群写操作时的投票。

// 选票数据结构
每个服务器在进行领导选举时,会发送如下关键信息
>> logicClock 每个服务器会维护一个自增的整数,名为logicClock,它表示这是该服务器发起的第多少轮投票
>> state 当前服务器的状态
>> self_id 当前服务器的myid
>> self_zxid 当前服务器上所保存的数据的最大zxid
>> vote_id 被推举的服务器的myid
>> vote_zxid 被推举的服务器上所保存的数据的最大zxid

// myid
每个Zookeeper服务器,都需要在数据文件夹下创建一个名为myid的文件,
该文件包含整个Zookeeper集群唯一的ID(整数)。
例如某Zookeeper集群包含三台服务器,hostname分别为zoo1、zoo2和zoo3,其myid分别为1、2和3,
则在配置文件中其ID与hostname必须一一对应,如下所示。在该配置文件中,server.后面的数据即为myid:
server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888

// zxid
类似于RDBMS中的事务ID,用于标识一次更新操作的Proposal ID。
为了保证顺序性,该zkid必须单调递增。
因此Zookeeper使用一个64位的数来表示:
高32位是Leader的epoch,从1开始,每次选出新的Leader,epoch加一。
低32位为该epoch内的序号,每次epoch变化,都将低32位的序号重置。
这样保证了zkid的全局递增性。

////////////////////////// 投票流程开始 //////////////////////////
1) 自增选举轮次
Zookeeper规定所有有效的投票都必须在同一轮次中。
每个服务器在开始新一轮投票时,会先对自己维护的logicClock进行自增操作。

2) 初始化选票
每个服务器在广播自己的选票前,会将自己的投票箱清空。该投票箱记录了所收到的选票。
例:服务器2投票给服务器3,服务器3投票给服务器1,则服务器1的投票箱为(2, 3), (3, 1), (1, 1)。
票箱中只会记录每一投票者的最后一票,如投票者更新自己的选票,
则其它服务器收到该新选票后会在自己票箱中更新该服务器的选票。

3) 发送初始化选票
每个服务器最开始都是通过广播把票投给自己。

4) 接收外部投票
服务器会尝试从其它服务器获取投票,并记入自己的投票箱内。
如果无法获取任何外部投票,则会确认自己是否与集群中其它服务器保持着有效连接。
如果是,则再次发送自己的投票;如果否,则马上与之建立连接。

5) 判断选举轮次
收到外部投票后,首先会根据投票信息中所包含的logicClock来进行不同处理:
>> 外部投票的logicClock大于自己的logicClock。
说明该服务器的选举轮次落后于其它服务器的选举轮次,
立即清空自己的投票箱并将自己的logicClock更新为收到的logicClock,
然后再对比自己之前的投票与收到的投票以确定是否需要变更自己的投票,
最终再次将自己的投票广播出去。
>> 外部投票的logicClock小于自己的logicClock。
当前服务器直接忽略该投票,继续处理下一个投票。
>> 外部投票的logickClock与自己的相等。
立即进行选票PK。

6) 选票PK
选票PK是基于(self_id, self_zxid)与(vote_id, vote_zxid)的对比:
>> 外部投票的logicClock大于自己的logicClock,
则将自己的logicClock及自己的选票的logicClock变更为收到的logicClock
>> 若logicClock一致,
则对比二者的vote_zxid,若外部投票的vote_zxid比较大,
则将自己的票中的vote_zxid与vote_myid更新为收到的票中的vote_zxid与vote_myid并广播出去,
另外将收到的票及自己更新后的票放入自己的票箱。
如果票箱内已存在(self_myid, self_zxid)相同的选票,则直接覆盖。
>> 若二者vote_zxid一致,
则比较二者的vote_myid,若外部投票的vote_myid比较大,
则将自己的票中的vote_myid更新为收到的票中的vote_myid并广播出去,
另外将收到的票及自己更新后的票放入自己的票箱

7) 统计选票
如果已经确定有过半服务器认可了自己的投票(可能是更新后的投票), 
则终止投票。否则继续接收其它服务器的投票。

8) 更新服务器状态
投票终止后,服务器开始更新自身状态。
若过半的票投给了自己,则将自己的服务器状态更新为LEADING,否则将自己的状态更新为FOLLOWING。
////////////////////////// 投票流程结束 //////////////////////////

/////////// 下图引自: ///////////
http://www.jasongj.com/zookeeper/fastleaderelection/
zk-集群启动领导选举.png
zk-follower重启领导选举.png
zk-leader重启领导选举.png
2.消息广播状态 – 即数据同步
client端发起请求,读请求由leader/follower/observer直接返回,写请求由它们转发给leader:
1) Leader 首先为这个事务分配一个全局单调递增的唯一事务ID (即 ZXID )。
2) 然后发起proposal给follower,Leader 会为每一个 Follower 都各自分配一个单独的队列,
然后将需要广播的事务 Proposal 依次放入这些队列中去,并且根据 FIFO策略进行消息发送。
3) 每一个 Follower 在接收到这个事务 Proposal 之后,都会首先将其以事务日志的形式写入到本地磁盘中去,
并且在成功写入后反馈给 Leader 服务器一个 Ack 响应。
4) 当 Leader 服务器接收到超过半数 Follower 的 Ack 响应后,
就会广播一个Commit 消息给所有的 Follower 服务器以通知其进行事务提交,
同时Leader 自身也会完成对事务的提交。

1.4 zookeeper数据结构图---->znode

zookeeper的数据模型,在结构上和标准文件系统非常相似;拥有一个层次的命名空间, zookeeper树中的每个节点都被称为一个Znode;
和文件系统的目录树一样,zookeeper树中的每个节点可以拥有子节点,但也有不同之处;如下:
// 1.Znode兼具文件和目录两种特点
1.即像文件一样维护着数据,元信息 ACL 时间戳等数据结构,
2.又像目录一样可以作为路径标示的一部分,并可以具有子Znode, 
3.用户可以对Znode具有增 删 改 查等操作(权限允许的情况下)

// 2.Znode具有原子性操作
1.读操作将获取与节点相关的所有的数据
2.写操作也将替换点节点的所有数据
3.另外,每一个节点都拥有自己的ACL(访问控制列表),这个列表规定了用户的权限,
即限定了特定用户对目标节点可以执行的操作

// 3.Znode存储数据大小有限制
1.zookeeper虽然可以关联一些数据,但是并没有被设计为常规的数据库或者大数据存储,
相反的是,它用来管理调度数据,比如分布式应用中的配置文件信息,状态信息,汇集位置等等;
2.上面的这些数据的共同特性就是他们都是很小的数据,通常以KB为大小单位,
Zookeeper的服务器和客户端都设计为严格检查并限制为每个Znode的数据大小最大1M,当然常规远小于这个值;

// 4.Znode 通过路径引用
1.如同linxu系统文件路径一样,路径必须是绝对的,因此他们必须是由斜杠字符开头
2.路径必须是唯一的
3.路径由Unicode字符串组成,并且有一些限制,字符串"zookeeper"用来保存管理信息,比如关键配置额信息

// 5.节点类型
在Zookeeper中,node可以分为持久节点和临时节点和顺序节点三大类。
可以通过组合生成如下四种类型节点:
1) PERSISTENT
持久节点,节点创建后便一直存在于Zookeeper服务器上,直到有删除操作来主动清除该节点。
2) PERSISTENT_SEQUENTIAL
持久顺序节点,相比持久节点,其新增了顺序特性,
每个父节点都会为它的第一级子节点维护一份顺序,用于记录每个子节点创建的先后顺序。
在创建节点时,会自动添加一个数字后缀,作为新的节点名,该数字后缀的上限是整形的最大值。
3) EPEMERAL
临时节点,临时节点的生命周期与客户端会话绑定,客户端失效,节点会被自动清理。
同时,Zookeeper规定不能基于临时节点来创建子节点,即临时节点只能作为叶子节点。
4) EPEMERAL_SEQUENTIAL
临时顺序节点,在临时节点的基础添加了顺序特性。

// 6.节点属性
1.dataVersion 数据版本号
每次对节点进行set操作,dataVersion的值都会增加1(即使设置的是相同的数据);
可以避免数据更新时出现的先后顺序问题;
2.cversion(children version) 子节点的版本号
当znode的子节点有变化时,cversion的值会增加1
3.aclversion
ACL的版本号
4.cZxid (create Znode zxid)
znode节点创建的事务id;在创建新的节点的时候的事务id;
5.mZxid (modified Znode zxid)
Znode被修改的事务id;即每次对znode的修改都会更新mZxid
6.ctime
节点创建时的时间戳
7.mtime
节点最新一次更新发生时的时间戳
8.ephemeralOwner
1) 如果该节点为临时节点,ephemeralOwner 值表示与该节点绑定的session id,
如果不是,ephemeralOwner 值为0;
2) 在client和server通信之前,首先需要建立连接,该连接称为session,
连接建立后,如果发生连接超时,授权失败或者显示关闭连接,连接便处于closed状态, 此时session结束;
znode的树形结构1.png

1.5 zk的特性

>> 顺序一致性
从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到Zookeeper中去。

>> 原子性
所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,
即整个集群要么都成功应用了某个事务,要么都没有应用。

>> 单一视图
无论客户端连接的是哪个 Zookeeper 服务器,其看到的服务端数据模型都是一致的。

>> 可靠性
一旦服务端成功地应用了一个事务,并完成对客户端的响应,
那么该事务所引起的服务端状态变更将会一直被保留,除非有另一个事务对其进行了变更。

>> 实时性
Zookeeper 保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。

>> 最终一致性:为客户端展示同一视图,这是zookeeper最重要的功能。 

1.6 zk的应用场景

1.分布式服务注册与订阅

2.分布式配置中心
发布与订阅模型,即所谓的配置中心,顾名思义就是发布者将数据发布到ZK节点上,
供订阅者获取数据,实现配置信息的集中式管理和动态更新。

3.命名服务
在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。
被命名的实体通常可以是集群中的机器,提供的服务地址,进程对象等等——这些我们都可以统称他们为名字(Name)。
其中较为常见的就是一些分布式服务框架中的服务地址列表。
通过调用ZK提供的创建节点的API,能够很容易创建一个全局唯一的path,这个path就可以作为一个名称。

4.分布式锁
分布式锁,这个主要得益于ZooKeeper为我们保证了数据的强一致性。
锁服务可以分为两类,一个是保持独占,另一个是控制时序。
1) 保持独占
就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。
通常的做法是把zk上的一个znode看作是一把锁,通过create znode的方式来实现。
所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。
2)控制时序
就是所有视图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。
做法和上面基本类似,只是这里 /distribute_lock 已绊预先存在,
客户端在它下面创建临时有序节点(CreateMode.EPHEMERAL_SEQUENTIAL). 
Zk的父节点(/distribute_lock)维持一份sequence, 保证子节点创建的时序性,从而也形成了每个客户端的全局时序.

5.Master选举

6.负载均衡

1.7 Zab与Paxos

1) Paxos算法的确是不关心请求之间的逻辑顺序,而只考虑数据之间的全序,
但很少有人直接使用paxos算法,都会经过一定的简化、优化。
2) Paxos算法在出现竞争的情况下,其收敛速度很慢,甚至可能出现活锁的情况,
例如当有三个及三个以上的proposer在发送prepare请求后,
很难有一个proposer收到半数以上的回复而不断地执行第一阶段的协议。
因此,为了避免竞争,加快收敛的速度,在算法中引入了一个Leader这个角色,
在正常情况下同时应该最多只能有一个参与者扮演Leader角色,而其它的参与者则扮演Acceptor的角色。
在这种优化算法中,只有leader可以提出议案,从而避免了竞争使得算法能够快速地收敛而趋于一致;
而为了保证leader的健壮性,又引入了leader选举,再考虑到同步的阶段,
渐渐的你会发现对Paxos算法的简化和优化已经和上面介绍的ZAB协议很相似了。

1.8 zk 的 Watcher 机制

1.8.1 概述

// 概述
Zookeeper采用了Watcher机制实现数据的发布/订阅功能。
该机制在被订阅对象发生变化时会异步通知客户端,
因此客户端不必在Watcher注册后轮询阻塞,从而减轻了客户端压力。

Watcher机制实际上与观察者模式类似,也可看作是一种观察者模式在分布式场景下的实现方式。

// Watcher架构
Watcher实现由三个部分组成:
>> Zookeeper服务端;
>> Zookeeper客户端;
>> 客户端的ZKWatchManager对象;
客户端首先将Watcher注册到服务端,同时将Watcher对象保存到客户端的Watch管理器中。
当ZooKeeper服务端监听的数据状态发生变化时,服务端会主动通知客户端,
接着客户端的Watch管理器会触发相关Watcher来回调相应处理逻辑,从而完成整体的数据发布/订阅流程。

// Watcher特性
>> 一次性  
Watcher是一次性的,一旦被触发就会移除,再次使用时需要重新注册。
>> 客户端顺序回调  
Watcher回调是顺序串行化执行的,只有回调后客户端才能看到最新的数据状态。
一个Watcher回调逻辑不应该太多,以免影响别的watcher执行。
>> 轻量级  
WatchEvent是最小的通信单元,结构上只包含通知状态、
事件类型和节点路径,并不会告诉数据节点变化前后的具体内容。
>> 时效性  
Watcher只有在当前session彻底失效时才会无效,
若在session有效期内快速重连成功,则watcher依然存在,仍可接收到通知。

// Watcher接口
任何实现了Watcher接口的类就是一个新的Watcher。
Watcher内部包含了两个枚举类:KeeperState、EventType。
// KeeperState (Watcher通知状态)
KeeperState是客户端与服务端连接状态发生变化时对应的通知类型。枚举属性:
>> Unknown(-1)              属性过期
>> Disconnected(0)          客户端与服务器断开连接时
>> NoSyncConnected(1)       属性过期
>> SyncConnected(3)         客户端与服务器正常连接时
>> AuthFailed(4)            身份认证失败时
>> ConnectedReadOnly(5)     
3.3.0版本后支持只读模式,一般情况下ZK集群中半数以上服务器正常,zk集群才能正常对外提供服务。
该属性的意义在于:
若客户端设置了允许只读模式,则当zk集群中只有少于半数的服务器正常时,
会返回这个状态给客户端,此时客户端只能处理读请求
>> SaslAuthenticated(6)     服务器采用SASL做校验时
>> Expired(-112)            会话session失效时

// EventType (Watcher事件类型)
EventType是数据节点(znode)发生变化时对应的通知类型。
EventType变化时KeeperState永远处于SyncConnected通知状态下;
当KeeperState发生变化时,EventType永远为None。枚举属性如下:
>> None (-1)                    无
>> NodeCreated (1)          Watcher监听的数据节点被创建时
>> NodeDeleted (2)          Watcher监听的数据节点被删除时
>> NodeDataChanged (3)      Watcher监听的数据节点内容发生变更时(无论内容数据是否变化)
>> NodeChildrenChanged (4)  Watcher监听的数据节点的子节点列表发生变更时
注:客户端接收到的相关事件通知中只包含状态及类型等信息,不包括节点变化前后的具体内容,
变化前的数据需业务自身存储,变化后的数据需调用get等方法重新获取。
Watcher架构.png
Watcher类图.png

客户端Watcher管理器:ZKWatchManager数据结构

//ZKWatchManager维护了三个map,key代表数据节点的绝对路径,value代表注册在当前节点上的watcher集合

// 代表节点上内容数据、状态信息变更相关监听
private final Map> dataWatches = new HashMap>();

// 代表节点变更相关监听
private final Map> existWatches = new HashMap>();

// 代表节点子列表变更相关监听
private final Map> childWatches = new HashMap>();

服务端Watcher管理器:WatchManager数据结构

// WatchManager维护了两个map
// 说明:WatchManager中的Watcher对象不是客户端用户定义的Watcher,
//       而是服务端中实现了Watcher接口的ServerCnxn抽象类,
//       该抽象类代表了一个客户端与服务端的连接

// key代表数据节点路径,value代表客户端连接的集合,该map作用为:
// 通过一个指定znode路径可找到其映射的所有客户端,当znode发生变更时
// 可快速通知所有注册了当前Watcher的客户端
private final HashMap> watchTable = new HashMap>();

// key代表一个客户端与服务端的连接,value代表当前客户端监听的所有数据节点路径
// 该map作用为:当一个连接彻底断开时,可快速找到当前连接对应的所有
// 注册了监听的节点,以便移除当前客户端对节点的Watcher
private final HashMap> watch2Paths = new HashMap>();
Watcher注册流程.png
Watcher通知流程.png
org.apache.zookeeper.ZooKeeper.png

https://www.jianshu.com/p/c68b6b241943 (zk 的 Watcher 机制)

2.zk安装与使用

zk的端口说明

一、zookeeper有三个端口(可以修改)
1、2181:对clinet端提供服务
2、3888:选举leader使用
3、2888:集群内机器通讯使用(Leader监听此端口)

二、部署时注意
1、单机单实例,只要端口不被占用即可
2、单机伪集群(单机,部署多个实例),三个端口必须修改为组组不一样, 如:
myid1 : 2181,3888,2888
myid2 : 2182,3788,2788
myid3 : 2183,3688,2688
3、集群(一台机器部署一个实例)

三、集群为大于等于3个基数
如 3、5、7....,不宜太多,集群机器多了选举和数据同步耗时时长长,不稳定。
目前觉得,三台选举+N台observer很不错。

2.1 docker-compose安装zk集群(伪集群)

docker-compose.yml

version: '3.4'

services:
  zoo1:
    image: zookeeper:3.4
    restart: always
    hostname: zoo1
    ports:
      - 2181:2181
    environment:
      ZOO_MY_ID: 1
      ZOO_SERVERS: server.1=0.0.0.0:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888

  zoo2:
    image: zookeeper:3.4
    restart: always
    hostname: zoo2
    ports:
      - 2182:2181
    environment:
      ZOO_MY_ID: 2
      ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=0.0.0.0:2888:3888 server.3=zoo3:2888:3888

  zoo3:
    image: zookeeper:3.4
    restart: always
    hostname: zoo3
    ports:
      - 2183:2181
    environment:
      ZOO_MY_ID: 3
      ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=0.0.0.0:2888:3888
      
# notice: execute command 'docker-compose up -d' to run zookeeper clusters

2.2 CentOS7 下安装zk (单机版)

#1.下载zk
wget https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/zookeeper-3.4.14/zookeeper-3.4.14.tar.gz

#2.解压重命名
tar -zxf zookeeper-3.4.14.tar.gz
mv zookeeper-3.4.14 zookeeper

#3.将zookeeper-3.4.3/conf目录下的zoo_sample.cfg文件拷贝一份,命名为为“zoo.cfg”
cd /opt/software/zookeeper/conf
cp zoo_sample.cfg zoo.cfg

#4.修改zoo.cfg内容为:
见下文

#5.创建dataDir参数指定的目录
这里指的是“/opt/software/zk_runtime/data”),并在目录下创建文件,命名为“myid”。

#6.编辑“myid”文件,并在对应的IP的机器上输入对应的编号。
如在zk1上,“myid”文件内容就是1。由于本次只在单点上进行安装配置,所以只有一个server.1。
若还有其他服务器,比如地址为192.168.1.102,
则在zoo.cfg文件中还需加入server.2=192.168.0.105:2888:3888。
那么myid文件在192.168.0.105服务器上的内容就是2。

#7.在/etc/profile文件中设置PATH
export ZOOKEEPER_HOME=/opt/software/zookeeper 
PATH=$ZOOKEEPER_HOME/bin:$PATH

#8.启动zk
zkServer.sh start

#9.查看是否启动成功
lsof -i:2181

zoo.cfg

tickTime=2000

#配置 Zookeeper 接受客户端初始化连接时最长能忍受多少个心跳时间间隔数。
#这里所说的客户端不是用户连接 Zookeeper服务器的客户端,而是 Zookeeper 服务器集群中连接到 Leader 的 Follower 服务器
#当已经超过 10 个心跳的时间(也就是 tickTime)长度后,
#Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。
#总的时间长度就是 10*2000=20 秒。
initLimit=10

#这个配置项标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,
#最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 5*2000=10 秒。
syncLimit=5

dataDir=/opt/software/zk_runtime/data
dataLogDir=/opt/software/zk_runtime/logs

clientPort=2181

#server.A=B:C:D
#其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;
#C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;
#D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,
#而这个端口就是用来执行选举时服务器相互通信的端口。
#如果是伪集群的配置方式,由于 B 都是一样,
#所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。
#2888端口号是zookeeper服务之间通信的端口,而3888是zookeeper与其他应用程序通信的端口。
server.1=192.168.0.104:2888:3888

2.3 CentOS7 下安装zk (集群版)

2.x Zookeeper数据查看工具ZooInspector

#1.下载
https://issues.apache.org/jira/secure/attachment/12436620/ZooInspector.zip

#2.解压即可

#3.运行jar包
// 进入目录ZooInspector\build,运行zookeeper-dev-ZooInspector.jar
java -jar zookeeper-dev-ZooInspector.jar //执行成功后,会弹出java ui client

#4.添加监控主机, 查看相关机器上的数据
点击左上角绿色连接按钮,输入zk服务地址:ip或者主机名:2181

2.x zk监控工具PrettyZoo

https://github.com/vran-dev/PrettyZoo/releases

PrettyZoo.png

https://www.cnblogs.com/linbingdong/p/6253479.html (paxos算法)
https://www.jianshu.com/p/82d0eb9da176 (curator系列)
https://www.jianshu.com/p/70151fc0ef5d (curator-->核心参考)
https://blog.csdn.net/HappyHeng/article/details/89303197 (权限)

4.代码实现

4.1工具类封装(参考apache dubbo)

https://github.com/zhangxin1932/java-tools.git

4.2分布式锁实现

https://www.jianshu.com/p/e4eb43573f84

5. 常见问题集锦

5.1 zookeeper写入数据超过1M大小

https://zookeeper.apache.org/doc/r3.3.3/zookeeperAdmin.html
#需要通过Java的环境变量来设置
jute.maxbuffer:(Java system property: jute.maxbuffer) 
#方案
首先这个参数的修改,有两种方式:
方式一:在zk的部署机器上,进入zk的conf目录下,新建一个java.env文件,然后写入如下内容:
export JVMFLAGS="-Xms512m -Xmx2048m -Djute.maxbuffer=10485760 " (10MB)
方式二:
修改bin目录下的zkServer.sh文件,在vi中搜索start单词,然后修改如下参数:
nohup "$JAVA" "-Djute.maxbuffer=10485760  -Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}"

#重启服务
这里要说明的是,为了避免造成大范围影响,我们操作的方式是先滚动重启服务端zk集群,
即单台修改完成之后,即可重启,然后使用jps -lvm命令,可以在进程列表中找到我们启动的进程,
如果观察环境参数中附带了我们设置的值,即代表修改生效。
然后依次再滚动重启其他的zk机器。启动的顺序一定是按照myid的值,从小到大依次启动,否则有可能导致启动失败。
在所有服务端启动成功后,可以根据需要决定是否需要启动客户端,
如果你真的决定客户端会写入大于1MB的数据包,那么就要在客户端任务启动的时候,同样加上x相同的环境变量:
java -Djute.maxbuffer=10485760 -jar  com.xxxx.Main

https://cloud.tencent.com/developer/article/1516691 (zk单节点存储限制)
https://blog.csdn.net/lbh199466/article/details/100077248 (zk之坑)

参考资源
https://blog.csdn.net/qq_25537177/article/details/84235890 (zk安装)
https://zookeeper.apache.org/doc/r3.4.14/zookeeperObservers.html (zk安装配置observer)
https://www.jianshu.com/p/f45af8027d7f (zooinspector数据监控工具)
http://www.imooc.com/article/251135 (zk概念)
http://www.jasongj.com/zookeeper/fastleaderelection/ (ZAB协议)
https://www.cnblogs.com/frankltf/p/10392151.html (ZAB协议)
http://ddrv.cn/a/174259 (ZAB协议)
https://blog.csdn.net/wangshouhan/article/details/89919404 (选举源码)
https://blog.csdn.net/u014636209/article/details/85411512 (znode)

踩坑指南
https://yq.aliyun.com/articles/227260 (EPHEMERAL实现集群的陷阱)
http://jm.taobao.org/2018/06/13/%E5%81%9A%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0%EF%BC%9F/ (阿里对zk的评与用)

你可能感兴趣的:(zookeeper)