背景
随着计算机的硬件和操作系统两者相辅相成地发展,从早期的ENIAC计算机到现在的x86的计算机,从以前的单一控制终端(Single Operator, Single Console, SOSC)的操作系统到现在百花争鸣的操作系统(如MacOS、Windows、Linux等),现代的操作系统发展还有一个最重要的特征是网络的出现,网络促进了网络操作性系统和分布式操作系统的出现。对于网络操作系统来说,其任务是将多个计算机虚拟成一个计算机。传统的网络操作系统是在现有操作系统的基础上增加网络功能,而分布式操作系统则是从一开始就把对多计算机的支持考虑进来,是重新设计的操作系统,所以比网络操作系统效率高。分布式操作系统除了提供传统操作系统的功能外,还提供多计算机协作的功能。
根据上述的发展,我们在其基础上构建的应用也分为集中式系统和分布式系统。集中式系统具有明显的单点问题。大型主机虽然在性能和稳定性方面表现卓越,但这不代表它永远不会出问题。另外随着业务的不断发展,用户访问迅速提高,计算机系统的规模也在不断扩大,在单一大型主机上进行的系统的扩容往往比较困难。
按照正常的套路来说,下一步应该是说随着PC的性能不断提升和网络技术的快速普及,所以很多企业就开始去掉大型机,从而改用普通PC来作为分布式的计算机系统。这样说就太平淡了,企业也不是这么容易说变就变,就算谷歌开始的时候敢用可靠性较低的服务器的一个重要原因,在于它是最初并不为用户提供存储和计算服务,服务器都是自己内部使用。如果计算到一半死机了,那就再从死机的断点重新启动计算。甚至如果一开始有些中间数据丢失了,那就再产生一遍。这时它的可靠性不像银行那样重要。2004年当谷歌开始提供Gmail服务,为了确保用户的数据不丢失,它采用了3x3九台服务器存一组数据,这个成本就不低了。同时期,雅虎等公司采用两台可靠性的服务器存储邮件。这时期,谷歌的Gmail是非常赔钱的。后来谷歌的云存储部门用软件实现了5台服务器分布式存储,达到过去九台服务器的可靠性,Gmail的成本才降下来。谷歌想方设法用廉价服务器集群取代超级计算机的过程远比很多人想象的复杂。这种想法其实早在谷歌之前就有了,但是其它公司就是因为无法解决其中的很多技术细节问题,使得采用大量低可靠性的服务器带来的好处还没有它带来的麻烦多,最终都放弃了。(这里引申出谷歌的文化之一)谷歌把事做到了极致,克服了各种技术困难,最后做成了。
企业吸收已证实可行的经验是很快速,于是后来亚马逊等公司也实现了廉价服务器集群取代超级计算机的功能。其中在国内,最为典型的就是阿里巴巴的“去IOE”活动,
也正是在这个大背景之下,还有在大数据和云计算驱动之下,ZooKeeper在雅虎里诞生了。用于解决很多分布式系统下的问题。当设计一个使用ZooKeeper的应用时,最好就是将应用数据和协调数据给分开。如邮件系统,用户只关心他们的邮箱内容,而不需要知道哪台邮箱服务器在处理。因此在这里邮箱内容就是应用数据,而协调数据则是具体是哪台邮件服务器在处理。
分布式的问题
由于分布式系统概念有很多种,在这里我们定义为:一个系统由多个组件组成,而每个组件独立并同时运行在不同的物理机器上。
分布式系统一诞生就面临诸多的难题和挑战。典型的问题有这些:
- 通信失败(Communication Failure)
由于分布系统引入了网络因素,而网络本身是不稳定的。因此分布式系统中各个结点之前进网络通信时,会出现消息丢失和消息延迟等现象。 - 网络分区(Network Partion)
网络分区,也称脑裂(split-brain),集群中部分节点之间不可达而引起的(或者因为节点请求压力较大,导致其他节点与该节点的心跳检测不可用)。当上述情况发生时,不同分裂的小集群会自主的选择出master节点,造成原本的集群会同时存在多个master节点。 - 三态
因为通信失败的问题,会带来其中一个问题是三态,三态即成功、失败与超时。分布式系统的每一次请求与响应都会有这三种结果。最麻烦的是超时,因为它存在这两种可能:
1.由于通讯失败,该请求(消息)并没有被成功地发送到接收方,而是在发送过程中就丢失了。
2.该请求(消息)成功地被接收方接收后,并进行了处理,但是在将响应反馈给发送方时,发生了消息丢失现象。 - 节点故障
这也是属于通信失败的情况,但着重点是说,机器自身挂了,无法发出消息。有可能是宕机或负荷严重的情况导致的。
上述分布式问题导致了一致性问题难以解决,而且在2002年的时候,MIT的Seth Gibert和Nancy Lynch证明的CAP定理。这个定理是:系统不可能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)。
既然是逻辑证明出来的,那它就一定对的。这也是分布式系统的边界或者说是上限。所以人们开始在一致性和可用性上权衡,从而诞生很多算法如2PC、3PC和Paxos算法,这些都影响着ZooKeeper的设计。
ZooKeeper不能解决所有分布式系统的问题。但它提供了一个很好框架去处理这些问题。
ZooKeeper为分布式系统提供了协调功能和控制冲突。协调意思是说几个进程需要一起做一些事情,例如,在Master-Worker分布式架构里,worker通知master它可用,master因此分派任务给它。
而控制冲突则不一样:它更多是这种情形,两个进程是不能同时进行,一个必须等待一个才能进行。例如,在Master-Worker分布式架构里,我们希望只有一个进程变成master,所以当多个进程同时激活自己为master时,必须实现互斥( mutual exclusion),只能有一个进程获得锁,并成为master角色。
ZooKeeper的API提供了:
- 强一致性,顺序和持久化的保证
- 提供实现同步的能力
- 一个更简单的方法处理分布式系统中的并发问题。
ZooKeeper的基础
我们将围绕一个例子去讲述概念并实践。这个例子是:Master-Worker分布式架构。
架构图如下:
Master进程的职责时跟踪Worker和任务,并将任务分派给Worker。为了实现这个Master-Worker系统,我们必须解决这三个关键问题:
Master故障
如果master故障并且变得不可用,系统就不能分配新任务或重新分配失败的任务。
Woker故障
如果worker故障,则分配给它的任务则完成不了。
通信失败
如果master和worker不能交换信息,则worker可能无法获取分派给它的任务。
为了解决上述问题,这个系统必须有以下功能:能在一个master挂掉之后,重新选择一个新的master;判断那些woker是可用的;当worker与master因为网络分区失去与master连接时能够重新分派任务;当一个节点获得锁之后发生网络分区或挂掉时,需让这个锁失效。
重新分配任务有以下情况:如果任务可重复执行,则可以无需任何校验的把这任务重新分派。但如果这个任务是不能重复执行的,则需要协调多个worker执行任务的情况。
znode
ZooKeeper并没有直接提供上述的功能,而是提供一个跟文件系统很像的API。这个被组织称树结构,每个结点都存储很小的数据的结点(不超过1M),被称为znode。如下图,根结点包含4个结点,其中3个又是分支结点,拥有叶子节点。而叶子结点就包含数据。
不存在的结点在znode里也是蕴含信息。如在这个Master-Worker例子里,如果master结点不存在,则表明master还没选出来。另外根据上图,我们可以看出/workers结点是当前系统中所有可用的woker的父节点。foo.com:2181是worker的信息。如果woker不可用了,就应该把对于的结点从/worers上删除。
而/tasks结点是父结点,其子结点是已经被创建的任务,且等待被执行。Master-Worker例子里,客户端就可以在/tasks下创建一个结点来代表一个新任务,并且等待该任务的状态。
最后/assign结点的子结点是所有已经被分派给worker的任务。
znode可以有和可以没有数据。如果有数据,数据类型必须是字节数据(Byte Array)。字节数组的含义解释就需要各自应用决定,ZooKeeper没有提供解析这字节数组的功能。
ZooKeeper的命令行提供了一下API:
create /path data
创建一个znode结点名为/path,包含数据data
delete /path
删除znode结点/path
exists /path
检查是否有/path结点
setData /path data
设置/path结点的数据
getData /path
获取/path结点数据
getChildren /path
获取/path结点的子结点列表
znode的模式
在创建znode结点时,我们可以指定模式(mode),不同模式决定znode结点的不同行为:
永久(Persistent)和临时(Ephemeral)znode结点
znode结点只能是永久结点或者是临时结点。永久结点/path只能被delete命令删除。而临时结点,在创建该结点的客户端故障了或失去与ZooKeeper失去连接时,这个结点会被删除。
在Master-Worker例子里,我们需要维护任务的分派情况,哪怕master故障了。
临时znode结点传递着这样的信息:结点的创建者的session有效,则结点的应用才能存在。例如,master的结点在Master-Worker例子里就是临时的。master故障时,master结点也不应该存在。同样的也适合worker的情况。
因为临时znode结点在它的创建者的session超时失效时被删除,则我们不允许临时结点拥有子结点。
序列znode结点(Sequantial Znodes)
一个znode可以被设置为序列(sequential)。一个序列结点时唯一的,单调递增的整数。是在path后面追加序列数据。例如,如果一个客户端创建一个序列znode结点/task/task-,ZooKeeper会分配一个序列,如1,追加到路径上,则为/task/task-1。序列znode结点提供一个简便地方法去创建拥有唯一名字的znode结点。也可以被用来查看创建znode结点的顺序。
总结
因此总的来说,znode有以下四个模式:persistent, ephemeral, persistent_sequential和ephemeral_sequential。
Watch与通知(Watches and Notifications)
由于是远程访问ZooKeeper,所以访问znode结点是非常昂贵的:高延迟或多无用的操作。考虑以下情况,如果下图,第二次使用getChildren /task返回的是同样的值,因此时没有必要的。
这是轮训(polling)的普遍出现的问题。我们使用一种叫通知(notifications)的机制来代理客户端的轮训:客户端在ZooKeeper上注册接收znode结点变化的通知。指定一个znode结点,接受其一个通知的注册,这过程叫 设置watch。一个watch是单步操作(one-shot operation),也就是说一个watch仅仅触发一个通知。如果想接受多个通知,则需要在接受到通知后,重新设置watch。设置watch和接受通知的过程如下图:
版本(Version)
每一个znode结点都会有一个版本号,这个版本号在每次结点的数据发生改变都会递增。这样ZooKeeper的一些API操作就可以带上条件,这操作如setData和Delete。设置数据时可以带上版本号,版本号匹配不上则操作失败。操作过程如下图:
ZooKeeper的架构
客户端可以通过client库来与ZooKeeper节点进行通信。
ZooKeeper服务可以有两种模式:单机(standalone)和法定人数(quorum)。单机模式也就是单个服务器,ZooKeeper的状态不会被复制(replicate)。而在法定人数模式下,则会有一组ZooKeeper服务器,也称为ZooKeeper套装(ZooKeeper ensemble),节点之间会复制ZooKeeper的状态,并一起提供服务。
ZooKeeper的法定人数
在法定人数模式下,ZooKeeper会冗余(replicate)它的数据到各个服务器里。当如果客户端必须等待每个服务器都保持好它的数据,才能往下进行操作,则延迟将会难以接受。在现实世界的公共事务管理里,法定人数是要求出席投票的最低人数。而在ZooKeeper里,法定人数是保证ZooKeeper能够正常工作,可用,的最少服务器数量。这个最少数量也是保证至少有这么多台服务器是同步了数据的。这样才能保证数据是被安全保存。如我们用5台ZooKeeper服务器,则法定人数则为3台。只要有3台服务器保存了数据,客户端则可以往下继续自己的事情,另外两天服务器最终会同步刚刚保存的数据。
会话(Session)
客户端在对ZooKeeper发送任何请求之前,都得先与ZooKeeper建立会话(session)。会话这个概念在ZooKeeper里是非常重要且关键的。所有操作都必须关联着会话。当一个会话因为某些原因结束时,通过这会话创建的临时结点也将会伴随这会话的结束而被删除。
客户端是通过TCP连接来与服务器通信,客户端只能连接一个服务器。如果客户端连接的一台服务器挂了,则session将会移动到下一台服务器,这个移动过程是透明的,由ZooKeeper负责处理的。
会话提供了有序保证(order guarantees),意思是一个会话里的操作都是FIFO的顺序。但跨session操作时,哪怕session不是重叠,而是连续的不同的session,这个FIFO的顺序也会被破坏,如:
- 客户端建立一个session,并异步的连续的发送两个操作,create /tasks和/workers.
- 第一个session失效
- 客户端建立其他session,也发送一个异步请求,create /assign
在这种情况下,是有可能仅仅只有/tasks和/assign被创建,/workers而没有被创建。这是因为被交错的会话给打断了。
ZooKeeper的命令行体验
通过命令行来操作ZooKeeper来实现Master-Worker这个例子。
安装ZooKeeper(quorum模式)
1.下载安装包zookeeper-3.4.5.tar.gz
2.解压zookeeper-3.4.5.tar.gz到/usr/local
tar -zxvf zookeeper-3.4.5.tar.gz
3.创建zoo.cfg配置文件
mv conf/zoo_sample.cfg conf/zoo.cfg
4.修改zoo.cfg配置文件,修改如下
#防止把根分区给被占满
dataDir=/usr/local/zookeeper-3.4.5/tmp
clientPort=2181
server.1=zk1.jc.com:2888:3888
server.2=zk2.jc.com:2888:3888
server.3=zk3.jc.com:2888:3888
其中2181是ZooKeeper服务器的开放给客户端访问的端口,而2888和3888是ZooKeeper节点之间用于通信和leader选举。
5.在各个结点的/usr/local/zookeeper-3.4.5/tmp目录下建立myid文件,内容为server的id,如给zk1.jc.com这个结点添加myid
echo "1">/usr/local/zookeeper-3.4.5/tmp/myid
6.在每个结点启动ZooKeeper服务
./bin/zkServer.sh start
7.查看每个结点的状态,就可以看到leader和follower了
./bin/zkServer.sh status
实现Master-Worker这个例子
我们通过zkCli.sh工具来实现Master-Worker这个例子的功能。
Master-Worker这个例子涉及到三个角色:
Master
这个master负责监控新woker和任务,并分派任务给可用的worker。
Worker
worker将自己注册到该系统中,并保证master能够看到它是可用的,可执行任务,然后监听新任务。
客户端
客户端创建新任务并等待系统的响应
Master角色
1.在窗口A连接ZooKeeper
./bin/zkCli.sh -server zk1.jc:2181,zk2.jc:2181,zk3.jc:2181
2.在窗口A创建临时znode结点/master
create -e /master "master1.jc.com:2223"
3.在窗口B连接ZooKeeper
./bin/zkCli.sh -server zk1.jc:2181,zk2.jc:2181,zk3.jc:2181
4.在窗口B创建临时znode结点/master,模拟两个进程激活master角色。这个会失败
#会返回Node already exists: /master
create -e /master "master2.jc.com:2223"
5.在窗口B,既然/master已存在,则需监听/master,以便当窗口A的master故障时,能快速恢复过来。stat命令是获取znode属性,然后后续参数true是设置一个watch在/master上
stat /master true
6.当窗口A退出时,窗口B会收到一下通知:
WatchedEvent state:SyncConnected type:NodeDeleted path:/master
7.此时窗口B就可以激活自己称为master
create -e /master "master2.jc.com:2223"
Worker角色、任务和任务分派
在继续客户端和Worker操作之前,我们需要创建三个父节点,/workers, /tasks和/assign
create /workers ""
create /tasks ""
create /assign ""
ls /
另外Master角色需监听worker和任务
ls /workers true
ls /tasks true
Worker角色
1.在窗口C连接ZooKeeper
./bin/zkCli.sh -server zk1.jc:2181,zk2.jc:2181,zk3.jc:2181
2.创建一个worker结点
create -e /workers/worker1.jc.com "worker1.jc.com:2224"
3.此时Master角色的窗口会受到通知
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/workers
4.worker需创建一个父节点来接受任务分派,并watch
create /assign/worker1.jc.com ""
ls /assign/worker1.jc.com true
客户端角色
1.在窗口D连接ZooKeeper
./bin/zkCli.sh -server zk1.jc:2181,zk2.jc:2181,zk3.jc:2181
2.提交任务,在这里是创建一个序列znode结点。
create -s /tasks/task- "cmd"
3.设置一个watch,等待任务完成
ls /task/task-0000000000 true
4.当任务创建是,Master的窗口将会收到通知
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/tasks
5.此时Master窗口查看任务,查看可用worker并分派任务
ls /tasks
ls /workers
create /assign/worker1.jc.com/task-0000000000 ""
6.此时Worker窗口C收到通知
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged
path:/assign/worker1.example.com
7.Worker窗口C,查看任务,并执行完任务后修改任务状态
ls /assign/worker1.jc.com
create /tasks/task-0000000000/status "done"
8.客户端收到通知,并查看任务完成结果
#通知
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged
path:/tasks/task-0000000000
#查看结果
get /tasks/task-0000000000
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。