zookeeper有什么用
zk是一个分布式领域的知名中间件,可以在其中存储 key-value形式的数据,每个key-value数据我们可以称为一个znode,znode之间又可以通过树形的方式来组织,从而产生父节点和子节点的联系。除了数据存储之外,zk还有很多特性,比如watch机制、临时节点机制,这使得zk可以用来实现很多其他需求,比如命名注册、分布式锁、pubsub机制、数据同步等
zookeeper 集群架构
zookeeper的特点
高可用
zk的高可用是通过冗余节点来保证的,如果其中有一个节点挂掉,还是能提供服务的。如果要保证所有节点都可以用来处理读请求,则需要所有节点数据做replication复制,而zk有专门的机制来保证这一点。
可扩展性
zk可以在线添加节点,新的节点会被很快纳入集群并提供服务,很方便的进行扩容。
读写操作
由于zk集群内的数据是一致的,因此读操作可以由任一节点来完成;而写操作由唯一的leader节点来统一控制,并同步到所有节点。实际应用中,client连接到集群中时,是通过tcp长连接连到任一可用的节点上的,如果是读,直接通过当前连接的节点来完成,如果是写操作,则会统一转发到leader节点来完成,等所有的节点数据都同步后,写操作才算完成,并通知对应的client。
所有的写操作都通过唯一的leader节点完成,也保证了客户端的操作的顺序性,这样在处理并发时就不会出问题。
数据同步
除了正常写操作时,所有节点的数据需要同步之外,如果有节点新加入集群(可能是新增或者宕机之后恢复),也会通过同步指令来保证新增的节点数据和其他节点的一致性。
watch机制
由于client与集群节点之间是通过 tcp 长连接来进行通信的,且会通过心跳来做健康检查,因此任何节点的变化都可以通过tcp实时通知到所有的client。
zookeeper的数据模型
zk的数据是存在磁盘上的,以目录树的形式存储,是由很多个 dataNode节点组成的一个树形结构,这个树形结构除了存储在磁盘中,在每个zk节点的内存中也维护着,叫做 DataTree,包括了zk集群中所有dataNode的数据。
除了实际数据,zk还有一份事务日志,类似于 mysql 的 binlog,这其实可以理解成增量修改。
zk在内存中构造了个DataTree的数据结构,维护着 path 到 dataNode 的映射以及 dataNode 间的树状层级关系。为了提高读取性能,集群中每个服务节点都是将数据全量存储在内存中。可见,zk最适于读多写少且轻量级数据(默认设置下单个 dataNode 限制为1MB大小)的应用场景。数据仅存储在内存是很不安全的,zk采用事务日志文件及快照文件的方案来落盘数据,保障数据在不丢失的情况下能快速恢复。
zk在集群中实际写数据的时候,为了保证一致性,使用了两阶段提交,其中第一阶段就是写这个事务日志,第二阶段才是实际更新内存中的 DataTree,也即当前数据。
数据快照位于磁盘的 version-2/snapshot.xxx 文件,日志文件为 version-2/log.xxxxx,所有写事务操作都是需要记录到日志文件中的,可通过 dataLogDir配置文件目录,文件是以写入的第一条事务zxid为后缀,方便后续的定位查找。默认设置下,每次事务日志写入操作都会实时刷入磁盘。
这些数据都是二进制格式,如果要查看,需要用lib目录的一些工具jar包:
查看实际的数据快照:
1 | $ cd lib |
查看事务日志:
1 | $ cd lib |
zk存储的数据是保证的最终一致性,也就是说数据最终会一致。对于zk这种分布式系统来说,最好的情况当然是所有节点数据一致,但也可能出现数据不一致的情况(比如两阶段提交时,有个别节点没有回应 ACK),这时一般是leader节点的数据比其他节点数据新,可以通过不停的同步来将所有节点的数据最终和leader节点保持一致。
根据CAP定理,P必然无法保障,因此A和C需要做权衡。zk是优先考虑高可用(A),牺牲了完全一致性,但他是最终一致性的。也就是说,由于多个节点的数据不会时刻保证一致性,因此在读取时是可能读取不到最新数据的。
zookeeper的ZAB协议
ZAB全称 Zookeeper Atomic Broadcast Protocol,意思就是 zk 的原子广播协议,这是zk的核心机制。它实际是 multi-paxos 的一种实现,类似的multi-paxos 实现的还有 raft 算法(ETCD采用)。
ZAB 简单来说是定义了一套规则,用来在各个节点之间复制数据。此外,它还定义了如何选举leader,并定义了如何处理节点恢复。其实就是分别实现了 multi-paxos 专门拆分的几个子问题。
数据复制
ZAB使用两阶段提交来同步数据。当然这个两阶段提交并不是传统意义上的两阶段提交,而是稍微改造过的,第一阶段只需要超过半数节点有 ACK 响应就实施第二阶段,即 COMMIT 数据修改。而且,在第一阶段,propose 不会被拒绝,因为这个过程就是简单的写日志,所以不存在拒绝导致回滚的处理。总之,是一个简化版本的二阶段提交。
另外需要注意的是,leader和follower之间的数据传输都是按照 FIFO 规则来控制的,所有的 propose 按照顺序依次处理,这样就能保证所有的修改操作都是顺序执行的;此外,server之间通信是采用 TCP 的,TCP的顺序性也保证了传输的顺序性。最终,我们解决了因为分布式环境下并发带来的数据不一致问题。
leader选举
数据同步的二阶段提交是需要一个协调者来发起和统筹整个过程的,在 zk 集群中就由 leader 来充当这个角色。所以如何选举 leader 也是个很重要的事情。
zk集群一启动,就需要选举一个leader,完成后进入对外服务状态;如果服务过程中,leader节点宕机,也需要在剩余节点中重新选举 leader,完成后再继续提供服务;
那什么样的人可以称为leader呢?
- 首先,leader 节点的数据必须要是最新的
- 其次,leader 被过半数的节点推举
选举的过程有点类似于现实生活中的投票,只要一个候选节点获得的票数超过半数,就成为 leader。稍微有点不同的是,每个投票者是可以多次修改投给哪个候选人的,只要任一时刻一个候选人获得的票数超过一半,leader 随机产生,选举过程也就结束了。
具体来讲,就是每个节点可以投票给集群中的任一节点,并将自己的投票结果通过广播 (broadcast) 通知其他节点。每个节点都会实时统计所有节点的得票情况,如果发现有节点得票过半,就完成选举。
需要说明的是,投票的标准是什么?其实就是各个节点的数据版本号,版本号约高的可以认为数据越新,在投票过程中,数据新的节点胜出,因为数据新的可以同步给数据老的,从而集群整体的数据更新;如果数据版本号一致,就比较节点的序号,约定序号大的节点胜出。
集群启动一开始,由于没有广播通信,各个节点都不知道其他节点的数据版本号,无从比较,因此都默认投给自己,并将投给自己的信息广播给其他节点。每个节点在收到广播后,将别人的数据版本号和自己的版本号比较,如果一样就比较节点序号,通过比较如果发现别人胜出,就修改自己的投票,再次广播给其他节点。通过这样一个不停广播加不停修改选票的过程,最终肯定会有一个节点的得票超过半数,并成为 leader 节点。
注意,只要有一个节点超过半数,就立即停止选举过程,后续收到的其他投票信息也不再处理了。例如有3个节点,只要有一个节点的票数大于等于2,则选举结束。有没有可能出现一种情况,就是有节点得票过半后,有一个节点的数据版本比 leader 的数据还要新,它又广播过来一个消息投票给自己?这时候是否要重新选举?
答案是否,不会重新选举。我们可以这样来理解,这个后来的节点,它的数据虽然比较新,但是他的数据并没有同步到其他过半节点(否则别人就会投票给它,而不是投给我们已经选出的数据版本更老一些的leader了),这是什么情况下才出现的?很可能是集群宕机,leader的数据没有及时同步给其他节点。这相当于一个没有完成的写操作(只修改了leader的数据,而没有来得及修改其他节点的数据),未完成的操作,其数据是可以丢弃的。实际遇到这种情况时,这个后来的节点会把自己的数据回滚到和 leader一致。
zookeeper如何防止脑裂
说人话,脑裂意思是集群中有多个leader,类似leader也就是大脑裂开一样。秘诀就是任何leader的选举都需要得票过半数。
参考
Apache ZAB—Zookeeper Atomic Broadcast Protocol)