从传统Java Web转入分布式系统应用,再到接触分布式协调框架ZooKeeper,通过痛苦的思维逻辑和理念转变,历经一个月时间,小伙伴们终于把ZooKeeper嵌入到了BoCloud博云的BeyondContainer中,并在其上进行相应功能的开发:服务注册与发现、集群管理、模块的高可用及分布式锁等。
在选定ZooKeeper之前,我们对其他的分布式框架也进行了调研和对比,分别 有etcd和consul。对于etcd而言,在原生接口和提供服务方式方面,etcd更适合作为集群配置服务器,用来存储集群中的大量数据,方便的REST接口也可以让集群中的任意一个节点在使用key
|value服务时获取方便,然而etcd在监控服务的状态和通知方面比较麻烦;consul方面,是使用Go语言开发的分布式协调,对业务发现的管理提供很好的支持,他的HTTP
API也能很好的和不同的语言绑定,但在业务检测方面有一定的延时,不太适合实时响应的情景;并且etcd和consul需要其他组件的配合才能达到ZooKeeper的服务能力,故ZooKeeper则更加的适合于提供分布式协调服务,实现分布式锁模型方面也较为简单方便,并且功能全,社区活跃,用户群体很大,对所有典型的用例都有很好的封装,支持不同语言的绑定,故最终选定ZooKeeper。
在产品开发中使用ZooKeeper是一件有趣的事情,下面就让我们来详细扒一扒我们的开发体验吧。
一、简单介绍
先来简单说明下ZooKeeper原理,再来谈谈在产品中的具体使用。ZooKeeper是一个开放源码的分布式应用程序协调服务,由知名互联网公司雅虎创建,是Google
Chubby的开源实现。设计目标是为分布式提供一致性服务,其没有直接采用Paxos算法,而是采用了被称为ZAB的一致性协议。ZooKeeper具有以下几个特征:
1、数据模型
ZooKeeper使用一个共享的、树形结构的名字空间-数据模型,其由一系列被称为ZNode的的数据节点组成,其层级关系,如文件系统的目录结构一样,结构如下图所示:
每个Znode上都会保存自己的数据内容以及属性信息,节点分为持久节点和临时节点,持久节点除非进行删除操作,否则将会一直保存在ZooKeeper上;而临时节点,他的生命周期和客户端会话绑定,一旦会话失效,这个客户端所创建的所有临时节点都会被删除。另外,可以给节点加上一个属性:sequential,节点创建的时候,会自动在节点后面追加一个由父节点维护的自增数字。
2、构建集群
一个ZooKeeper集群通常由一组机器组成,一般3-5台机器就可以组成一个可用的集群,其结构和工作原理如下图所示:
只要集群中存在超过一半的机器能够正常工作,集群就能够正常对外服务。
ZooKeeper具有以下三种角色:
Leader: 为客户端提供读写服务
Follower:为客户端提供读服务,参与Leader选举过程
Observer:为客户端提供读服务,不参与Leader选举过程
3、顺序访问
对于客户端的每个更新请求,ZooKeeper都会分配一个全局唯一的递增编号,反映了所有事务操作的先后顺序,通过这个特点来实现更高层次的同步原语。
4、高性能
由于ZooKeeper将全局数据存储在内存中,并直接服务于客户端的所有非事物请求,因此他特别适用于以读写操作为主的应用场景。
二、核心处理
在学习、调研和实际产品使用的过程中,总结了两种ZooKeeper的处理方式,以应对各种使用场景的实现。分别为:阻塞式和自杀式。
阻塞式:
所有实例会向ZooKeeper的指定路径下把实例ip:port注册上去,注意,这里注册的节点类型是一个临时顺序节点。完成注册后,每个实例都可以获取到所有的节点列表,再通过对比判断自己是否是所有节点中序号最小的,如果是最小的则作为Leader,来执行定时任务或者数据库同步。当Leader机器发生故障,会再次按“小序号优先”策略选出Leader继续执行任务。具体的做法是,所有实例在指定路径下注册临时顺序子节点,并得到子节点列表,通过排序判断自己是否是最小子节点,如果是即为Leader,否则对前一个比自己序号小的节点,注册一个“节点存在”的watcher监听,来监控节点存在情况,一旦客户端断开连接或者会话失效,对应节点会被自动删除,对它进行监控的实例会受到事件通知,以此类推,从而选出最小节点,即Leader。其流程图如下:
自杀式:
与等待式最大的区别在于,当实例发现自己不是最小节点时,会自主删除。具体的做法是,注册“最小节点存在”的watcher监听,来监控最小节点存在情况,一旦Leader机器与ZooKeeper断开连接,会话失效,对应的节点会被自动删除,则其他实例会接收到变更通知,再向指定路径下注册节点,并判断自己是否是最小节点,如果是最小节点则为Leader,不是,则自杀删除自己注册的节点。其流程如下:
这里需要特别声明下,所有的watcher都是一次性的,如果需要再次监控变化,需要再次注册watcher监听。
三、应用场景
接下来,我们谈谈ZooKeeper在产品中的具体实现和开发,以保证产品的分布式运行。
1、服务注册中心
随着分布式的发展和微服务的增多,需要一个管理这些服务的控制中心,即服务注册中心,其主要提供所有服务注册信息的中心存储,同时负责将服务注册信息的更新实时通知给服务消费者,主要通过ZooKeeper的Watcher机制实现。
服务启动,注册临时节点,格式:/{project}/services/{module}/{ip:port},例如在192.168.1.200服务器上启动某服务模块,服务端口为1234,则其注册节点为:/zk_project/services/zk_module/192.168.1.200:1234,并开始初始化服务缓存,把服务模块所有节点信息保存在缓存中,且通过watcher监控路径为/{project}/services/{module}/下的节点变化,若发生变化,获取节点信息更新缓存,保证缓存和ZooKeeper的信息保持一致,其他模块亦是如此。当需要访问其他模块时,通过算法,得到相应的模块ip:port信息,再通过Http访问其他模块,当然安全认证的过程是不可少的。架构设计如下如所示:
2、集群管理
随着分布式系统规模的日益扩大,集群中的机器规模也随之变大,因此,如何更好地进行集群管理也显得越来越重要了,否则则是一盘散沙。
例如,在日常开发和运维的过程中,平台需要知道当前有多少在线的服务器,以及对机器进行上下线的操作。为了实现自动化的线上运维,我们对机器的上下线情况有一个全局的监控。首先将指定的agent部署到这些机器上并启动,向ZooKeeper指定路径下注册临时节点,完成后,对当前路径watcher的监控就会接受到节点变更事件,即上线通知,同样机器下线时,监控同样会接收到下线通知,这样便实现了对服务器上下线的检测,最后同步数据库,保证数据库准确性以及和真实场景的一致性。其结构流程如下:
注明:这里的同步数据库操作,是由服务实例集群的Leader执行,接下来的Leader选举会详细说明
3、Leader选举
Leader选举,它是用来保证集群运行的过程中任务执行的唯一性和准确性,如果每个节点服务器都能执行Leader才能执行任务,会导致服务混乱以及数据的不准确性。
分布式都是多实例多点运行,一个服务对应一个多实例服务集群,这些实例运行着相同的程序,对外提供相同的服务。但某些任务只能由Leader执行,当Leader服务器down机或者发生故障时,需要从其他的实例中选举一个Leader来继续执行任务。Leader选举就是通过上述自杀式处理方式实现的,因为需要选出网络性能最好的机器来当Leader。结构图如下:
4、分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或者同一个系统的不同主机之间共享一个或一组资源,那么访问这些资源的时候,会需要通过一些手段来防止彼此之间的干扰,以保证一致性,这个情况下,就需要使用分布式锁了。
产品中分布式锁的实现是通过上述阻塞式,如果是最小节点则获取锁,进行事务处理,不是则大序号临时节点会watcher监听前一个小序号节点存在情况,如果小序号节点消失,大序号节点的实例会受到事件通知,再通过判断,如果是最小,则获取锁,否则继续监听前一个小序号节点,以此类推循环,直到拿到锁为止。结构图如下:
四、实践总结
ZooKeeper在分布式系统开发过程中,占据着很重要的地位。在产品选型调研之后,选ZooKeeper作为分布式协调、服务注册与发现等的解决方案,因为它具有如下优点:
1、开源软件,背景强悍,很多互联网公司都在使用;
2、部署简单、容易入门,提供简单API;
3、支持多语言的客户端;
4、存储的可靠性强,表现为配置管理、命名服务以及多备份;
5、可通过Watcher机制实现Push模型,节点信息的变更能够及时发出通知;
6、客户端与服务器通信的连接状态自动维护,并能自动删除节点并通知。
但是ZooKeeper也有自己缺点:
1、是一个CP型系统,所以当网络分区问题发生时,系统就不能注册或查找服务;
2、需要自己去实现服务注册与发现逻辑;
3、使用ZooKeeper会带来新的复杂性和运维问题。
不过,ZooKeeper在成熟、健壮及丰富的特性上具有着很大的优势,所以最终决定采用ZooKeeper作为我们的解决方案。 |