zookeeper是什么

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,由Yahoo公司开发,目前广泛应用于互联网领域。

我们可以按照字面意思去理解它,zookeeper,就是动物园管理者,只不过我讲的这个zookeeper他管理的不是动物园里的动物,而是分布式系统里的服务器,作为一个管理员,他要做的就是协调分布式系统中的多个服务器,使得系统可以正常工作。

Zookeeper提供了什么

那么zookeeper提供了什么呢,实际上他只提供了三个东西,一个是文件系统,一个是通知机制,还有一个是集群管理机制,这个看似很简单,但却可以解决分布式系统中的许多问题。那么接下来我就具体介绍一下zookeeper可以做什么。

Zookeeper可以做什么

命名服务

首先是命名服务。命名服务就是帮助我们对资源进行命名的服务,命名的主要目的是能够更好的定位,比如在茫茫人海中,一叫你的名字就能立刻找到你,就是这个意思。

刚才我提到zookeeper提供的第一个东西就是文件系统,指的就是zookeeper提供了这样一种树形结构的文件系统,zookeeper可以在自己的文件系统中创建以路径为名称的节点,用于存储服务器地址等信息。阿里巴巴开发的分布式服务框架DUBBO就是用zookeeper来作为其命名服务,维护全局的服务器列表,实现方式就是服务器启动时,在ZK的某个路径下创建一个代表自己的节点。

配置管理

接下来说一下配置管理。每个程序一般都会需要一些配置信息,如果程序分散部署在多台机器上,要逐个改变配置就变得很困难。现在把这些配置全部放到zookeeper上去,保存在 Zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 Zookeeper 的通知,然后就从 Zookeeper 获取新的配置信息并应用到系统中,这样就省去了手动去逐个修改配置信息的麻烦。其实这个过程中最关键的一点就是用到了zookeeperwatcher机制,watcher就是之前提过的zookeeper的发布/订阅机制,客户端可以向zookeeper服务器注册watcher,订阅自己感兴趣的节点,当相应的节点发生变化时,zookeeper服务器就会向客户端发布通知。

这种发布/订阅机制不仅可以用于配置管理,还可以用于实现许多其他功能,比如热切换。

这是热切换的一种场景,系统中有两台数据库服务器和一台接入服务器,其中一台数据库服务器是备用的,系统运行过程中,接入服务器需要向数据库查询一些数据,热切换就是指,当主数据库出现故障时,接入服务器可以自动发现故障并切换到备用数据库服务器进行数据查询。使用zookeeper实现热切换的流程就是主数据库启动时先向zookeeper服务器发起注册,这样zookeeper服务器就会创建一个代表主数据库的节点,之后主数据库和zookeeper之间会建立链路监控,也就是每隔指定时间,数据库会向zookeeper服务器发送一条消息表示自己还在正常运行。而接入服务器则需要向zookeeper服务器订阅主数据库的信息。当zookeeper没有在指定时间收到主数据库发来的链路监控消息时,就会认为主数据库出现故障,并向接入服务器发布通知,接入服务器就会切换到备用数据库进行数据查询,这样就实现了热切换。

分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式 。

在分布式系统中,虽然有多个服务器提供服务,但系统数据只有一套,也就是多个服务器要使用同一套数据,这时就存在两个问题,一个是某服务器在读数据时,另一个服务器修改了数据,导致读到的数据不是最新的数据,另一个问题时某服务器在修改数据的时候另一个服务器读这块数据,这也会导致读到的数据不是最新的,而分布式锁就是用来解决这两种问题的一种机制。

针对两种问题,分布式锁也分为共享锁和排它锁两类。

共享锁又称读锁,简称S锁,共享锁就是在事务T要读取数据A时,对A加上S锁,这时其他事务也可以读数据A,但是就不能修改数据A了,这样就避免了读数据的同时数据被修改。

