从零开始的高并发(三)--- Zookeeper集群的搭建和leader选举

3,545 阅读16分钟

前言

① 前情摘要

上一篇 我们提到了基于zookeeper下的分布式锁的简单实现,我们分别通过节点不可重名+watch机制(不推荐),取号 + 最小号取lock + watch的原理来各实现了一把分布式锁,第二种类似于去银行办理业务的先领号,等叫号的一种形式。

我们现在已经知道,zookeeper能够帮助我们解决的是分布式下应用进程间的协同问题,它简单,有序,可复制且快速(基础篇一 的2 - ④ 小节),核心有数据模型,会话机制和watch监听机制(基础篇一 的3 - ① ~ ④ 小节),zookeeper还有其对应的丰富的第三方客户端方便我们进行开发,在至此zookeeper入门的前两篇就告一段落了。

② 入门篇的链接:

从零开始的高并发(一)--- zookeeper的基础概念

从零开始的高并发(二)--- Zookeeper实现分布式锁

内容1:Zookeeper集群的简单搭建

原本应该和leader选举分P说的,但是因为集群搭建过程相对不复杂,主要的内容其实还是paxos算法的那块,所以我们就合成1P来说吧,内容看起来会较多,其实是截图比较多而已,希望大家能够耐心

① 为什么我们需要zookeeper集群

刚刚我们所提到的,zookeeper的可复制性就是zookeeper集群的特点,为了提供可靠的zookeeper服务,我们需要集群的支持,这个集群还有个特点,就是这个集群中只要有大多数的节点准备好了,就可以使用这项服务。容错集群设置至少需要3台服务器,强烈建议使用奇数个服务器(为啥推荐奇数看到后面就知道啦),然后建议每个服务都运行在单独的机器上。

② 集群搭建配置

这里我们演示的是伪集群的方式,不过一台机器做伪集群和几台机器分别做集群需要注意的细节肯定是更多的。可以说伪集群如果能配置妥当的话,到了真集群下配置就会更加简单明了。

1.需要了解的配置参数

需要注意的配置
    1.initLimit:
        集群中的小弟follower服务器,和领导leader服务器之间完成初始化同步连接时的能接受的最大心跳数,
        此时如果集群环境非常大,同步数据的时间较长,这个参数我们需要进行适当调整.
        
    注意,在zookeeper中,任何时间的设置都是以ticktime的倍数来进行定义,如果我们设置initLimit=2.那我们能接受的最大时间就是ticktime*2
        
    2.syncLimit:
        follower和leader之间请求和应答能接受的最大心跳数
    
    集群节点的配置
        server.id = host:port:port
        id:通过在各自的dataDir目录下创建一个myId的文件来为每台机器赋予一个服务器id,这个id我们一般用基数数字表示
        两个port:第一个follower用来连接到leader,第二个用来选举leader

2.zoo.cfg文件的修改

这里我们打开zookeeper根目录下的conf文件夹,找到zoo.cfg文件,


① dataDir和dataLogDir

zookeeper官方建议我们添加上DataLogDir来存放事务日志。如果只有dataDir目录而没有dataLogDir目录的话,它会把运行日志和事务日志都放在dataDir的那个目录上面去,事务日志和运行日志有什么区别?这个事务日志就相当于我们zookeeper的数据库,需要使用到读取和恢复等功能的时候,它就需要这么一个数据库来恢复。


② port注意事项

我们的zookeeper集群开启的话会有3个端口号,第一个端口号是客户端去连接它的端口号。clientPort是指我们的java客户端去连接它所使用的端口号,

第二个是小弟follower和leader去同步数据所用到的端口号, 第三个就是我们去进行leader选举时所用到的端口号。在末尾加上我们的节点配置,192.168.1.104使用的就是本机ip了,这里下方1001就是小弟和leader同步数据所用的端口号,那万一我们的leader崩溃,挂掉了,我们就需要重新去选举,此时我们就需要用到第三个端口,也就是2001端口号


③ 伪集群的搭建

此时我copy了两份zookeeper出来做伪集群


打开 zookeeper-3.5.2-alpha 02/conf/setting.xml,也就是第二个zookeeper的setting.xml


同理打开zookeeper-3.5.2-alpha 03/conf/setting.xml,也就是第三个zookeeper的setting.xml


④ 集群开启前的注意事项---myid

在开启3个集群之前,我们还需要去进行一个操作,看到我们的dataDir了吗,我们要去到这个指定目录下创建一个myid文件,前面也提到了myid是一个数字,我们就用1,2,3作为我们zookeeper01,02,03的id即可。

注意,myid是一行只包含机器id的文本,id在集群中必须是唯一的,其值应该在1~255之间,我们这里用的词为应该,而不是限制,如果真有一天超过了限制数集群超过了255,这个zookeeper集群就过大了,响应会非常慢。还有就是文件内不要有空格或者其他字符,只需要打上1,2,3···,即可。


这里会导致的报错可能有两个

因为zookeeper运行报错的话会闪退,我们可以找到zkServer.cmd,右键编辑,在末尾加上pause来查看报错信息

