分布式系统的定义
《分布式系统原理和范型》一书中定义:分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像是单个相关系统。
从进程角度看,两个程序分别运行在两台主机的进程上,它们互相协作最终完成同一个服务或者功能,那么理论上这个两个程序所组成的系统,也可以称作是"分布式系统"。
分布式系统遇到的挑战
分布式系统问题的本质
分布式各系统之间都需要进行网络通信,所以本来在单一架构中能保证的数据一致性,升级为分布式系统后数据的一致性就难以保证,而Zookeeper的诞生就可以解决这个本质问题:数据一致性,再加上Zookeeper的其他特性还可以解决分布式锁,分布式定时任务等场景问题。
Apache Zookeeper是Apache软件基金会的一个软件项目,用于为分布式系统提供分布式配置、命名注册、分布式同步以及组服务。
下载地址
下载后解压缩,这里下载的事3.4.13版本,进入zookeeper根目录
conf
文件夹下是存放配置文件的地方,复制一份zoo_sample.cfg
,名称改为zoo.cfg
,这个文件就是zookeeper默认使用的配置文件。
下面看下zoo.cfg配置文件中的各配置项
tickTime
的两倍。initLimit
用于配置这个同步时间,默认是10倍的tickTime
。sycnLimit
指定的时间间隔后,还没有收到Follower发回的响应,则认为这个Follower已经不在线了。dataLogDir
,事务日志的写性能直接影响zk性能。服务端启动: bin/zkServer.sh start xxxx
,其中xxxx
代表配置文件路径(默认使用zookeeper/conf/zoo.cfg),其中start
表示启动,选项包括(start|start-foreground|stop|restart|status|upgrade|print-cmd)。
客户端连接ZK: bin/zkCli.sh -server ip:port
,ip:port
默认使用localhost:2181
help
查看可用的执行命令
ls /
列出根目录下的内容
创建节点:
create /zk_test my_data
在根目录下创建节点,节点名称 zk_test
,节点中保存的数据my_data
。可以使用flag指定创建的节点是临时的,持久的还是顺序的。不填写代表默认持久。
create -e /zk_ephemeral
,客户端断开连接时,临时节点会被清除。create -s /zk_seq
,zookeeper会向节点路径填充10为序列号,节点/zk_seq
将转换为/zk_seq0000000001
,下一个序列号是/xxxx0000000002
(xxxx代表输入的节点名称),顺序递增。获取数据:
get /zk_test
获取zk_test
节点中的数据
设置数据:
set /zk_test my_data2
更新/zk_test
节点数据为my_data2
删除数据:
delete /zk_test
删除zk_test
节点,有子节点则无法删除
递归删除数据:
rmr /zk_test
删除zk_test
节点,包含子节点
检查状态:
stat /zk_test
,获取指定znode的元数据,包含时间戳、版本号、ACL、数据长度和znode等细项
更多信息可参考ZooKeeper Programmer’s Guide
为了方便模拟集群,在单台机器上搭建一套包含4个实例的zookeeper集群(其中包含一个observer),生产环境中一般在不同的机器上安装实例,并且参加选举的ZK实例数最好是单数。
第一步:复制4个配置文件
端口号分别设为2181、2182、2183、2184
新增数据存放位置,zkjdata
文件夹下分别设为 data1、data2、data3、data4
其中zkjqdata
,是在zookeeper根目录下新建的文件夹,由于存放集群数据,可根据实际情况自行指定。
第二步:配置文件中增加集群配置
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
server.4=localhost:2890:3890:observer
其中localhost
后面的第一个端口用于同步,第二个端口用于选举Leader。
observer
标识该实例的角色是观察者,另外observer所在的节点配置中还需要增加peerType=observer
。
peerType=observer
这个选项更多是为了方便运维人员识别哪台机器是Observer,底层并不根据这个配置来判定谁是Observer角色,所以也可以不配
四个配置文件内容如下:
zoo1.cfg
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/zookeeper-3.4.13/zkjqdata/data1
clientPort=2181
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
server.4=localhost:2890:3890:observer
zoo2.cfg
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/zookeeper-3.4.13/zkjqdata/data2
clientPort=2182
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
server.4=localhost:2890:3890:observer
zoo3.cfg
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/zookeeper-3.4.13/zkjqdata/data3
clientPort=2183
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
server.4=localhost:2890:3890:observer
zoo4.cfg
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/zookeeper-3.4.13/zkjqdata/data4
clientPort=2184
peerType=observer
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
server.4=localhost:2890:3890:observer
第三步:新增myid
文件
在第一步创建的每个数据文件夹下新增myid
文件,文件的内容要和第二步中server.X
中".
后面的X
值对应
比如第一个实例的配置文件内容如下:
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/iflytek/zookeeper-3.4.13/zkjqdata/data1
clientPort=2181
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
server.4=localhost:2890:3890:observer
那么需要在data1文件夹下新增myid文件,内容为1。
第四步:指定配置文件启动实例
bin/zkServer.sh start /zookeeper-3.4.13/conf/zoo1.cfg
bin/zkServer.sh start /zookeeper-3.4.13/conf/zoo2.cfg
bin/zkServer.sh start /zookeeper-3.4.13/conf/zoo3.cfg
bin/zkServer.sh start /zookeeper-3.4.13/conf/zoo4.cfg
然后使用bin/zkCli -server localhost:2181
,bin/zkCli -server localhost:2182
,bin/zkCli -server localhost:2183
,bin/zkCli -server localhost:2184
,分别登录系统看实例是否能成功连上ZK Server。
测试:
1、在某个ZK Server下执行新建命令,看看其它ZK Server是否同步到数据。
2、停止某个ZK Server Leader,看下新选举出来的Leader是哪一个。
3、参与选举的ZK Server 停掉过半,整个集群会处于无法使用状态。
4、停掉observer所在Server,会发现对集群没有影响。
以上请自行测试,这里不再演示。
Zookeeper服务中的每个Server可服务于多个Client,并且Client可连接到ZK服务中的任一台Server来提交请求。若是读请求,则由每台Server的本地副本数据库直接响应。若是改变Server状态的写请求,需要通过一致性协议(Zab协议)来处理。
简单来说,Zab协议规定: 来自Client的所有写请求,都要转发给ZK服务集群中唯一的Leader,由Leader根据该请求发起一个Proposal,然后,其他的Server对该Proposal进行投票。之后,Leader对投票进行收集,当投票数量过半时Leader会向所有的Server发送一个通知消息。最后,当Client所连接的Server收到该消息时,会把该操作更新到内存中并对Client写请求做出回应。
Zookeeper服务器在上述协议中实际扮演了两个职能。一方面从客户端接收连接与操作请求,另一方面对操作结果进行投票。这两个职能在Zookeeper集群扩展的时候彼此制约。例如,当我们希望增加ZK服务中CLient数量的时候,那么我们需要增加Server的数量。然而,从Zab协议对写请求的处理过程中我们可以发现,增加服务器的数量,会增加对协议中投票过程的压力。因为Leader节点必须等待集群中过半Server响应投票,于是节点的增加拖慢整个投票过程的可能性随之增加,导致写操作随之下降。
为此我们引入了不参与投票的服务器,Observer可以接受客户端的连接,并将写请求转发给Leader节点。但是Leader节点不会要求Observer参加投票。相反,Observer不参与投票过程,只接受投票结果。但这种方式也存在一定问题,因为协议中的通知阶段,仍然与服务器的数量呈线性关系。但是这里的串行开销很低,我们可以认为这个阶段的开销不会成为主要瓶颈。
统一命名服务
在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务地址,提供者等信息。比如分布式服务框架中的服务地址列表,在Zookeeper中,能够很容易创建一个全局唯一的path,这个path就可以作为一个名称。
配置中心
分布式系统的配置项可以交给Zookeeper管理,将配置信息保存在ZK的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到ZK的通知,然后从ZK中获取新的配置信息应用到系统中。
集群管理和Master选举
利用Zookeeper的两个特性,可以实现对集群机器存活性的监控:
在分布式系统中,有些业务逻辑往往只需要整个集群中的某一台机器执行,其余机器可以共享这个结果,master选举便是这种场景下需要解决的问题。
分布式锁
分布式锁主要得益于Zookeeper为我们保证了数据的强一致性。锁服务可以分为两类:
保持独占
所有试图来获取这个锁的客户端,最终只有一个可以成功获取。通常的做法是把ZK上的一个znode看做是一把锁,通过create znode的方式来实现。所有客户端都去创建/distribute_lock
节点,最终成功创建的客户端也即拥有了这把锁。
控制时序
所有试图来获取这个锁的客户端,最终都会被安排执行,只是有个全局时序了。做法和保持独占基本类似,只是这里/distribute_lock
已经预先存在,客户端在它下面创建临时有序节点。父节点/distribute_lock
维持一份sequence,保证子节点创建的时序性,从而形成了每个客户端的全局时序。
Zookeeper满足CAP定理中的CP原则。如果Leader服务器挂掉,需要重新进行选举,在选举过程中,集群是不可用的,牺牲了可用性。