zookeeper应用及其原理解析

miloyang
0 评论
/ /
524 阅读
/
8689 字
24 2023-10

在分布式架构中,其中协调服务,你绕不过去zookeeper,很多设计理念都是借鉴与zookeeper,值得了解下。

zookeeper介绍

zookeeper官网

在分布式组件里面,zookeeper这个中间件属于鼻祖的地位,因为在很早分布式概念提出来的时候,zookeeper里面有一些精彩的设计,比如分布式锁、集群选举、崩溃恢复、数据同步等等,这些精彩的场景,zookeeper有一些精妙的设计。后面很多中间件,比如redis、kafka都需要用或者借鉴了zookeeper的设计。

zookeeper是一种分布式协调服务,用于管理大型主机。在分布式环境中协调和管理服务是一个复杂的过程,zookeeper通过及其简单的架构和api解决了这个问题。

这么说吧,如果纯小白学习,你就把zookeeper当做redis来学习,它就是存储数据用的。然后再深挖。

应用场景

分布式协调组件

我们有一个服务A,分别部署了两台服务器(服务A-1和服务A-2),作为集群,通过Nginx做负载均衡,其中里面都有一个变量比如叫做flag,默认值为true。
在第一次访问的时候,把服务A-1的flag设为了false,此时服务B-1的值还是true。这就会造成集群内状态不对等。那么通过zookeeper中间件,可以监听,一旦节点发生改变,就会通知所有监听放改变自己的值。

分布式锁

比如redis,是AP模型,做到了分布式下的可用性,但是在某些情况下数据会不一致,通过zookeeper在实现分布式锁上面,可以做到强一致性(顺序一致性)。后续详聊。

无状态化的实现

还是集群架构,有A、B、C三个服务器,都部署了登录系统,此时用户登录上来后,链接到了A系统,A系统保存了用户的登录态,但下一次负载均衡可能连接到了B系统,此时B系统没有登录态,就会造成状态不一致,那么可以通过zookeeper作为中间件配置登录信息,维护登录状态。

zookeeper安装

熟悉的朋友都知道,一般都是通过docker来安装:

  • 拉取最新的zookeeper镜像

    docker pull zookeeper

  • 运行镜像

    因为我本身环境自带zookeeper,2181端口被暂用,所以使用了2182端口
    docker run --name zk01 -p 2182:2181 -d zookeeper:latest

  • 进入容器

    docker exec -it zk01 /bin/bash

  • 找到zkCli.sh

root@1f766ac661c2:/apache-zookeeper-3.9.1-bin/bin# find / -name "zkCli.sh"
/apache-zookeeper-3.9.1-bin/bin/zkCli.sh
root@1f766ac661c2:/apache-zookeeper-3.9.1-bin/bin# cd /apache-zookeeper-3.9.1-bin/bin
root@1f766ac661c2:/apache-zookeeper-3.9.1-bin/bin# zkCli.sh
# 这里运行起来说明安装成功

数据模型

zk中的数据是保存在节点上面的,节点就是znode,多个znode之间构成一颗树的目录结构。 zk中的数据模型,很像数据结构中的树,也像文件系统的目录。

jnhaYzI3Pumf4vlca2qEE4o

比如上图,最开始都是根目录 / ,然后下面分成多个节点,比如学校、企业,这种节点叫Znode。但是不同于树的节点,Znode的引用方式是路径引用的,类似与文件路径中的,比如要找到一年级,就是:
/学校/小学/一年级
这样的层级结构,让每一个Znode节点拥有唯一的路径,就像命名空间一样对不同信息做出清晰的隔离。
我们来实际操作一下,比如创建\学校\小学\一年级这样的路径出来。

[zk: localhost:2181(CONNECTED) 5] create /学校/小学/一年级
Created /学校/小学/一年级
[zk: localhost:2181(CONNECTED) 6] set /学校/小学/一年级 milo
[zk: localhost:2181(CONNECTED) 7] get /学校/小学/一年级
milo

ps:看到这里,你是不是觉得,这不就是redis么。。。 到这里,你先这么理解吧。

