4.3.1 分布式系统与Zookeeper

随着大型网站的各种高并发访问、海量数据处理等场景越来越多,如何实现网站的高可用、易伸缩、可扩展、安全等目标就显得越来越重要。为了解决这样一系列问题,大型网站的架构也在不断发展。提高大型网站的高可用架构,不得不提的就是分布式。
本节我们简单介绍分布式系统及zookeeper的相关概念和原理。

1 什么是分布式系统

1.1 单机结构

大家最熟悉的就是单机结构,当一个系统业务量很小的时候所有的代码都放在一个项目中就好了,然后这个项目部署在一台服务器上。整个项目所有的服务都由这台服务器提供。这就是单机结构。显而易见的,单机的处理能力是有限的,当你的业务增长到一定程度的时候,单机的硬件资源将无法满足你的业务需求。此时便出现了集群模式。

1.2 集群结构

将单机复制几份,这样就构成了一个集群。集群中每台服务器就叫做这个集群的一个节点,每个节点都提供相同的服务,这样系统的处理能力就相当于提升了好几倍。但问题是用户的请求究竟由哪个节点来处理呢?最好能够让负载较小的节点来处理,这样使得每个节点的压力都比较平均。要实现这个功能,就需要在所有节点之前增加一个“调度者”的角色,用户的所有请求都先交给它,然后它根据当前所有节点的负载情况,决定将这个请求交给哪个节点处理。这个“调度者”有个响亮的名字——负载均衡服务器。集群结构的好处就是系统扩展非常容易。如果随着系统业务的发展,当前的系统又支撑不住了,那么给这个集群再增加节点就行了。但是,当你的业务发展到一定程度的时候,你会发现一个问题——无论怎么增加节点,似乎整个集群性能的提升效果并不明显了。这时候,你就需要使用微服务结构了。

1.3 分布式结构

从单机结构到集群结构,你的代码基本无需要作任何修改,你要做的仅仅是多部署几台服务器,每台服务器上运行相同的代码。但是,当你要从集群结构演进到微服务结构的时候,之前的那套代码就需要发生较大的改动了。所以对于新系统来说,系统设计之初就采用微服务架构,这样后期运维的成本会更低。分布式结构就是将一个完整的系统,按照业务功能,拆分成一个个独立的子系统,在分布式结构中,每个子系统就被称为“服务”。。在我们的直播系统开发过程中,按照微服务的思想,我们将功能模块拆分成多个独立的服务,如:用户服务、拉流服务、存储服务、编解码服务等等。这一个个服务都是一个个独立的项目,可以独立运行。如果服务之间有依赖关系,那么通过RPC方式调用。这样的好处有很多:系统之间的耦合度大大降低,可以独立开发、独立部署、独立测试,系统与系统之间的边界非常明确,排错也变得相当容易,开发效率大大提升。我们可以针对性地扩展某些服务。假设某一时刻直播量突然暴增,我们可以针对性地提升拉流系统、编解码系统的节点数量,而对于后台用户系统、存储系统而言,节点数量维持原有水平即可。同时,各个服务的复用性更高。当我们将用户系统作为单独的服务后,公司所有的产品都可以使用该系统作为用户系统,无需重复开发。

1.4 CAP定理

CAP定理指的是在一个分布式系统中, Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼而最多取其二。我们先来看看这三个特性的含义。

一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。

可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。

分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

通俗的讲,一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中。这就叫分区。
当一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容忍性就提高了。 然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。 总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。
CAP理论就是说在分布式系统中,我们最多只能实现CAP中的两点,如图4-13.

图4-13
值得一提的是,在zookeeper中采用的是一致性和可用性的平衡方案——最终一致性,后面我们会看到。

2 Zookeeper简介

要想管理好我们自己的分布式系统,就不能不提到大名鼎鼎的分布式协调框架Zookeeper。
ZooKeeper是一个开源的分布式协调服务,由雅虎创建,是Google Chubby的开源实现。分布式应用程序可以基于ZooKeeper实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。

2.1 为什么选择Zookeeper

大部分分布式系统都需要一个主控、协调器或控制器来管理物理分布的子进程(如资源、任务分配等)。如果每一个系统都开发私有的协调程序,这样会造成反复编写的浪费,且难以形成通用的、伸缩性好的协调器。ZooKeeper是Google的Chubby一个开源实现,是Hadoop的分布式协调服务。它高效、可靠地解决数据一致性问题,在工业界大型分布式系统中已经得到了广泛的使用和认可。同时,作为一个框架,zookeeper是轻量级的,无论是部署还是调用都非常简单易用。 让我们来看看Zookeeper的一些特性。

最终一致性