一个是myid没放对目录,需要放在我们的dataDir的路径下


二是myid里面除了数字还有其他的字符,这里我刻意在1前面敲了一个空格


这些错误都需要尽量去避免,不要因为自己操作太快没注意一些小的细节。

以我的zookeeper01为例,把myid文件放在data文件夹下即可,这个目录结构最好还是手动创建

不仅仅是启动集群,后面我们将要说到的选举和节点间的通信都是要使用到myid的


⑤ 集群开启时的报错---ConnectException

注意,在多节点未启动成功,比如仅仅只启动了一个节点时,我们启动可能会报错,比如以下这个Connection Exception:Connection refused:connect,这是因为我们和02,03之间未能建立通信导致的,所以此时我们只需要把其他的节点都正常开启即可


③ 连接我们的zookeeper集群

集群中的所有节点都可以提供服务,客户端连接时,连接串中可以指定多个或者全部集群节点的连接地址,当一个节点连接不通时,客户端将自动切换另一个节点。指定地址的时候我们使用英文输入下的逗号,来进行分割

1.zookeeper的集群监控---命令方式

这个需要在linux下来进行演示,因为电脑没环境所以就不演示了

2.zookeeper的集群监控---JMX

JMX(Java Management Extensions) --- Java管理扩展,是一个为应用程序,设备,系统等1植入管理功能的框架

在我们的jdk的bin目录下运行jconsole.exe

打开jconsole之后,我们看到了我们正在运行的几个服务,比如下图中我正在跑的服务有jconsole本身,idea的maven,还有3个我们正在开启的zookeeper服务

我们随便选择一个点开,选择使用不安全的连接即可,此时我们切换到Mbean视图

这里ReplicatedServer可复制的服务中,id1代表我们设置的myid的值为1

属性中我们看到一个数值QuorumSize代表的是集群中有多少台服务

此时它是作为小弟follower,我们也可以尝试打开另外的节点,比如我的第二个节点就是作为leader节点

工具大概有一个了解就差不多了,我们终于开始今天我们的主题,集群的leader选举

内容二:zookeeper集群的灵魂 --- leader

① 谁才是leader

每台服务器都可能成为leader,那到底leader是如何被选举出来的呢?

② 分布式一致性算法---paxos算法

1.paxos是什么---官方英文说明

这里尝试去翻译了一下

P1a:提议者选择一个提案编号n,给大多数接收者发送一个带有编号n的提案预请求

P1b:如果接收者收到了编号n的预请求,n大于前面已经响应过的提案预请求编号,这时接收者做出响应,
承诺不再接收编号比n小的提案预请求,并且在响应中会带上自己曾接收过的最高编号提案

P2a:如果提议者接收到了大多数接收者对于n的提案响应,这时提议者会给这些接收者的每一个服务发送接受请求,
此接受请求的内容为编号n的提案并带上一个value,这个value是如何取值的呢,
是取接收者响应中最高编号所对应的值,如果响应中根本不存在提议值,则可以任意取值

P2b:如果接收者接收到一个编号为n的接收请求,它接收该提案,但如果它将对大于编号n的预请求做出响应,则不接受n的提案

2.paxos的存在角色

ps:拜占庭将军问题不存在于zookeeper的集群中

3.paxos算法的流程简析(结合官方流程说明)

首先我们必须清楚paxos算法的两个约束

1.最终只有一个提议会被选择,只有被选择的提议值才会被learner去记录
2.最终会有一个提议生效,paxos协议能够让proposer发送的提议朝着能被大多数Acceptor接受的那个提议靠拢,
保证了最后结果的生成(少数服从多数原则)

我们现在开始进行流程分析


① 预请求第一阶段

提议者会发送的两种消息:prepare是预请求,accept是接收请求

提议者的提议请求构成一个(n,v)结构,n为序号,v为提议值

简单说明一下,这里提议者A向接收者C,D,E发送了自己的一个预请求prepare request(n=2,v=5),C,D正常接收,但是E断线,没有第一时间接收到

提议者B向C,D,E发送自己的预请求prepare request(n=4,v=8),此时E已经恢复通讯。


② 预请求第二阶段

这里我们如何理解?首先我们第一阶段中C,D都是正常接收到了提议者A的预请求的,此时它们在先前没有接收过任何的预请求(no previous),所以它们会对提议者A的请求作出应答(prepare response),这里对应的概念就是P2b:如果接收者接收到一个编号为n的接收请求,它接收该提案

所以此时它们对提议者A作出它们的应答,就是设置它们当前收到的提议就是(n=2,v=5),并向A作出承诺,不再接收序号小于2的提议请求,这里对应的就是P1b:如果接收者收到了编号n的预请求,n大于前面已经响应过的提案预请求编号,这里因为先前根本就没人请求过啊,所以前面响应过的预请求编号就是0,满足条件后,这时接收者C,D做出响应,承诺不再接收编号比n小的提案预请求,所以并且在响应中会带上自己曾接收过的最高编号提案,也就是它们接收到的(n=2,v=5)