那么,Znode是什么样的结构呢?

znode

在zk中的znode,包含了四个部分:

data:保存数据,如刚刚我们把milo保存在/学校/小学/一年级的znode里面

acl:权限,定义了什么样的用户能够操作这个节点,且能够进行怎样的操作。 
    c:create 创建权限,允许在该节点下创建子节点
    r:read 读取权限,允许读取该节点的内容以及子节点的列表信息
    d:delete 删除权限,允许删除该节点的子节点信息
    a:admin 管理者权限,允许对该节点进行acl权限设置
    
stat:描述当前znode的元数据,通过get -s 来查看,get -s /学校,可以看到一些信息。这些都是元数据

child:当前节点的子节点

那 有哪些类型呢?注意了,这个类型不是redis的那几种类型。

类型

  • 持久节点

    创建的节点,在会话结束后依然存在,保存数据,我们刚刚创建的 /学校/小学/一年级,就是持久节点,使用create创建。

  • 持久序号节点

    这是啥意思?理解成高并发下,同时很多创建了数据,那么是谁创建的呢?就会在节点上带一个数值,越后执行数值越大,适用于分布式锁的单调递增场景下。

     create -s /test1
    Created /test10000000008
    
  • 临时节点

    使用 create -e来创建。

    zk: localhost:2181(CONNECTED) 0] create -e /test5
    Created /test5
    

    临时节点是在会话结束后,自动被删除的,不然怎么叫做临时。那么临时节点是如何被删除的呢?看下图:

    YxSOo5NzyH2zPef5gc

    • 对于持久节点,zk客户端和服务器连接建立好了之后,服务器会生成并返回一个sessionid,发给zk客户端。这个sessionid是保存在服务端的,一直存在。
    • 临时节点,也是生成一个sessionid,存在服务端,但是这个sessionid是有一个过期时间的,如果持续会话的话,这个时间会一直续约的,一直存货。一旦会话断开,时间到期,服务端就会删除没有续约的sessionid对应的临时节点。

    这个有什么应用场景呢?其中有一个很重要的场景,就是可以实现服务注册和发现。比如服务A和服务B都是分布式的,都在注册中心里面,通过注册中心就可以实现A和B的通信,但是一旦A宕机了,注册中心就可以通过临时节点检查到宕机,从而B调不到A服务。

  • 临时序号节点

    跟持久序号节点相同,适用于临时的分布式锁。

  • Container节点

    容器节点,当前容器没有任何子节点的时候,该容器节点会被zk定时删除,一般是60s。通过-c来创建。create -c /mycontainer

  • TTL节点

    可以指定节点的到期事件,到期后被zk定时删除,不过只能通过系统配置来开启。

持久化

zk的数据是运行在内存里面的,那如果没有落地,必然会丢,所以zk提供了两种持久化机制:

  • 事务日志

    zk把执行的命令以日志形式的方式保存在指定的路径中,如果没有指定,则按照dataDir指定的路径

  • 数据快照 zk会在一定的时间间隔内左一次内存数据的快照,把该时刻的内存数据保存在快照里面。

朋友们,像不像redis的持久化机制? 不过zk是这两种形式都是开启的。
在回复的时候先恢复快照文件中的数据到内存中,再用日志文件中的数据做增量回复,这样恢复速度更快。

使用

创建节点相关

- 创建持久节点
  create path [data] [acl]

- 创建持久序号节点
  create -s path [data] [acl]

- 创建临时节点
  create -e path [data] [acl]

- 创建临时序号节点
  create -e -s path [data] [acl]

- 创建容器节点
  create -c path [data] [acl]

查询相关

  • 普通查询
- ls [-s -R] path
  -s 详细信息
  -R 当前目录和子目录中的所有信息
  • 查询节点相关信息
