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

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

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

ZooKeeper的watch

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

可是光看文档不足以对watch功能有一个具体的感觉。因此接下来就让咱们安装并运行一个ZooKeeper服务端,实际体验一下。程序员

ZooKeeper下载安装和启动

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

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

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

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

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

用Go语言写的ZooKeeper的watch示例

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

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

watch children

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

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上。有兴趣的小伙伴能够尝试一下

相关文章
相关标签/搜索