温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Etcd和ZooKeeper,究竟谁在watch的功能表现更好?

发布时间:2020-07-02 22:18:55 来源:网络 阅读:1083 作者:七仙女很忙 栏目:开发技术

Etcd和ZooKeeper,究竟谁在watch的功能表现更好?

ZooKeeper和Etcd的主要异同可以参考这篇文章,此外,Etcd的官网上也有对比表格(https://coreos.com/etcd/docs/latest/learning/why.html),本文不加赘述。

本文主要关注这两者在watch上的功能差异。ZooKeeper和Etcd都可以对某个key进行watch,并在当这个key发生改变(比如有更新值,或删除key的操作发生)时触发。

ZooKeeper的watch

ZooKeeper的watch功能可参考其官网文档

但是光看文档不足以对watch功能有一个具体的感受。所以接下来就让我们安装并运行一个ZooKeeper服务端,实际体验一下。

ZooKeeper下载安装和启动

首先,要使用ZooKeeper,我们可以去其官网的Release页面下载最新的ZooKeeper。
下载下来是一个tar包,解压并进入zookeeper目录:

tar zxvf zookeeper-3.4.14.tar.gz
cd zookeeper-3.4.14

其conf目录中是配置文件,我们需要将zoo_sample.cfg复制为zoo.cfg
然后执行bin目录下的zkServer.sh启动ZooKeeper服务:

cp conf/zoo_sample.cfg conf/zoo.cfg
bin/zkServer.sh start

ZooKeeper服务启动后会在本地默认的2181端口开始监听。

用Go语言写的ZooKeeper的watch示例

首先,我们需要下载这样一个第三方的go包用来访问ZooKeeper服务:

go get github.com/samuel/go-zookeeper/zk

watch children

go-zookeeper源码的example目录中提供了一个basic.go,这个程序可以watch根目录"/"下的子节点的创建和删除事件:

package main

import (
        "fmt"
        "time"

        "github.com/samuel/go-zookeeper/zk"
)

func main() {
        c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second) //*10)
        if err != nil {
                panic(err)
        }
        children, stat, ch, err := c.ChildrenW("/")
        if err != nil {
                panic(err)
        }
        fmt.Printf("%+v %+v\n", children, stat)
        e := <-ch
        fmt.Printf("%+v\n", e)
}

这个示例代码调用ChildrenW方法watch根目录"/"下的children节点。
用go run运行这段代码:

$ go run basic.go
2019/04/16 16:11:33 Connected to 127.0.0.1:2181
2019/04/16 16:11:33 Authenticated: id=72753663009685508, timeout=4000
2019/04/16 16:11:33 Re-submitting `0` credentials after reconnect
[1 zookeeper] &{Czxid:0 Mzxid:0 Ctime:0 Mtime:0 Version:0 Cversion:2 Aversion:0 EphemeralOwner:0 DataLength:0 NumChildren:2 Pzxid:32}

我们可以看到客户端已经连接上并打印出了根目录"/"的children和stat,目前根目录"/"下的children共有两个,分别是"1"和"zookeeper"。
程序现在阻塞在ChildrenW创建的channel ch上,等待事件发生。
接下来,让我们另开一个console运行ZooKeeper自带的客户端zkCli.sh并用create命令创建一个子节点"/2":

$ bin/zkCli.sh
[zk: localhost:2181(CONNECTED) 2] create /2 value
Created /2

此时,由于根目录下新增了一个子节点,之前的basic.go程序打印出watch事件并退出:

{Type:EventNodeChildrenChanged State:Unknown Path:/ Err:<nil> Server:}

需要注意的是,这个watch操作触发一次后channel就会关闭。所以试图用range ch的方式循环watch不可行,客户端代码必须再次调用ChildrenW才能watch下一个事件。
经过多次类似测试后,我们可以发现,ChildrenW仅能watch子节点 child的创建和删除等操作,对某个child的值进行更新操作是无法被watch捕捉的,而且也无法捕捉孙节点的创建删除操作。

watch node

如果需要捕捉某个节点的值的更新操作,我们需要用GetW方法来进行watch,见下列示例watch.go:

package main

import (
        "fmt"
        "os"
        "time"

        "github.com/samuel/go-zookeeper/zk"
)