- cZxid:创建节点的事务ID
- mZxid:修改节点的事务ID
- pZxid:添加和删除子节点的事务ID
- ctime:节点创建的时间
- mtime:节点最近修改的时间
- dataVersion:节点内数据的版本,每更新一次数据,版本会+1
- aclVersion:此节点的权限版本
- ephemeralOwner:如果当前节点是临时节点,该是是当前节点所有者的session id。如果节点不是临时节点,则该值为零
- dataLength:节点内数据的长度
- numChildren:该节点的子节点个数
  • 查询节点的内容
get [-s] path
-s 详细信息

删除相关

  • 普通删除
delete /path
  • 乐观锁删除

乐观锁:就是整个系统乐观的认为当前系统并发不是很严重,所以很多的时候不需要上锁。这也是一种锁,只是在真正并发出现的时候才去上的。 版本号:每次正对于node进行操作,版本都会加1.如:

[zk: localhost:2181(CONNECTED) 2] set /test2 milo1
[zk: localhost:2181(CONNECTED) 3] ls -s /test2
dataVersion = 1
[zk: localhost:2181(CONNECTED) 4] set /test2 milo2
[zk: localhost:2181(CONNECTED) 5] ls -s /test2
dataVersion = 2
- delete [-v] path
  -v 版本,要是真正的版本号,
- deleteall path [-b batch size]

如果此时我带版本为1去删除/test2,肯定是删除不成功的,因为版本不对,需要带真正的版本,这是版本相关问题,后续介绍。

[zk: localhost:2181(CONNECTED) 2] delete -v 1 /test2
version No is not valid : /test2
[zk: localhost:2181(CONNECTED) 3] delete -v 2 /test2

权限设置

  • 注册当前会话的账号和密码:

    addauth digest miloyang:123456

  • 创建节点/test-node,并设置权限(指定该节点的用户,以及用户所拥有的权限cdwra,创建、删除、可读、可写、可设置)

    create /test-node abcd auth:miloyang:123456:cdwra

  • 在另一个会话中必须先使用账号密码,才能拥有操作节点的权限

Go语言相关操作

先拉包:go get -u github.com/samuel/go-zookeeper/zk

建立连接

func Connect() (*zk.Conn, error) {
    hosts := []string{"ip:port"}
    conn, _, err := zk.Connect(hosts, time.Second*5)
    if err != nil {
        return nil, err
    }
    return conn, nil
}

相关操作

func NodeValue(conn *zk.Conn, path string) {
    result, stat, err := conn.Get(path)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(result))
    fmt.Println(stat) // 详细信息
}

func SetNodeValue(conn *zk.Conn, path string, value []byte) {
    // 获取节点的version
    _, stat, err := conn.Get(path)
    if err != nil {
        panic(err)
    }
    _, err = conn.Set(path, value, stat.Version)
    if err != nil {
        panic(err)
    }
}

// 创建节点
func CreateNode(conn *zk.Conn) {
    //创建永久节点
    path, err := conn.Create("/name", []byte("miloyang"), 0, zk.WorldACL(zk.PermAll))
    if err != nil {
        panic(err)
    }
    fmt.Printf("Created node path[%v]\n", path)

    /*
        //创建临时节点,在会话结束时会自动删除临时节点
        ephemeral, err := conn.Create("/ephemeral", nil, zk.FlagEphemeral, zk.WorldACL(zk.PermAll))
        if err != nil {
            panic(err)
        }
        fmt.Printf("Created ephemeral node path[%v]\n", ephemeral)

        //创建顺序节点
        sequence, err := conn.Create("/sequence", nil, zk.FlagSequence, zk.WorldACL(zk.PermAll))
        if err != nil {
            panic(err)
        }
        fmt.Printf("Created sequence node path[%v]\n", sequence)

        //创建临时顺序节点 create -es /ephemeralsequece
        ephemeralsequece, err := conn.Create("/ephemeral_seq", nil, zk.FlagEphemeral|zk.FlagSequence, zk.WorldACL(zk.PermAll))
        if err != nil {
            panic(err)
        }
        fmt.Printf("Created ephemeralsequece node path[%v]\n", ephemeralsequece)

    */
}
人未眠
工作数十年
脚步未曾歇,学习未曾停
乍回首
路程虽丰富,知识未记录
   借此博客,与之共进步