首先说一下什么是分布式架构,早期使用所有服务部署在一台服务器的一个进程中,随着互联网发展,演变为分布式架构,就是多个服务分别部署在不同机器的不同进程中。
一致性协议:事务需要跨多个分布式节点时,为了保证事务的ACID特性,需要选举出一个协调者来协调分布式各个节。
zookeeper简介:核心简介、CAP原则、一致性协议(2PC、3PC、Paxos算法、ZAB协议)
zookeeper环境搭建:单机环境、zk集群搭建
zookeeper基本使用:zookeeper数据结构、zookeeper命令操作、zookeeper API操作
zookeeper源码解析:zk启动过程、watcher核心机制、leader选举
leader选举算法:
zookeeper应用场景:分布式配置中心、负载均衡个、统一命名服务、DNS服务、集群管理、分布式锁(可以基于数据库、可以基于Redis、也可以基于zookeeper)、分布式队列
zookeeper是一个开源的分布式协调服务,提供分布式数据一致性解决方案,分布式应用程序可以实现数据发布订阅、负载均衡个、命名服务、集群管理分布式锁、分布式队列等功能。
CAP原则,
一致性Consistency(一致性指的是强一致性)、
可用性Availability (系统提供的服务一直处于可用状态,用户的操作请求在指定的响应时间内响应请求,超出时间范围,认为系统不可用)、
分区容错性Partition tolerance(分布式系统在遇到任何网络分区故障的时候,仍需要能够保证对外提供一致性和可用性服务,除非是整个网络都发生故障),在一个分布式系统中不可能同时满足一致性、可用性、分区容错性,最多满足两个,对于分布式互联网应用而言,必须保证P,所以要么满足AP模型、要么满足CP模型。
一致性协议:
2PC二阶段提交,顾名思义,二阶段提交叫事务的提交过程分为两个阶段:
阶段一 提交事务请求,1、协调者向所有的参与者节点发送事务内容,询问是否可以执行事务操作,并等待其他参与者节点的
反馈;阶段二 事务提交,根据一阶段各个参与者节点反馈的ack,如果所有参与者节点反馈ack,则执行事务提交,否则中断事务
二阶段提交存在的问题:
同步阻塞
二阶段提交过程中,所有参与事务操作的节点处于同步阻塞状态,无法进行其他的操作
单点问题
一旦协调者出现单点故障,无法保证事务的一致性操作
脑裂导致数据不一致
如果分布式节点出现网络分区,某些参与者未收到commit提交命令。则出现部分参与者完成数据提交。未收到commit的命令的参与者则无法进行事务提交,整个分布式系统便出现了数据不一致性现象
3PC三阶段提交
3PC是2PC的改进版,实质是将2PC中提交事务请求拆分为两步,形成了CanCommit、PreCommit、doCommit三个阶段的事务一致性协议
Paxos算法
Paxos算法是Leslie Lamport 1990年提出的一种一致性算法,该算法是一种提高分布式系统容错性的一致性算法,解决了3PC中网络分区的问题,paxos算法可以在节点失效、网络分区、网络延迟等各种异常情况下保证所有节点都处于同一状态,同时paxos算法引入了“过半”理念,即少数服从多数原则。
在paxos算法中,有四种种角色,分别具有三种不同的行为,但多数情况,一个进程可能同时充当多种
角色。
client:系统外部角色,请求发起者,不参与决策
proposer:提案提议者
acceptor:提案的表决者,即是否accept该提案,只有超过半数以上的acceptor接受了提案,该
提案才被认为被“选定”
learners:提案的学习者,当提案被选定后,其同步执行提案,不参与决策
Paxos算法分为两个阶段:prepare阶段、accept阶段
ZAB协议(Fast Paxos)
由于paxos算法实现起来较难,存在活锁和全序问题(无法保证两次最终提交的顺序),所以
zookeeper并没有使用paxos作为一致性协议,而是使用了ZAB协议。
ZAB( zookeeper atomic broadcast ):是一种支持崩溃恢复的原子广播协议,基于Fast paxos实现
ZooKeeper使用单一主进程Leader用于处理客户端所有事务请求,,即写请求。当服务器数据发生变更好,集群采用ZAB原子广播协议,以事务提交proposal的形式广播到所有的副本进程,每一个事务分配一个全局的递增的事务编号xid。
若客户端提交的请求为读请求时,则接受请求的节点直接根据自己保存的数据响应。若是写请求,且当前节点不是leader,那么该节点就会将请求转发给leader,leader会以提案的方式广播此写请求,如果超过半数的节点同意写请求,则该写请求就会提交。leader会通知所有的订阅者同步数据
zookeeper的三种角色:
为了避免zk的单点问题,zk采用集群方式保证zk高可用
leader
leader负责处理集群的写请求,并发起投票,只有超过半数的节点同意后才会提交该写请求
follower
处理读请求,响应结果。转发写请求到leader,在选举leader过程中参与投票
observer
observer可以理解为没有投票权的follower,主要职责是协助follower处理读请求。那么当整个zk集群读请求负载很高时,为什么不增加follower节点呢?原因是增加follower节点会让leader在提出写请求提案时,需要半数以上的follower投票节点同意,这样会增加leader和follower的通信通信压力,降低写操作效率。
zookeeper两种模式
恢复模式
当服务启动或领导崩溃后,zk进入恢复状态,选举leader,leader选出后,将完成leader和其他机器的数据同步,当大多数server完成和leader的同步后,恢复模式结束
广播模式
一旦Leader已经和多数的Follower进行了状态同步后,进入广播模式。进入广播模式后,如果有新加入的服务器,会自动从leader中同步数据。leader在接收客户端请求后,会生成事务提案广播给其他机器,有超过半数以上的follower同意该提议后,再提交事务。注意在ZAB的事务的二阶段提交中,移除了事务中断的逻辑,follower要么ack,要么放弃,leader无需等待所有的follower的ack
leader选举算法:
启动过程
每一个server发出一个投票给集群中其他节点
收到各个服务器的投票后,判断该投票有效性,比如是否是本轮投票,是否是 looking状态
处理投票,pk别人的投票和自己的投票 比较规则xid>myid “取大原则”
统计是否超过半数的接受相同的选票
确认leader,改变服务器状态
添加新server,leader已经选举出来,只能以follower身份加入集群中
崩溃恢复过程
leader挂掉后,集群中其他follower会将状态从FOLLOWING变为LOOKING,重新进入leader选举
同上启动过程
消息广播算法:
一旦进入广播模式,集群中非leader节点接受到事务请求,首先会将事务请求转发给服务器,leader服务器为其生成对应的事务提案proposal,并发送给集群中其他节点,如果过半则事务提交;
leader接受到消息后,消息通过全局唯一的64位自增事务id,zxid标识
leader发送给follower的提案是有序的,leader会创建一个FIFO队列,将提案顺序写入队列中发送给follower
follower接受到提案后,会比较提案zxid和本地事务日志最大的zxid,若提案zxid比本地事务id大,将提案记录到本地日志中,反馈ack给leader,否则拒绝
leader接收到过半ack后,leader向所有的follower发送commit,通知每个follower执行本地事务
zookeeper应用场景:配置中心、负载均衡、命名服务、DNS服务、集群管理、分布式锁、分布式队列
分布式配置中心,我们件高配置信息存在zk中的一个节点中,同时给该节点注册一个数据节点变更的watcher监听,一旦节点数据发生变更,所有的订阅该节点的客户端可以获取数据变更通知。
负载均衡,一般用nginx做负载均衡,是没有问题的,在原有的架子上,但是当我们新增一台tomcat机器的时候,多出来的一台服务,在 nginx反向代理的服务器列表中没有新增的这台tomcat,所以无法新增进去,需要更新负载均衡的算法。如果是zookeeper做反向代理,是怎么解决的,会建立一个监听器,对子节点进行监听,如果多了一台机器,会及时更新服务器列表,然后就会及时更新负载均衡算法,当请求过来后,会按照新的负载均衡算法,把请求发到某一台机器上。
命名服务,dubbo,客户端根据指定的唯一标识,获取到指定信息。上层仅仅需要一个全局唯一名称,就像数据库中的主键。
DNS服务,域名配置,当需要频繁变更域名时,需要在规模的集群中变更,无法保证实时性。域名解析,应用解析时,首先从zk域名节点中获取域名映射的ip和端口。域名变更,每个应用都会在对应的域名节点注册一个数据变更的watcher监听,一旦监听的域名节点数据变更,zk会像所有订阅的客户端发送域名变更通知,从而保证实时性。
集群管理,zookeeper集群管理是通过watcher机制和创建临时节点来实现,以机器上下线和机器监控为例
机器上下线,新增机器的时候,将Agent部署启动时,会向zookeeper指定的节点下创建一个临时子节点,当子节点创建完成后,machines就可以接收到“子节点变更事件”,即上线通知,就可以对这个新加入的机器开启相应的后台管理逻辑。另一方面,监控中心同样也可以获取到机器的运行状态信息,这样就实现了对机器上线/下线的检测。
机器监控,Agent会定时将主机的运行状态信息写入到machines主机节点,监控中心通过订阅这些节点的数据变化来获取主机的运行信息。
分布式锁,在分布式的情况下,使用加锁lock对象,是不能满足线程安全的,因为是不同的机器都有lock对象,而这个锁对象是不同的,所以无法保证线程安全。所以这时候使用分布式锁。
数据库实现分布式锁
需要创建一张锁表,创建一个字段是 唯一key,然后添加进来值后,别的线程再往里添加前,先查询该字段是否有值,如果有值说明有线程在操作这个字段,需要等待,如果没有值,则可以添加这个字段的值。
Redis实现分布式锁
Redis分布式锁基于setnx(set if not exists),设置成功,返回1;设置失败,返回0,释放锁的操作通过del指令来完成。如果设置锁后在执行过程时,程序抛出异常,导致del指令没有调用,锁永远无法释放,这样就会陷入死锁。所以我们拿到锁之后会给锁加上一个过期时间,这样即使中间出现异常,过期时间到后会自动释放锁。
同时在setnx和expire如果进程挂掉,expire不能执行也会死锁,所以要保证setnx和expire是一个原子性操作即可。
redis实现分布式锁注意的事项
redis如何避免死锁,使用expire 给锁加一个释放时间
lock获取锁方法
redis实现分布式锁存在一个问题,为了解决redis单点问题,我们会部署redis集群,在Sentinel集群中,主节点突然挂掉了,同时主节点中有锁还没有来得及同步到从节点,这样会导致系统中同样一把锁被两个客户端同时持有,因此导致不安全,redis官方为了解决这个问题,推出Redlock算法,但是带来的网络消耗较大。
分布式Redisson实现:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.3</version>
</dependency>
获取锁释放锁
Config config = new Config();
config.userSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
Redisson redisson = (Redisson)Redisson.create(config);
RLock mylock = redisson.getLock(key);
zookeeper实现分布式锁
原理:有序临时节点+watch监听 来实现
如果是最小的,就获取锁,并且执行完成之后,这个节点会被删掉。
如果不是最小的,它会监听它前面的一个节点,当它前一个节点被删除的时候,它就会获得锁,依次类推。
代码实现:
zookeeper提供了分布式数据一致性解决方案。
zookeeper分布式队列
队列特征:FIFO(先入先出),zookeeper实现分布式队列的步骤:
跟分布式锁的原理是一样的,在队列节点下创建临时顺序节点,例如queue_info
调用getChildren()接口来获取queue_info 节点下所有子节点,获取队列中所有元素
比较自己节点是否是序号最小的节点,如果不是,则等待其他节点出队列,在序号最小的节点注册watcher
获取watcher通知后,重复步骤