这是Zookeeper最重要的特性,最终一致性是C和A的折中方案。当客户端发起写请求,该请求会在Zookeeper集群中所有机器上执行,当有超过一半的机器执行完成,Zookeeper即向客户端返回写入成功。因此,通常我们在部署Zookeeper集群时一般部署奇数台。

顺序性

从同一个客户端发起的事务请求,最终会严格按照其发送顺序被应用到Zookeeper中。每个事务都会被分配一个事务ID,该ID由Zookeeper统一管理,全局唯一,且递增。每个事务按照ID顺序保存在事务队列中。

可靠性

一旦Zookeeper成功的应用一事务,并完成了客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下去。

实时性

因为采用最终一致性,Zookeeper不能保证客户端能得到刚更新的数据。如果需要最新数据,可以在读取数据之前调用sync()接口。

原子性

类似于数据库事务操作,一次数据更新要么成功,要么失败

单一视图

无论客户端连接到哪个服务器,看到的数据模型都是一致的。也就是,同一客户端无论什么时候连接到哪个服务器上,都不会看到比自己之前看到的数据更早版本的数据。单一视图保证是由ZooKeeper客户端与服务端建立连接请求时的一些校验操作实现的。还记得刚才说的事务ID吗,这个ID会保存在服务器和客户端中。当服务器发现客户端的事务ID高于自己时,会拒绝客户端连接。客户端会自动寻找其他的Zookeeper服务器连接。

2.2 Zookeeper架构及原理

先来看看Zookeeper的整体架构,如图4-14。
图4-14
下面我们依据架构图介绍一下Zookeeper的几个核心概念,这些概念有助于读者更深入的了解Zookeeper。

角色

如图4-14所示,Zookeeper中包含三种角色:Leader、Follower、Observer。 Leader主要用来更新系统状态,处理其他服务器发来的事务请求,并负责进行投票的发起和决议。 Follower用来处理客户端非事务请求并向客户端返回结果,如果是写事务请求则转发给Leader。Follower通过心跳同步Leader的状态,并在选主过程中参与投票。 Observer用来处理客户端非事务请求并向客户端返回结果,如果是写事务请求则转发给Leader。Observer通过心跳同步Leader状态,但不参与投票过程。显然,Observer的目的是为了扩展系统,提高读取速度。

Zookeeper读写

Zookeeper的写入流程如图4-15所示。


图4-15 在Zookeeper集群中,读可以从任意一个Zookeeper Server读。这一点是保证Zookeeper比较好的读性能的关键。我们重点说说写请求。
首先客户端可以向集群中任一Server提交写请求,如果该Server是Follower而不是Leader,则转发写请求。Leader收到请求后,会为该事务分配一个全局唯一的事务ID,将事务Id与事务请求绑定在一起,组成一个消息体,然后放入队列,发送给Follower。Follower接收到写请求后,先以日志的形式写在本地,然后返回一个确认消息给Leader。当Leader收到一半以上写成功的ACK后,就认为该写成功了,发起一个提交事务通知,通知Follower提交事务。 这就是Zookeeper数据写入最终一致性算法——ZAB算法。

数据模型Znode

Znode是Zookeeper中特有的数据结构,视图结构类似Linux文件系统,但没有目录和文件的概念。Znode是Zookeeper中数据的最小单元,可以保存数据,但不能太大。Znode通过挂载子节点构成一个树状的层次化命名空间,根由“/”开始。如图4-16。

图4-16

每个Znode的节点类型包括持久节点(PERSISTENT)、临时节点(EPHEMERAL)和顺序节点(SEQUENTIAL)。通常,每个节点都是由客户端去创建的。持久节点就是永久保存在Zookeeper上的节点,无论客户端离线或宕机都一直存在。而临时节点在客户端与Zookeeper失去联系的时候会被自动清除。值得注意的是,客户端与Zookeeper失去联系包括主动断开连接以及由于网络不稳定而掉线并在规定的超时时间内未重新连上任一Zookeeper Server。临时节点的这个特性非常有用,它可以帮助我们实现集群master选举及分布式锁等功能。顺序节点就是在创建节点时,在节点的名字后面加上一堆逐渐递增的数字,这个数字可以帮助我们识别客户端的访问顺序。
好,组合一下,我们一共可以生成四种不同的节点类型:持久节点、临时节点、持久顺序节点、临时顺序节点。

Znode状态

我们通过命令行get 路径的方式可以查看Znode的状态,如图4-17。