此时接收者E是接收到了提议者B的提案,也是对应P1b和P2b,承诺不再接收比4小的编号提案,并把自己的曾接收的最高编号提案设置为(n=4,v=8)


③ 接受第一阶段

随后我们的C,D接收者也会接收到提议者B的提议请求,先前C,D的提案为(n=2,v=5),此时由于B提议(n=4,v=8)了,4>2,所以它们会给提议者B也发送一个提议响应,表示它们曾经提案过(n=2,v=8),之后它们自己进行修改,把提案修改为(n=4,v=5),并且再承诺不再接收n小于4的提案请求。这里v为什么值为5之后的阶段会解释

接收者E因为是先接收了提议者B的提案(n=4,v=8),所以之后再接收提议者A的提案(n=2,v=5)时,4>2,所以直接无视掉A的提案,也不对A作出回复


④ 接受第二阶段

此时proposerB收到了超过半数Acceptor所发的提案响应,便会对所有的接收者都发送一个接收请求

提议者A在预请求第二阶段收到两个提议响应之后会给C,D发送它的提案接受请求(n=2,v=5),可现在C,D明显已经不再是它的人了,所以C,D会无情抛弃它发送的接受请求

提议者B收到C,D的响应(n=2,v=5)的时候,会发送一个接收请求(n=4,v=5),为什么会是自己的编号4但是又取5作为提案值呢,因为这里的5是众望所归,是C,D它们响应回来的数字中最大的,请看下面我们接收第一阶段的图,要明白,决定最终提案值的并不是接收者接收到的你的提议值,而是作为提议者自身所接收到的最大编号对应值,请注意P2a中对于paxos算法的描述,取接收者响应中最高编号所对应的值

⑤ 学习者参与流程

此时learner才真正地参与进来,在经过接受第二阶段后,proposerB会把它得出来的(n=4,v=5)发送给所有的它能联系到的接收者,每个人都接收完成之后,此时就算A再发起投票,因为它能接收回来的响应中,肯定编号最大对应的v值为5,所以5就是这个算法的最终结果,无法再更改了。此时学习者就会去进行同步

③ zookeeper集群的leader选举要求

对选举leader的要求:

选出的leader节点上要持有最高zxid
过半数节点同意

内置实现的选举算法

LeaderElection
FastLeaderElection(默认的)
AuthFastLeaderElection

④ zookeeper集群leader选举机制的内容及概念

服务器id---myid
事务id---服务器中存放的最大zxid
逻辑时钟---发起的投票轮数计数
选举状态:
    LOOKING:竞选状态
    FOLLOWING:跟随状态,同步leader状态,参与投票
    OBSERVING:管擦状态,同步leader状态,不参与投票
    LEADING:领导者状态

⑤ zookeeper集群leader选举算法

选举算法的步骤

1.每个服务实例均发起选举自己为leader的投票
2.其他服务实例收到投票邀请时,比较发起者的数据事务id是否比自己最新的事务ID大,大则给它投一票,小则不投票,相等则比较发起者的服务器ID,大则投票给它
3.发起者收到大家的投票反馈后,看投票数(包括自己的票数)是否大于集群的半数,大于则成为leader,未超过半数且leader未选出,则再次发起投票

现在我们来讲一下,刚刚我们搭建好的集群是如何选举出节点2为leader的

首先我们节点一先被运行起来,因为没人和它联系,所以当第二个节点启动的时候,第二个节点也给自己发起一个投票,此时第一个节点把自己的信息广播出去给其他的节点,但是此时第三个节点并没有启动起来,所以它只能广播给第二台,然后第二个节点的广播也发送给第一个节点,此时它们各自都持有节点1,2的信息,此时它们会先比较事务id,这时,可能初期这两个节点的事务id都为0,那都为0的情况下是怎样的呢,就再比较服务器id,这时我们的节点1的myid为1,节点2的myid为2,所以节点1就会投票给节点2,此时节点2就是两票,节点1是自己的一票

此时我们的节点3的服务也起来了,此时它会接收到节点1和节点2的广播消息,它自己也会给节点1和节点2发送自己想要当leader的广播,可是,它会发现,此时节点2已经有了两票了,所以它就只能接受节点2为leader的结果。

⑥ 通过启动日志来简单分析

节点1的zkServer.cmd的启动日志

稍微往下到节点2启动的状态

最后渠道节点3启动的状态

节点2的zkServer.cmd的启动日志

节点3的zkServer.cmd的启动日志

finally

也是扯了不少东西,简单再回顾一下

集群搭建一定要注意myid的存放位置和编辑时候别手快带上了一些无法识别的字符,其余也没什么好说的

paxos算法比较晦涩难懂,也是不敢说自己有多深入的研究,如果讲的不妥当或者有另外想法的,大家可以给我留言,会进行改进

zookeeper的选举其实不算很难理解,如果要测试的话,只需要在搭建集群那里多加几个集群,再通过jconsole或者运行日志来查看就能测试不同的结果,在网络波动大的时候也会有其他的改变

下一篇:从零开始的高并发(四)--- Zookeeper的经典应用场景