zookeeper介绍
Zookeeper 会维护一个具有层次关系的数据结构,它非常类似于一个标准的文件系统.
Zookeeper 这种数据结构有如下这些特点:
- 每个子目录项如 NameService 都被称作为 znode,这个 znode 是被它所在的路径唯一标识,如 Server1 这个 znode 的标识为 /NameService/Server1
- znode 可以有子节点目录,并且每个 znode 可以存储数据,注意 EPHEMERAL 类型的目录节点不能有子节点目录
- znode 是有版本的,每个 znode 中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据
- znode 可以是临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除,Zookeeper 的客户端和服务器通信采用长连接方式,每个客户端和服务器通过心跳来保持连接,这个连接状态称为 session,如果 znode 是临时节点,这个 session 失效,znode 也就删除了
znode 的目录名可以自动编号,如 App1 已经存在,再创建的话,将会自动命名为 App2 - znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是 Zookeeper 的核心特性,Zookeeper 的很多功能都是基于这个特性实现的
zookeeper应用举例
Zookeeper 作为一个分布式的服务框架,主要用来解决分布式集群中应用系统的一致性问题,它能提供基于类似于文件系统的目录节点树方式的数据存储,但是 Zookeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达到基于数据的集群管理
其应用场景大致可以分为以下几类:
1. 统一命名服务(Name Service)
分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住,通常情况下用树形的名称结构是一个理想的选择,树形的名称结构是一个有层次的目录结构,既对人友好又不会重复。说到这里你可能想到了 JNDI,没错 Zookeeper 的 Name Service 与 JNDI 能够完成的功能是差不多的,它们都是将有层次的目录结构关联到一定资源上,但是 Zookeeper 的 Name Service 更加是广泛意义上的关联,也许你并不需要将名称关联到特定资源上,你可能只需要一个不会重复名称,就像数据库中产生一个唯一的数字主键一样。
Name Service 已经是 Zookeeper 内置的功能,你只要调用 Zookeeper 的 API 就能实现。如调用 create 接口就可以很容易创建一个目录节点。
2.配置管理(Configuration Management)
配置的管理在分布式应用环境中很常见,例如同一个应用系统需要多台 PC Server 运行,但是它们运行的应用系统的某些配置项是相同的,如果要修改这些相同的配置项,那么就必须同时修改每台运行这个应用系统的 PC Server,这样非常麻烦而且容易出错。
像这样的配置信息完全可以交给 Zookeeper 来管理,将配置信息保存在 Zookeeper 的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中。
3.集群管理(Group Membership)
Zookeeper 能够很容易的实现集群管理的功能,如有多台 Server 组成一个服务集群,那么必须要一个“总管”知道当前集群中每台机器的服务状态,一旦有机器不能提供服务,集群中其它集群必须知道,从而做出调整重新分配服务策略。同样当增加集群的服务能力时,就会增加一台或多台 Server,同样也必须让“总管”知道。
Zookeeper 不仅能够帮你维护当前的集群中机器的服务状态,而且能够帮你选出一个“总管”,让这个总管来管理集群,这就是 Zookeeper 的另一个功能 Leader Election。
它们的实现方式都是在 Zookeeper 上创建一个 EPHEMERAL 类型的目录节点,然后每个 Server 在它们创建目录节点的父目录节点上调用
getChildren(String path, boolean watch) 方法并设置 watch 为 true,由于是 EPHEMERAL 目录节点,当创建它的 Server 死去,这个目录节点也随之被删除,所以 Children 将会变化,这时
getChildren上的 Watch 将会被调用,所以其它 Server 就知道已经有某台 Server 死去了。新增 Server 也是同样的原理。
Zookeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个 EPHEMERAL 目录节点,不同的是它还是一个 SEQUENTIAL 目录节点,所以它是个 EPHEMERAL_SEQUENTIAL 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是 EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。
4.共享锁
共享锁在同一个进程中很容易实现,但是在跨进程或者在不同 Server 之间就不好实现了。Zookeeper 却很容易实现这个功能,实现方式也是需要获得锁的 Server 创建一个 EPHEMERAL_SEQUENTIAL 目录节点,然后调用
getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用
exists(String path, boolean watch) 方法并监控 Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。
5. 队列管理
Zookeeper 可以处理两种类型的队列:
- 当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。
- 队列按照 FIFO 方式进行入队和出队操作,例如实现生产者和消费者模型。
同步队列用 Zookeeper 实现的实现思路如下:
创建一个父目录 /synchronizing,每个成员都监控标志(Set Watch)位目录 /synchronizing/start 是否存在,然后每个成员都加入这个队列,加入队列的方式就是创建 /synchronizing/member_i 的临时目录节点,然后每个成员获取 / synchronizing 目录的所有目录节点,也就是 member_i。判断 i 的值是否已经是成员的个数,如果小于成员个数等待 /synchronizing/start 的出现,如果已经相等就创建 /synchronizing/start。
用下面的流程图更容易理解:
FIFO 队列用 Zookeeper 实现思路如下:
实现的思路也非常简单,就是在特定的目录下创建 SEQUENTIAL 类型的子目录 /queue_i,这样就能保证所有成员加入队列时都是有编号的,出队列时通过 getChildren( ) 方法可以返回当前所有的队列中的元素,然后消费其中最小的一个,这样就能保证 FIFO。
为更容易理解zookeeper到底做了什么,我这里写了个小例子,选举leader节点,用golang书写,主要用到了zookeeper的创建目录及监听接口。
zookeeper集群的搭建
我的zookeeper集群与以往一样,继续使用docker来搭建,简单方便,不了解的可以直接点击我的以往博客.
将下述文件保存为docker-compose.yml,并利用docker-compose up -d 来启动zookeeper集群。说明一下,docker镜像依旧使用hyperledger/fabric-zookeeper镜像。有兴趣的可以自己制作。
# Copyright IBM Corp. All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
version: '2'
networks:
default:
services:
zookeeper0:
container_name: zookeeper0
image: hyperledger/fabric-zookeeper
environment:
- ZOO_MY_ID=1
- ZOO_SERVERS=server.1=zookeeper0:2888:3888 server.2=zookeeper1:2888:3888 server.3=zookeeper2:2888:3888
ports:
- 2181:2181
- 2888:2888
- 3888:3888
networks:
default:
aliases:
- ${CORE_PEER_NETWORKID}
zookeeper1:
container_name: zookeeper1
image: hyperledger/fabric-zookeeper
environment:
- ZOO_MY_ID=2
- ZOO_SERVERS=server.1=zookeeper0:2888:3888 server.2=zookeeper1:2888:3888 server.3=zookeeper2:2888:3888
ports:
- 2182:2181
- 2889:2888
- 3889:3888
networks:
default:
aliases:
- ${CORE_PEER_NETWORKID}
zookeeper2:
container_name: zookeeper2
image: hyperledger/fabric-zookeeper
environment:
- ZOO_MY_ID=3
- ZOO_SERVERS=server.1=zookeeper0:2888:3888 server.2=zookeeper1:2888:3888 server.3=zookeeper2:2888:3888
ports:
- 2183:2181
- 2890:2888
- 3890:3888
networks:
default:
aliases:
- ${CORE_PEER_NETWORKID}
leader节点选举
先解释一下代码,这里利用了github.com/samuel/go-zookeeper/zk这个包来对zookeeper进行访问。这个包非常非常好用,几乎涵盖了zookeeper的所有接口,不过有点小bug。
每个client启动后,首先会尝试创建临时节点subNamespacePath,若创建成功则此节点变为leader节点,若尝试创建失败,则返回。然后所有的client都会监听subNamespacePath节点,一旦该节点有异动,则重新开始选举leader。
当client1节点被删除(该进程被kill掉)时,由于subNamespacePath是个临时节点,也会被zookeeper相应的删除掉。被zookeeper节点删除掉时,client2节点和client3节点会收到通知,subNamespacePath被删除了。则client2和client3节点可以重新启动选举leader节点
client2和client3重新选举,重新选举后client2成功,则该节点可以视为主节点。
分别从三个客户端运行下述代码
go run main.go 0
go run main.go 1
go run main.go 2
可以观察到某一个0号节点被选举为主节点,然后停掉0号节点后,随机选择一个节点为主节点。
package main
import (
"os"
"fmt"
"time"
"github.com/samuel/go-zookeeper/zk"
)
var zookeeperServers = []string{"localhost:2181","localhost:2182","localhost:2183"}
var namespacePath = "/namespacePath"
var childPath = "subNamespacePath"
func runServer(id chan int,childId string){
conn, connChan, err := zk.Connect(zookeeperServers, 3*time.Second)
if err != nil {
fmt.Println("get connection from zookeeper error")
return
}
connEvent := <-connChan
if connEvent.State == zk.StateConnected {
fmt.Println("connect to zookeeper server success!")
}else{
fmt.Println("connect to zookeeper err",connEvent.State)
}
fmt.Println("the server session id is ",conn.SessionID())
startSelectLeader(conn,childPath,childId)
children, state, childChan, err := conn.ChildrenW(namespacePath+"/"+childPath)
if err != nil {
fmt.Println("watch children error, ", err)
}
fmt.Println("watch children result, ", children, state)
isKilled := false
for {
select {
case childEvent := <-childChan:
if childEvent.Type == zk.EventNodeDeleted {
fmt.Println("znode delete event ", childEvent)
fmt.Println("start select leader")
startSelectLeader(conn,childPath,childId)
}else if childEvent.Type == zk.EventNodeCreated{
fmt.Println("the zookeeper state is ",state)
}else if childEvent.Type == zk.EventNotWatching{
startSelectLeader(conn,childPath,childId)
}
case killedId := <- id:
fmt.Println("I am killed",killedId)
isKilled = true
break
}
if isKilled {
break
}
}
fmt.Println("please close the connection")
conn.Close()
}
func startSelectLeader(conn *zk.Conn,childPath string,childId string){
path, err := conn.Create(namespacePath, nil, 0, zk.WorldACL(zk.PermAll))
if err == nil {
fmt.Println("create root path success",path)
} else {
fmt.Printf("create root path failure err:%v\n ", err)
}
childpath, err := conn.Create(namespacePath+"/"+childPath, nil, zk.FlagEphemeral, zk.WorldACL(zk.PermAll))
if err == nil {
fmt.Printf("create childPath success and child path is %v child id is %v\n",childpath,childId)
} else {
fmt.Printf("create child path failure err:%v\n ", err)
}
}
func main(){
id0 := make(chan int)
id1 := make(chan int)
id := os.Args[1]
go runServer(id0,id)
time.Sleep(time.Duration(5) * time.Second)
<- id1
}
参考文档如下:
https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/
http://blog.csdn.net/xinguan1267/article/details/38422149