排它锁,也就是写锁,是在事务T要修改数据A时,对A加上X锁,这时就只有事务T可以读取或修改数据A,而其他事务无法在读写数据A,这样就避免了修改数据时数据被别人读取。

那么zookeeper是怎么实现分布式锁的呢?

其实得益于zookeeper的树形数据结构以及节点的一些特性,在zookeeper中实现分布式锁非常简单,而且有多种方法,这里我就就介绍一种比较通俗易懂的实现方式。

首先,想要获取锁的人要在特定目录下创建一个临时节点,并标明创建的是读锁还是写锁,接下来就需要获取并监听这个目录下所有的子节点,因为这个目录下的所有节点都是要获取锁的人,那么既然这么多人都想获取锁,自己到底能不能获取到呢,这就需要判断自己在子节点中的顺序,之后,如果需要的是读锁,那么就看目录下有没有比自己序号小的写节点,如果没有,说明之前的锁都是读锁,那么自己就也可以读,这样就成功获得了读锁而如果需要的是写锁,那就需要看自己是不是目录下序号最小的节点了,如果不是,就要等前面的节点都释放掉了自己才能获得写锁。注意在读取或修改完数据之后,要释放自己申请的锁,不然别人就一直无法正常访问锁住的数据了,而释放的方式也很简单,就是删除自己创建的这个节点,这样排在自己后面的节点就成为序号最小的节点就可以获取锁了。

对于分布式锁还存在一个问题就是死锁,死锁就是指节点申请到锁以后出了故障崩溃了无法主动释放锁,导致这块数据被永久的锁住了,对于这个问题zookeeper也可以轻松解决,我们看申请锁的第一步就是创建一个临时节点,为什么创建的是临时节点呢,就是因为临时节点在出现故障之后会自动删除,这样也就不会导致死锁的发生。

队列管理

在分布式系统中还有一个非常重要的概念就是消息队列,消息队列可以解决应用解耦、异步消息、流量削锋等问题,实现系统的高性能、高可用。我们先看下面这两个图来理解一下什么是消息队列。

首先传统的用户请求都是请求者直接发给服务器,服务器处理完成之后在将结果返回给请求者,这样会导致一个问题就是请求者在发出请求后会阻塞住,在收到服务器回复之前什么都干不了,而如果引入了消息队列,那么这个处理的流程就会变成请求者先把请求发给消息队列,然后服务器在从消息队列获得需要处理的消息,处理完成之后在将结果返回给请求者,这种方式下,请求者只要把请求发给消息队列就可以干其他事情了,这样就实现了请求的异步处理,也解除了请求者和服务器之间的耦合。

而流量削锋是什么意思呢,实际上就是在一些抢购的时候,抢购的时间一到,大量的请求会几乎同时到达服务器,这很容易导致服务器崩溃,而使用消息队列的话,所有的请求都先发到消息队列,把消息队列的长度设置为抢购商品的数量,超过队列长度的请求直接返回失败,而成功进入队列的请求再由服务器慢慢处理,这样就轻松化解了抢购时服务器短时间的巨大压力。

在分布式系统中,同类的请求可以发送到同一个队列中,系统中的多台服务器都可以从队列中获取请求并进行处理,这样实际上也就实现了将请求分配给多个服务器去处理,再配合适当的负载均衡算法,就可以充分的利用系统中的服务器集群了。

利用zookeeper的顺序节点可以非常简单的实现队列。

生产者,也就是请求的发起者每产生一个请求,就会在指定目录下创建一个节点,并将请求内容存储在节点中,而消费者,也就是消息的处理者,从目录下获取序号最小的节点里存储的请求内容,获取请求后删除该节点,避免请求被重复处理,这样就实现了分布式系统的消息队列。

集群管理

