说到zookeeper,我们不得不先说一下分布式系统的基本概念。
对于分布式,定义很多,而归根究底就是多个实现相同功能或组合起来实现相同功能的软件系统的集合,只要是这些应用部署在不同的物理主机上,达到共同协作完成任务或者一起完成相同的任务的系统架构都属于分布式的范围,分布式带来的好处主要是:
1.高可用(ha)
几年前一般的系统都是单机运行,即使现在也有很多公司对于一些不特别重要的系统也是采用这种单机的运行模式,这种模式在正常情况下是可以保证应用的可用性的,然而事情总不能像想象的那么美好,系统运行期间有一些问题是我们无法避免的,如:硬盘坏了(这种情况还算普遍吧),网络崩溃(对于支付宝这种公司的支付业务都出现过因为网络电缆损坏导致系统数据丢失、服务不可用),一些意外情况导致程序挂掉(即使cpu负载不高,其它硬件运行正常也不见得就是永不宕机)
解决这样的情况有效的办法之一是主从,对于主从大家现在应该并不陌生,对于mysql,redis这种成熟的开源系统是有成熟的主从实现的,而对于我们自己建立一些应用程序呢,难点不是我们如何把服务部署在多个服务器上,而是:
主从结构图:
zookeeper架构图:
(1)主节点失效
主节点失效时我们需要有一个备份主节点(backup master),当主节点(primary master)崩溃时,从节点接管主节点的角色,进行故障转义,然而,并不是从节点简单开始处理进入主节点的请求,新的主节点要能恢复到旧的主节点崩溃时的状态,对于主节点状态的可恢复性,我们不能依靠从已经崩溃的主节点来获取这些信息,而需要从其他地方获取,也就是通过zookeeper来获取,状态恢复不是唯一重要的问题,假如主节点有效,备份主节点却认为主节点已经崩溃,这种错误的假设可能发生在以下情况,例如主节点负载很高,导致消息延迟,其中一个从节点将会接管成为第二个主节点(master的选举),更糟糕的是只有一部分从节点停止与第一个主节点间的通信与第二个主节点建立主从关系,也就是脑裂(split-brain),系统中两个或者多个部分开始独立工作,导致整体行为不一致,我们需要找出一种方法处理主节点失效的情况和选举新的主节点的过程。
(2)从节点失效
客户端向主节点提交任务,之后主节点将任务派发到有效的从节点,从节点接收到派发的任务,执行完这些任务后悔向主节点报告执行状态,主节点下一步会将执行结果通知给客户端,这个过程中如果从节点崩溃了,所有已派发给这个从节点且尚未完成的任务需要重新派发,其中首要需求是让主节点具有检测从节点崩溃的能力,主节点必须能够检测到从节点的崩溃,并确定哪些从节点是否有效以便派发奔溃节点的任务,一个从节点崩溃时,从节点也许执行了部分任务,也许全部执行完了,但没有报告结果,我们还有必要执行某些恢复过程来清除之前的状态
2.采用集群的方式达到提高应用程序的吞吐量,同时也达到了高可用的目的
然而有时候我们要保证同一时间只有一个节点在运行(多节点并发执行可能会导致异常的情况),对于web集群软负载均衡的架构模式已经有很多成熟的实现方式,nginx,lvs等,当然zookeeper也能实现,但并不是强项。
ZooKeeper负载均衡的实现思路
把ZooKeeper作为一个服务的注册中心,在其中登记每个服务,每台服务器知道自己是属于哪个服务,在服务器启动时,自己向所属服务进行登记,这样,一个树形的服务结构就呈现出来了
client列表可以在配置中心做全局配置,改变配置后zookeeper会广播到需要接收配置变化的service上,动态更新client列表。
服务的调用者(service)到配置中心里面查找:能提供所需服务的服务器列表,然后自己根据负载均衡算法,从中选取一台服务器(client)进行连接
调用者取到服务器列表后,就可以缓存到自己内部,省得下次再取,当服务器列表发生变化,例如某台服务器宕机下线,或者新加了服务器,ZooKeeper会自动通知调用者重新获取服务器列表
由于ZooKeeper并没有内置负载均衡策略,需要调用者自己实现,这个方案只是利用了ZooKeeper的树形数据结构、watcher机制等特性,把ZooKeeper作为服务的注册和变更通知中心
从实现负载均衡的思路中可以看出zookeeper可以作为一个动态的配置中心存在,包括我们应用程序的一些动态的配置都可以注册到zookeeper中,达到平滑的更新配置信息的目的,不会影响当前服务的运行,阿里开源的dubbo其中就可以采用zookeeper作为注册中心。
3.分布式锁(Distribute Lock)
分布式锁,这个主要得益于ZooKeeper为我们保证了数据的强一致性,即用户只要完全相信每时每刻,zk集群中任意节点(一个zk server)上的相同znode的数据是一定是相同的。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把ZK上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。
控制时序,就是所有试图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock 已经预先存在,客户端在它下面创建临时有序节点。Zk的父节点(/distribute_lock)维持一份sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。
共享锁在同一个进程中很容易实现,但是在跨进程或者在不同 Server 之间就不好实现了。Zookeeper 却很容易实现这个功能,实现方式也是需要获得锁的 Server 创建一个 EPHEMERAL_SEQUENTIAL (自增的节点id)目录节点,然后调用 getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用 exists(String path, boolean watch) 方法并监控Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。