func main() {
        c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second) //*10)
        if err != nil {
                panic(err)
        }
        b, stat, ch, err := c.GetW(os.Args[1])
        if err != nil {
                panic(err)
        }
        fmt.Printf("%+v %+v\n", string(b), stat)
        e := <-ch
        fmt.Printf("%+v\n", e)
}

运行watch.go监视/1节点的内容变更:

$ go run watch.go /1
2019/04/16 16:56:16 Connected to 127.0.0.1:2181
2019/04/16 16:56:16 Authenticated: id=72753663009685517, timeout=4000
2019/04/16 16:56:16 Re-submitting `0` credentials after reconnect
value &{Czxid:2 Mzxid:60 Ctime:1555314817581 Mtime:1555404853396 Version:11 Cversion:4 Aversion:0 EphemeralOwner:0 DataLength:5 NumChildren:2 Pzxid:28}

在zkCli中用set命令设置/1的值

[zk: localhost:2181(CONNECTED) 12] set /1 value

watch.go打印出事件:

{Type:EventNodeDataChanged State:Unknown Path:/1 Err:<nil> Server:}

注意这里的事件Type是EventNodeDataChanged,且"/1"节点必须一开始存在,如果"/1"节点不存在,试图对"/1"进行GetW就会报错。

watch existence

如果我们希望watch某个节点的存在性发生的变化,我们需要用ExistsW,见示例exist.go

package main

import (
        "fmt"
        "os"
        "time"

        "github.com/samuel/go-zookeeper/zk"
)

func main() {
        c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second) //*10)
        if err != nil {
                panic(err)
        }
        b, stat, ch, err := c.ExistsW(os.Args[1])
        if err != nil {
                panic(err)
        }
        fmt.Printf("%+v %+v\n", b, stat)
        e := <-ch
        fmt.Printf("%+v\n", e)
}

运行exist.go监视"/2"的存在性

$ go run exist.go /2
2019/04/16 17:12:33 Connected to 127.0.0.1:2181
2019/04/16 17:12:33 Authenticated: id=72753663009685521, timeout=4000
2019/04/16 17:12:33 Re-submitting `0` credentials after reconnect
false &{Czxid:0 Mzxid:0 Ctime:0 Mtime:0 Version:0 Cversion:0 Aversion:0 EphemeralOwner:0 DataLength:0 NumChildren:0 Pzxid:0}

用zkCli创建/2

[zk: localhost:2181(CONNECTED) 14] create /2 2
Created /2

exist.go打印事件

{Type:EventNodeCreated State:Unknown Path:/2 Err:<nil> Server:}

注意这里create事件的Type是EventNodeCreated。同样,如果发生delete事件,那么Type将是EventNodeDeleted

ZooKeeper总结

  1. watch children只能watch子节点,不能递归watch孙节点
  2. watch children只能watch子节点的创建和删除,不能watch子节点值的变化
  3. watch node只能对已经存在的node进行watch,对不存在的node需要watch existence
    除了上述的这些不足以外,在其官网文档中自己也提到,在watch被触发和重新设置之间发生的事件将被丢弃,无法被捕捉。
    接下来让我们看看Etcd的watch。

Etcd的watch

Etcd的watch功能见其API文档:https://coreos.com/etcd/docs/latest/learning/api.html#watch-api。

Etcd支持Docker镜像启动而无需安装,只要我们预先安装了Docker,那么只需执行一条简单的命令就可以直接在本机启动Etcd服务。

用Docker启动Etcd

Etcd在其github的Release页面:https://github.com/etcd-io/etcd/releases上提供了Docker启动命令,让我们可以免去繁琐的下载安装步骤,只需执行下列代码,就可以将这个docker镜像下载到本地运行:

rm -rf /tmp/etcd-data.tmp && mkdir -p /tmp/etcd-data.tmp && \
  docker rmi gcr.io/etcd-development/etcd:v3.3.12 || true && \
  docker run \
  -p 2379:2379 \
  -p 2380:2380 \
  --mount type=bind,source=/tmp/etcd-data.tmp,destination=/etcd-data \
  --name etcd-gcr-v3.3.12 \
  gcr.io/etcd-development/etcd:v3.3.12 \
  /usr/local/bin/etcd \
  --name s1 \
  --data-dir /etcd-data \
  --listen-client-urls http://0.0.0.0:2379 \
  --advertise-client-urls http://0.0.0.0:2379 \
  --listen-peer-urls http://0.0.0.0:2380 \
  --initial-advertise-peer-urls http://0.0.0.0:2380 \
  --initial-cluster s1=http://0.0.0.0:2380 \
  --initial-cluster-token tkn \
  --initial-cluster-state new