之前所讲的不管是命名服务还是配置管理还是分布式锁还有队列管理,它们都是单个zookeeper服务器可以实现的功能,但由于zookeeper充当了分布式系统服务协调者的角色,因此一旦zookeeper服务器出现故障,整个系统就会瘫痪,另外,当系统中服务器数量较大时,单个zookeeper服务器要处理所有服务器的协调业务也会显得力不从心,因此zookeeper本身也需要以集群的形式运行在分布式系统中,而其他服务器则可以通过任意一台zookeeper服务器获得协调服务。但这也对zookeeper提出了一个新的要求,就是zookeeper集群的所有zookeeper服务器要向外呈现一样的视图,也就是说不管连接了哪个zookeeper服务器,获取到的信息都是一样的。所以,zookeeper需要提供一种可靠的集群管理机制。

zookeeper集群中,每个zookeeper的根目录下,都有一个group member节点,每个加入集群的zookeeper服务器都要在该目录下创建节点,这样group member节点就存储了集群中所有zookeeper服务器的信息。

接下来要做的就是保证每个zookeeper服务器,他的数据都是一致的,为了做到这一点,我们需要保证集群中只有一个zookeeper服务器有写的权利,为什么呢,我们举一个简单的例子:

 

这个系统中有两台zookeeper服务器和两个数据库,两个数据库分别注册到两个zookeeper服务器上,zookeeper会在DB目录下依次创建节点,如果说DB1先注册,DB2后注册,那么ZK1会先在DB目录下创建DB1节点并通知ZK2,这样DB2再向ZK2注册时,ZK2就会在DB目录下创建DB2节点,这样不会有任何冲突,但是如果DB1DB2恰好同时发起了注册,或者注册的时间差很小,那么ZK1ZK2就会同时在DB目录下创建DB1节点并通知对方,这样就会产生冲突导致ZK1ZK2无法同步,这就是集群中每个zookeeper服务器都有写权限的后果。而如果只有ZK1有写权限,当DB2ZK2发起注册时,ZK2会告知ZK1,由ZK1写入再同步系统中所有ZK,这样就避免了冲突的问题。所以说集群中就需要一个leader来执行所有的写操作,这样才能保证集群中各个zookeeper节点的一致性。但是这又产生了一个新的问题,就是怎么选取这个leader

Zookeeper集群中,leader选举是基于paxos算法实现的,paxos算法是一种基于消息传递的一致性算法,在分布式领域应用的非常广泛,大家有兴趣的可以自己深入学习一下,我这里就不细讲了,这里我就简单的概括一下zookeeper选举leader的几个步骤。首先是第一轮投票,所有ZK节点第一次投票都把会票投给自己,也就是都推选自己当leader,因为这个时候每个节点都不知道其他节点的信息,也不知道应该投给谁,所以就先投自己一票,投的这个票上呢有两个重要信息,一个是sid,另一个是zxidsid就是serverid,也就是这个ZK节点在集群中的序号,每个ZK节点的sid都是唯一的,不可能重复,而zxid是一个递增的标识事务的id,用来保证事务的顺序一致性,也就说zxid越大,对应的事务就越新,因此在处理第一轮投票的时候,首先要看的就是谁的zxid最大,因为zxid最大的ZK节点提出了最新的事务,也就是说他的状态是最新的,所以集群中所有节点都应该与这个节点同步,如果zxid相同,比如在系统刚启动时所有ZK节点都还没有产生事务,zxid都还是0,这时就选择sid最大的ZK节点,之前我们说过sid是不会重复的,因此一定存在一个sid最大的ZK节点,经过对第一轮投票的处理之后,所有ZK节点都会按照这个规则进行第二轮投票,直到某个ZK节点获得超过半数的投票,该节点就成功的当选了集群的leader,这里还要强调的一点就是一定要获得超过半数的投票才能当选leader,比如在系统启动的过程中,不同的ZK节点不是同时启动的,假设集群由5ZK节点,有两个先启动了,这两个节点会先统一意见,比如都投给ZK2,但这时ZK2还不能成为leader,因为集群有5ZK节点,必须在某个节点获得3票以上时才能产生leader,这是选举leader时需要注意的一点。

