面试阿里,腾讯90%会被问到的zookeeper,把这篇文章看完就够了。
zookeeper高容错数据一致性协议(CP)的分布式小文件系统,提供类似于文件系统的目录方式的数据存储。
- 全局数据一致性:每个server保存一份相同的数据副本,client无论连接到哪个server展示的数据都是一致的。
- 可靠性:一旦事务成功提交,就会被保留下来。
- 有序性:客户端发起的事务请求,在也会顺序的应用在Zookeeper中。
- 数据更新原子性:一次数据更新要么成功要么失败,不存在中间状态。
- 实时性:保证客户端在一个间隔时间范围内获取服务的更新消息或服务器失效信息。
zookeeper单机
数据模型:
每一个节点都是znode(兼具文件和目录两种特点),每一个znode都具有原子操作。znode存储的数据大小有限制(默认1MB),通过绝对路径引用。
znode分为3个部分:
- stat:状态信息,描述znode的版本和权限等信息。
- data:与该znode关联的数据。
- children:该znode下的子节点。
znode节点类型
- 临时节点:该节点的生命周期依赖于创建它的会话,一旦会话结束临时节点就会被删除。临时节点不允许拥有子节点。
- 永久节点:只能通过客户端显示执行删除操作。
- 临时序列化节点。
- 永久序列化节点。
znode序列化:znode的名字后面追加一个不断增加的序列号。每一个序列号对父节点来说是唯一的,可以记录每一个子节点的先后顺序。
znode节点属性
zookeeper的节点属性包括节点数据,状态,权限等信息。
属性 | 说明 |
---|---|
cZxid | znode创建节点的事务ID,Zookeeper中每个变化都会产生一个全局唯一的zxid。通过它可确定更新操作的先后顺序 |
ctime | 创建时间 |
mZxid | 修改节点的事务ID |
mtime | 最后修改时间 |
pZxid | 子节点变更的事务ID,添加子节点或删除子节点就会影响子节点列表,但是修改子节点的数据内容则不影响该ID |
cversion | 子节点版本号,子节点每次修改版本号加1 |
dataversion | 数据版本号,数据每次修改该版本号加1 ,多个客户端对同一个znode进行更新操作时,因为数据版本号,才能保证更新操作的先后顺序性。例:客户端A正在对znode节点做更新操作,此时如果另一个客户端B同时更新了这个znode,则A的版本号已经过期,那么A调用setData不会成功。 |
aclversion | 权限版本号,权限每次修改该版本号加1 |
dataLength | 该节点的数据长度 |
numChildern | 该节点拥有的子节点的数量 |
znode ACL权限控制
ACL权限控制使用 schema:id:permission来标识。
示例:setAcl /test2 ip:128.0.0.1:crwda
Schema
Schema枚举值 | 说明 |
---|---|
world | 使用用户名设置,id为一个用户,但这个id只有一个值:anyone,代表所有人 |
ip | 使用IP地址进行认证,ID对应为一个IP地址或IP段 |
auth | 使用已添加认证的用户进行认证,以通过addauth digest user:pwd 来添加当前上下文中的授权用户 |
digest | 使用“用户名:密码”方式认证 |
permission
权限 | ACL简写 | 描述 |
---|---|---|
CREATE | c | 可以创建子节点 |
DELETE | d | 可以删除子节点(仅下一级节点) |
READ | r | 可以读取节点数据及显示子节点列表 |
WRITE | w | 可以设置节点数据 |
ADMIN | a | 可以设置节点访问控制列表权限 |
权限相关命令
命令 | 使用方式 | 描述 |
---|---|---|
getAcl | getAcl <path> | 读取ACL权限 |
setAcl | setAcl <path> <acl> | 设置ACL权限 |
addauth | addauth <scheme> <auth> | 添加认证用户 |
zookeeper:提供了分布式发布/订阅功能,能让多个订阅者同时监听某一个主题对象,通过watche机制来实现。
zookeeper watch机制
一个Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端,以便通知它们。
- 父节点的创建,修改,删除都会触发Watcher事件。
- 子节点的创建,删除会触发Watcher事件。
监听器watch特性
特性 | 说明 |
---|---|
一次性 | Watcher是一次性的,一旦被触发就会移除,再次使用时需要重新注册。监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。一次性可以减轻这种压力 |
客户端顺序回调 | 客户端 Watcher 回调的过程是一个串行同步的过程。 |
轻量 | Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。 |
监听器原理
- 首先要有一个main()线程,在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
- 通过connect线程将注册的监听事件发送给Zookeeper服务端。
- 在Zookeeper服务端的注册监听器列表中将注册的监听事件添加到列表中。
- Zookeeper监听到有数据或路径变化,就会将这个消息发送
给listener线程。
- listener线程内部调用了process()方法来触发Watcher。
zookeeper会话管理
- 客户端会不时地向所连接的ZkServer发送ping消息,ZkServer接收到ping消息,或者任何其它消息的时候,都会将客户端的session_id,session_timeout记录在一个map中。
- Leader ZkServer会周期性地向所有的follower发送心跳消息,follower接收到ping消息后,会将记录session信息的map作为返回消息,返回给leader,同时清空follower本地的map。 Leader使用这些信息重新计算客户端的超时时间。
- 一旦在session timout的时间到,leader即没有从其它follower上收集到客户端的session信息,也没有直接接收到该客户端的任何请求,那么该客户端的session就会被关闭。
zookeeper数据模型
- zk维护的数据主要有:客户端的会话(session)状态及数据节(dataNode)信息。
- zk在内存中构造了个DataTree的数据结构,维护着path到dataNode的映射以及dataNode间的树状层级关系。为了提高读取性能,集群中每个服务节点都是将数据全量存储在内存中。所以,zk最适于读多写少且轻量级数据的应用场景。
3.数据仅存储在内存是很不安全的,zk采用事务日志文件及快照文件的方案来落盘数据,保障数据在不丢失的情况下能快速恢复。
Zookeeper集群
集群角色
- Leader:集群工作的核心,事务请求的唯一调度和处理者,保证事务处理的顺序性。对于有写操作的请求,需统一转发给Leader处理。Leader需决定编号执行操作。
- Follower:处理客户端非事务请求,转发事务请求转发给Leader,参与Leader选举。
- Observer观察者:进行非事务请求的独立处理,对于事务请求,则转发给Leader服务器进行处理.不参与投票。
事务
- 事务:ZooKeeper中,能改变ZooKeeper服务器状态的操作称为事务操作。一般包括数据节点创建与删除、数据内容更新和客户端会话创建与失效等操作。对应每一个事务请求,ZooKeeper 都会为其分配一个全局唯一的事务ID,用 ZXID 表示,通常是一个64位的数字。每一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些事务操作请求的全局顺序。
- 事务日志:所有事务操作都是需要记录到日志文件中的,可通过 dataLogDir配置文件目录,文件是以写入的第一条事务zxid为后缀,方便后续的定位查找。zk会采取“磁盘空间预分配”的策略,来避免磁盘Seek频率,提升zk服务器对事务请求的影响能力。默认设置下,每次事务日志写入操作都会实时刷入磁盘,也可以设置成非实时(写到内存文件流,定时批量写入磁盘),但那样断电时会带来丢失数据的风险。事务日志记录的次数达到一定数量后,就会将内存数据库序列化一次,使其持久化保存到磁盘上,序列化后的文件称为"快照文件"。有了事务日志和快照,就可以让任意节点恢复到任意时间点
?
- 数据快照:数据快照是zk数据存储中另一个非常核心的运行机制。数据快照用来记录zk服务器上某一时刻的全量内存数据内容,并将其写入到指定的磁盘文件中,可通过dataDir配置文件目录。可配置参数snapCount,设置两次快照之间的事务操作个数,zk节点记录完事务日志时,会统计判断是否需要做数据快照(距离上次快照,事务操作次数等于[snapCount/2~snapCount] 中的某个值时,会触发快照生成操作,随机值是为了避免所有节点同时生成快照,导致集群影响缓慢)。
过半原则
?1.?过半:所谓“过半”是指大于集群机器数量的一半,即大于或等于(n/2+1),此处的“集群机器数量”不包括observer角色节点。leader广播一个事务消息后,当收到半数以上的ack信息时,就认为集群中所有节点都收到了消息,然后leader就不需要再等待剩余节点的ack,直接广播commit消息,提交事务。半数选举导致zookeeper通常由2n+1台server组成。
zookeeper的两阶段提交:zookeeper中,客户端会随机连接到 zookeeper 集群中的一个节点,如果是读请求,就直接从当前节点中读取数据。如果是写请求,那么请求会被转发给 leader 提交事务,然后 leader 会广播事务,只要有超过半数节点写入成功,那么写请求就会被提交。
- Leader将写请求转化为一个Proposal(提议),将其分发给集群中的所有Follower节点。
- Leader等待所有的Follower节点的反馈,一旦超过半数Follower进行了正确的反馈,那么Leader就会再次向所有的Follower节点发送Commit消息,要求各个Follower节点对前面的一个Proposal节点进行提交。
- leader节点将最新数据同步给Observer节点。
- 返回给客户端执行的结果。
ZAB协议
ZooKeeper 能够保证数据一致性主要依赖于 ZAB 协议的消息广播,崩溃恢复和数据同步三个过程。
消息广播
- 一个事务请求进来之后,Leader节点会将写请求包装成提议(Proposal)事务,并添加一个全局唯一的 64 位递增事务 ID,Zxid。
- Leader 节点向集群中其他节点广播Proposal事务,Leader 节点和 Follower 节点是解耦的,通信都会经过一个 FIFO 的消息队列,Leader 会为每一个 Follower 节点分配一个单独的 FIFO 队列,然后把 Proposal 发送到队列中。
- Follower 节点收到对应的Proposal之后会把它持久到磁盘上,当完全写入之后,发一个ACK给Leader。
- 当Leader节点收到超过半数Follower节点的ACK之后会提交本地机器上的事务,同时开始广播commit,Follower节点收到 commit 之后,完成各自的事务提交。
消息广播类似一个分布式事务的两阶段提交模式。在这种模式下,无法处理因Leader在发起事务请求后节点宕机带来的数据不一致问题。因此ZAB协议引入了崩溃恢复机制。
崩溃恢复
当整个集群在启动时,或者Leader失联后,ZAB协议就会进入恢复模式,恢复模式的流程如下:
- 集群通过过民主举机制产生新的Leader,纪元号加1,开始新纪元
- 其他节点从新的Leader同步状态
- 过半节点完成状态同步,退出恢复模式,进入消息广播模式
Leader选举流程
server工作状态
状态 | 说明 |
---|---|
LOOKING | 竞选状态,当服务器处于该状态时,它会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态。 |
FOLLOWING | 跟随者状态。表明当前服务器角色是 Follower。它负责从Leader同步状态,并参与选举投票。 |
LEADING | 领导者状态。表明当前服务器角色是 Leader。 |
OBSERVING | 观察者状态,表明当前服务器角色是Observer,它负责从同步leader状态,不参与投票。 |
选举原则
- 选举投票必须在同一轮次中进行,如果Follower服务选举轮次不同,不会采纳投票。
- 数据最新的节点优先成为Leader,数据的新旧使用事务ID判定,事务ID越大认为节点数据约接近Leader的数据,自然应该成为Leader。
- 如果每个个参与竞选节点事务ID一样,再使用server.id做比较。server.id是节点在集群中唯一的id,myid文件中配置。
选举阶段
集群间互传的消息称为投票,投票Vote主要包括二个维度的信息:ID、ZXID
?
ID ? 候选者的服务器ID
?ZXID 候选者的事务ID,从机器DataTree内存中获取,确保事务已经在机器上被commit过。
选主过程中主要有三个线程在工作
- 选举线程:主动调用lookForLeader方法的线程,通过阻塞队sendqueue及recvqueue与其它两个线程协作。
- WorkerReceiver线程:选票接收器,不断获取其它服务器发来的选举消息,筛选后会保存到recvqueue队列中。zk服务器启动时,开始正常工作,不停止
- WorkerSender线程:选票发送器,会不断地从sendqueue队列中获取待发送的选票,并广播至集群。
- WorkerReceiver线程一直在工作,即使当前节点处于LEADING或者FOLLOWING状态,它起到了一个过滤的作用,当前节点为LOOKING时,才会将外部投票信息转交给选举线程处理;
- 如果当前节点处于非LOOKING状态,收到了处于LOOKING状态的节点投票数据(外部节点重启或网络抖动情况下),说明发起投票的节点数据跟集群不一致,这时,当前节点需要向集群广播出最新的内存Vote(id,zxid),落后的节点收到该Vote后,会及时注册到leader上,并完成数据同步,跟上集群节奏,提供正常服务。
全新集群选举
- 每一机器都给自己一票。
- 主要服务器ID的值,值越大选举权重越大。
- 投票数过半,选举结束。
非全新集群选举
- 逻辑时钟:逻辑时钟小的选举结果被忽略
- 数据ID:数据ID大的胜出
- 服务ID:数据ID相同,服务器ID大的胜出,被选举为leader。
选举过程详细说明
Leader选举是集群正常运行的前提,当集群启动或Leader失联后,就会进入Leader选举流程。
- 所有节点进入LOOKING状态
- 每个节点广播携带自身ID和ZXID的选票,投票推举自己为Leader
- 节点接收其他节点发送的选票,把选票信息和自己推举的选票进行PK(选票中ZXID大者胜出,ZXID相同,则ID大者胜出)
- 如果外部选票获胜,则保存此选票信息,并把选票广播出去(赞成该选票)
- 循环上述3-4步骤
- 当有选票得到超过半数节点赞成,且该选票的所有者也赞成该选票,则选举成功,该选票所有者成为Leader
- Leader切换为LEADING,Follower切换为FOLLOWING,Observer切换为OBSERVING状态,选举结束,进入数据同步流程。
数据同步流程
数据同步流程,是要以Leader数据为基础,让集群数据达到一致状态。
- 新Leader把本地快照加载到内存,并通过日志应用快照之后的所有事务,确保Leader数据库是最新的。
- Follower和Observer把自身的ZXID和Leader的ZXID进行比较,确定每个节点的同步策略
- 根据同步策略,Leader把数据同步到各节点
- 每个节点同步结束后,Leader向节点发送NEWLEADER指令
- 同步完成的Follower节点返回ACK
- 当Leader收到过半节点反馈的ACK时,认为同步完成
Leader向Follower节点发送UPTODATE指令,通知集群同步完成,开始对外服务。
zk应用举例
- 命名服务:通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。通过创建全局唯一的path作为一个名字。
- 分布式锁:独占锁,获取数据之前要求所有的应用去zk集群的指定目录去创建一个临时非序列化的节点。谁创建成功谁就能获得锁,操作完成后断开节点。其它应用如果需要操作这个文件就可去监听这个目录是否存在。
- 控制时序:通过创建一个临时序列化节点来控制时序性。
- 心跳检测:让不同的进程都在ZK的一个指定节点下创建临时子节点,不同的进程直接可以根据这个临时子节点来判断对应的进程是否存活。大大减少了系统耦合。
- master选举:每个客户端请求创建同一个临时节点,那么最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很容易地在分布式环境中进行 Master 选举了。成功创建该节点的客户端所在的机器就成为了Master。同时,其他没有成功创建该节点的客户端,都会在该节点上注册一个子节点变更的 Watcher,用于监控当前 Master 机器是否存活,一旦发现当前的Master挂了,那么其他客户端将会重新进行 Master 选举。
zookeeper缺点:
?1. 非高可用:极端情况下zk会丢弃一些请求:机房之间连接出现故障。
- zookeeper master就只能照顾一个机房,其他机房运行的业务模块由于没有master都只能停掉,对网络抖动非常敏感。
- 选举过程速度很慢且zk选举期间无法对外提供服务。
- zk的性能有限:典型的zookeeper的tps大概是一万多,无法覆盖系统内部每天动辄几十亿次的调用。因此每次请求都去zookeeper获取业务系统master信息是不可能的。因此zookeeper的client必须自己缓存业务系统的master地址。
- zk本身的权限控制非常薄弱.
- 羊群效应: 所有的客户端都尝试对一个临时节点去加锁,当一个锁被占有的时候,其他的客户端都会监听这个临时节点。一旦锁被释放,Zookeeper反向通知添加监听的客户端,然后大量的客户端都尝试去对同一个临时节点创建锁,最后也只有一个客户端能获得锁,但是大量的请求造成了很大的网络开销,加重了网络的负载,影响Zookeeper的性能.
? *?解决方法:是获取锁时创建一个临时顺序节点,顺序最小的那个才能获取到锁,之后尝试加锁的客户端就监听自己的上一个顺序节点,当上一个顺序节点释放锁之后,自己尝试加锁,其余的客户端都对上一个临时顺序节点监听,不会一窝蜂的去尝试给同一个节点加锁导致羊群效应。
- zk进行读取操作,读取到的数据可能是过期的旧数据,不是最新的数据。如果一个zk集群有10000台节点,当进行写入的时候,如果已经有6K个节点写入成功,zk就认为本次写请求成功。但是这时候如果一个客户端读取的刚好是另外4K个节点的数据,那么读取到的就是旧的过期数据。
zookeeper脑裂:
假死:由于心跳超时(网络原因导致的)认为leader死了,但其实leader还存活着。
脑裂
由于假死会发起新的leader选举,选举出一个新的leader,但旧的leader网络又通了,导致出现了两个leader ,有的客户端连接到老的leader,而有的客户端则连接到新的leader。
quorum(半数机制)机制解决脑裂
在zookeeper中Quorums有3个作用:
- 集群中最少的节点数用来选举leader保证集群可用。
- 通知客户端数据已经安全保存前集群中最少数量的节点数已经保存了该数据。一旦这些节点保存了该数据,客户端将被通知已经安全保存了,可以继续其他任务。而集群中剩余的节点将会最终也保存了该数据。
- 假设某个leader假死,其余的followers选举出了一个新的leader。这时,旧的leader复活并且仍然认为自己是leader,这个时候它向其他followers发出写请求也是会被拒绝的。因为每当新leader产生时,会生成一个epoch标号(标识当前属于那个leader的统治时期),这个epoch是递增的,followers如果确认了新的leader存在,知道其epoch,就会拒绝epoch小于现任leader epoch的所有请求。那有没有follower不知道新的leader存在呢,有可能,但肯定不是大多数,否则新leader无法产生。Zookeeper的写也遵循quorum机制,因此,得不到大多数支持的写是无效的,旧leader即使各种认为自己是leader,依然没有什么作用。