图4-17
图中已经标注了各个字段的含义。注意到其中有三个version,即cversion、dataVersion、aclVersion,这个version是表示对数据节点数据内容的变更次数,强调的是变更次数,因此就算在修改的时候数据内容的值没有发生变化,version的值也会递增。这里我们可以简单的先了解在数据库技术中,通常提到的“悲观锁”和“乐观锁”。
悲观锁具有严格的独占和排他特性,能偶有效的避免不同事务在同一数据并发更新而造成的数据一致性问题。实现原理就是:假设A事务正在对数据进行处理,那么在整个处理过程中,都会将数据处于锁定的状态,在这期间,其他事务将无法对这个数据进行更新操作,直到事务A完成对該数据的处理,释放对应的锁。一份数据只会分配一把钥匙,如数据库的表锁或者行锁(for update)。
乐观锁的具体实现是,表中有一个版本字段,第一次读的时候,获取到这个字段。处理完业务逻辑开始更新的时候,需要再次查看该字段的值是否和第一次的一样。如果一样那么执行更新操作,反之拒绝。
Zookeeper的版本作用就是类似于乐观锁机制,用于实现乐观锁机制的“写入校验”,在处理数据更新的时候会去检查版本,保证分布式数据的原子性操作。

Watcher机制

在zookeeper中,引入了watcher机制来通知客户端,服务端的节点信息发生了变化。其允许客户端向服务端注册一个watcher监听,当服务端的一些指定事件触发了这个watcher,就会向指定的客户端发送一个事件通知,如图4-18。

图4-18 watcher的工作机制主要包括三个步骤:客户端注册watcher、服务端处理watcher和客户端回调watcher事件。在具体的流程上,客户端向Zookeeper服务器注册Watcher事件监听的同时,会将Watcher对象存储在客户端WatchManager中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象执行回调逻辑。
Watcher通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。例如针对NodeDataChanged事件,ZooKeeper的Watcher只会通知客户指定数据节点的数据内容发生了变更,而对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据,这也是ZooKeeper的Watcher机制的一个非常重要的特性,保证网络传输的高效性。

3 Zookeeper应用场景

Zookeeper是一个高可用的分布式系统管理和协调框架,并且能够很好的保证分布式环境中数据的一致性。在越来越多的分布式系统(Hadoop、HBase、Kafka)中,Zookeeper都作为核心组件使用。下面我们简单介绍一下在日常开发中使用到Zookeeper的一些场景。

3.1 配置管理

我们的直播系统采用的微服务分布式架构,所有子服务都共享相同的的配置文件。这些配置文件的管理和同步是一个需要解决的问题。任一服务对配置文件修改后,它应该能够快速同步到所有服务上。
我们将各个配置信息分别写入Zookeeper的Znode上,各个服务节点起一个线程监听这些Znode,一旦Znode中的数据被修改,Zookeeper将负责通知各个服务节点,在回调方法中,各个服务去重新获取配置文件信息。这样借助Zookeeper的watcher机制就实现了配置文件的一致性。

3.2 集群管理

Master选举可以说是ZooKeeper最典型的应用场景了。比如HDFS中Active NameNode的选举、YARN中Active ResourceManager的选举和HBase中Active HMaster的选举等。
针对Master选举的需求,通常情况下,我们可以选择常见的关系型数据库中的主键特性来实现:希望成为Master的机器都向数据库中插入一条相同主键ID的记录,数据库会帮我们进行主键冲突检查,也就是说,只有一台机器能插入成功——那么,我们就认为向数据库中成功插入数据的客户端机器成为Master。
依靠关系型数据库的主键特性确实能够很好地保证在集群中选举出唯一的一个Master。但是,如果当前选举出的Master挂了,那么该如何处理?谁来告诉我Master挂了呢?显然,关系型数据库无法通知我们这个事件。
利用ZooKeepr的强一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即ZooKeeper将会保证客户端无法创建一个已经存在的ZNode。也就是说,如果同时有多个客户端请求创建同一个临时节点,那么最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很容易地在分布式环境中进行Master选举了。成功创建该节点的客户端所在的机器就成为了Master。同时,其他没有成功创建该节点的客户端,都会在该节点上注册一个子节点变更的Watcher,用于监控当前Master机器是否存活,一旦发现当前的Master挂了,那么其他客户端将会重新进行Master选举。这样利用临时节点和watcher机制就实现了Master的动态选举。

3.3 分布式锁

分布式锁主要用于在分布式环境中保护跨进程、跨主机、跨网络的共享资源实现互斥访问,以达到保证数据的一致性。假设锁空间的根节点为/lock。客户端连接zookeeper,并在/lock下创建临时顺序节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁。执行业务代码,完成业务流程后,删除对应的子节点释放锁。这样利用临时顺序节点和watcher机制就轻松实现了分布式锁。

results matching ""

    No results matching ""