leader选举出来之后,集群中的写操作就可以由leader来负责完成了,leader在收到写请求之后,使用了一种消息广播机制来实现所有节点的同步,首先leader接收到写请求会产生一个proposalleader会将这个proposal发送给集群中的其他follower节点,注意这里是通过消息队列来发送的,leader节点和每个follower节点之间都存在一个消息队列,关于消息队列我刚刚讲过,他可以实现异步解耦,如果这里没有消息队列,leader每发一条proposal给一个节点,都要等到回复才能给下一个节点发,这样延时会非常大,而使用消息队列的话,leader只需把proposal发给所有的消息队列,再由follower从队列获取proposal并处理就可以了,follower收到proposal之后,会把数据先复制下来,并回复ACK,在leader收到半数个ACK之后,就会向所有follower节点发送commit提交,也就是告知本次写操作生效了,其实这就类似是一种投票决议的过程,超过半数的人同意了proposal,这个proposal就可以生效了。这个就是zookeeper数据同步的流程,我们可以发现,不管是选举leader,还是数据同步,都需要获得半数以上节点的支持,这其实正是zookeeper集群的一个特性,就是只有集群中超过一半节点能够正常工作,这个集群才可以使用,如果说集群中半数以上ZK节点挂掉了,这个集群就不可用了。

总结

Zookeeper适合读请求较多的系统

首先,zookeeper适合读请求较多的系统,下图是zookeeper官网给出的一组测试数据,横坐标是读请求占总请求的比例,纵坐标是zookeeper集群每秒可以处理的请求数,可以看到,读请求所占比例越大,zookeeper每秒可以处理的请求就越多,这个其实很好理解,因为zookeeper集群里多个ZK节点可以分担系统读的压力,但是每次写请求都要经过一个比较复杂的同步过程,因此zookeeper集群对写请求的处理能力实际还不如单服务器强,所以说对于写请求占很大比例的系统,zookeeper并不适用。

Zookeeper不能保证可用性

第二点是zookeeper不能保证可用性,解释这一点之前我要先介绍一下分布式系统的CAP原则,CAP原则指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼,在分布式系统中最多只能满足其中的两项。其中,一致性是指集群所有节点对外呈现的数据是一致的,也就是同一时刻不会出现从两个不同的节点读取数据结果会不同的情况,这一点zookeeper是满足的,因为之前讲过zookeeper有一套非常可靠的数据同步机制,分区容错性是指如果分布式系统部署在多个网络分区,其中部分网络分区出现了问题,系统仍然可用,这点zookeeper也是满足的,因为部分网络分区除了问题只会导致这些分区的节点不可用,但不会影响整体系统,但是可用性zookeeper却是无法保证的,这一方面是因为zookeeper集群必须保证半数以上节点正常,整个系统才能工作,如果半数以上节点崩溃了,系统就不可用了,另一方面可用性要求的是对所有的请求都能在较短时间内得到响应,比如搜索引擎类的系统,要求搜索一个关键字可以在0.3秒之内给出结果,才算是具有可用性,如果时间太长,那只能说是能用,不能算是可用,对zookeeper而言,问题有两个,一个是所有的写请求要完成都需要经过一个集群内所有节点同步的过程,如果同步失败或时间太长,就违背了可用的原则,另一个问题是集群的leader出现问题后,要经过选举和恢复同步两个过程集群才能继续正常工作,那么在这段时间系统就是不可用的,因此我们说zookeeper无法满足可用性,对于某些服务。它对一致性要求不高,但一定要保证可用性,这时候zookeeper就不适用了。当然这并不是说zookeeper设计的不好,因为zookeeper设计的主要目的就是确保分布式集群的一致性,在某些场景,如果一致性无法满足可能会导致系统更混乱,在这些场景放弃一部分可用性是值得的,毕竟CAP这三点是无法同时满足的,所以在实际的分布式应用中,我们还是应该根据具体情况进行取舍,选择最适合我们的系统架构和协调机制。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