用Go语言写Etcd的watch

Etcd本身就是用Go写的,且官方提供了Go的SDK,当前最新的版本是v3,我们可以直接用go get获取:

go get go.etcd.io/etcd/clientv3

prefix watch

Etcd支持单点watch,prefix watch以及ranged watch。
和ZooKeeper不同,Etcd不会根据事件的不同而要求调用不同的watch API,三类watch的区别仅在于对key的处理不同:
单点watch仅对传入的单个key进行watch;
ranged watch可以对传入的key的范围进行watch,范围内的key的事件都会被捕捉;
而prefix则可以对所有具有给定prefix的key进行watch。
作为示例,本文仅给出prefix watch的代码prefix.go如下:

package main

import (
        "context"
        "fmt"
        "log"
        "time"

        "go.etcd.io/etcd/clientv3"
)

func main() {
        cli, err := clientv3.New(clientv3.Config{
                Endpoints:   []string{"127.0.0.1:2379"},
                DialTimeout: 5 * time.Second,
        })
        if err != nil {
                log.Fatal(err)
        }
        defer cli.Close()

        rch := cli.Watch(context.Background(), "foo", clientv3.WithPrefix())
        for wresp := range rch {
                for _, ev := range wresp.Events {
                        fmt.Printf("%s %q : %q\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
                }
        }
}

可以看到,Etcd的watch channel是可以重复利用的,客户端可以不停地从channel中接收到来自服务端的事件通知。
运行prefix.go,客户端就会一直阻塞在channel上等待事件通知:

$ go run prefix.go

在另一个console下面,我们可以用docker镜像中提供的Etcd的客户端etcdctl来进行一些PUT和DELETE操作

$ docker exec etcd-gcr-v3.3.12 /bin/sh -c "ETCDCTL_API=3 /usr/local/bin/etcdctl put foo 1"
OK
$ docker exec etcd-gcr-v3.3.12 /bin/sh -c "ETCDCTL_API=3 /usr/local/bin/etcdctl put foo2 2"
OK
$ docker exec etcd-gcr-v3.3.12 /bin/sh -c "ETCDCTL_API=3 /usr/local/bin/etcdctl put foo/1 a"
OK
$ docker exec etcd-gcr-v3.3.12 /bin/sh -c "ETCDCTL_API=3 /usr/local/bin/etcdctl put foo/2 b"
OK
$ docker exec etcd-gcr-v3.3.12 /bin/sh -c "ETCDCTL_API=3 /usr/local/bin/etcdctl del foo"
1
$ docker exec etcd-gcr-v3.3.12 /bin/sh -c "ETCDCTL_API=3 /usr/local/bin/etcdctl del foo/1"
1
$ docker exec etcd-gcr-v3.3.12 /bin/sh -c "ETCDCTL_API=3 /usr/local/bin/etcdctl del foo/2"
1

与之对应的prefix.go输出是:

$ go run prefix.go
PUT "foo" : "1"
PUT "foo2" : "2"
PUT "foo/1" : "a"
PUT "foo/2" : "b"
DELETE "foo" : ""
DELETE "foo/1" : ""
DELETE "foo/2" : ""

可以看到,Etcd的PUT语义覆盖了ZooKeeper的create语义和set语义。同时,prefix watch不仅可以watch节点自身的PUT和DELETE,也可以watch其所有的子孙节点的PUT和DELETE。

ZooKeeper和Etcd的watch基本功能就介绍到这里,接下来,我们要谈谈watch机制一个至关重要的问题:

事件发生的太快来不及watch怎么办

通常我们使用watch功能是为了让程序阻塞等待某些事件的发生并进行相应的处理,然而现实世界中处理的速度有可能跟不上事件发生的速度。
比如ZooKeeper的watch在捕捉到一个事件后channel就会关闭,需要我们再次去发送watch请求。在此期间发生的事件将丢失,下文引用自ZooKeeper官网文档原文:

Because watches are one time triggers and there is latency between getting the event and sending a new request to get a watch you cannot reliably see every change that happens to a node in ZooKeeper. Be prepared to handle the case where the znode changes multiple times between getting the event and setting the watch again. (You may not care, but at least realize it may happen.)

Etcd解决这个问题的方法是在API的请求和响应中添加了一个版本号,客户端可以在watch请求中指定版本号来获取自该版本号以来发生的所有变化,见prefix_with_rev.go的示例:

package main

import (
        "context"
        "fmt"
        "log"
        "os"
        "strconv"
        "time"

        "go.etcd.io/etcd/clientv3"
)

func main() {
        cli, err := clientv3.New(clientv3.Config{
                Endpoints:   []string{"127.0.0.1:2379"},
                DialTimeout: 5 * time.Second,
        })
        if err != nil {
                log.Fatal(err)
        }
        defer cli.Close()

        rev := 0
        if len(os.Args) > 1 {
                rev, err = strconv.Atoi(os.Args[1])
                if err != nil {
                        log.Fatal(err)
                }
        }
        rch := cli.Watch(context.Background(), "foo", clientv3.WithPrefix(), clientv3.WithRev(int64(rev)))
        for wresp := range rch {
                for _, ev := range wresp.Events {
                        fmt.Printf("%s %q : %q, %d\n", ev.Type, ev.Kv.Key, ev.Kv.Value, ev.Kv.ModRevision)
                }
        }
}

注意和prefix.go相比,这里在调用Watch方法时额外提供了一个clientv3.WithRev(int64(rev))的参数用来指定版本号,rev=0意味着不指定。同时,我们还会打印出捕捉到的事件中发生的改变的版本号ev.Kv.ModRevision。

现在我们指定版本号1运行prefix_with_rev.go,程序立即打印出ModRevision大于等于1的所有变化,并继续阻塞等待新的事件:

$ go run prefix_with_rev.go 1
PUT "foo" : "bar", 2
PUT "foo" : "1", 3
PUT "foo/1" : "1", 4
PUT "foo/1" : "1", 5
PUT "foo" : "1", 6
PUT "foo" : "2", 7
PUT "foo/2" : "2", 8
DELETE "foo/2" : "", 9
PUT "foo" : "1", 10
PUT "foo2" : "2", 11
PUT "foo/1" : "a", 12
PUT "foo/2" : "b", 13
DELETE "foo" : "", 14
DELETE "foo/1" : "", 15
DELETE "foo/2" : "", 16
PUT "foo" : "a", 17
PUT "foo" : "a", 18
PUT "foo" : "a", 19
PUT "foo" : "a", 20
PUT "foo" : "a", 21
PUT "foo" : "a", 22
PUT "foo" : "a", 23

注意ModRevision等于1的事件并没有出现在结果中,这是因为该事件的Key不满足prefix=foo条件。

总结

不得不承认,作为后起之秀,Etcd在watch方面完胜ZooKeeper。

从功能的角度来看,Etcd只需要调用一次watch操作就可以捕捉所有的事件,相比ZooKeeper大大简化了客户端开发者的工作量。
ZooKeeper的watch获得的channel只能使用一次,而Etcd的watch获得的channel可以被复用,新的事件通知会被不断推送进来,而无需客户端重复进行watch,这种行为也更符合我们对go channel的预期。

ZooKeeper对事件丢失的问题没有解决办法。Etcd则提供了版本号帮助客户端尽量捕捉每一次变化。要注意的是每一次变化都会产生一个新的版本号,而这些版本不会被永久保留。Etcd会根据其版本留存策略定时将超出阈值的旧版本从版本历史中清除。

从开发者的角度来看,ZooKeeper是用Java写的,且使用了自己的TCP协议。对于程序员来说不太友好,如果离开了ZooKeeper提供的SDK自己写客户端会有一定的技术壁垒,而ZooKeeper官方只提供了Java和C语言的SDK,其它语言的开发者就只能去寻求第三方库的帮助,比如github.com/samuel/go-zookeeper/zk。

另一方面,Etcd是用Go写的,使用了Google的gRPC协议,官方除了提供Go语言的SDK之外,也提供了Java的SDK:https://github.com/etcd-io/jetcd。
另外Etcd官方还维护了一个zetcd项目:https://github.com/etcd-io/zetcd,它在Etcd外面套了一个ZooKeeper的壳。让那些ZooKeeper的客户端可以无缝移植到Etcd上。有兴趣的小伙伴可以尝试一下。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI