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总结
- watch children只能watch子节点,不能递归watch孙节点
- watch children只能watch子节点的创建和删除,不能watch子节点值的变化
- 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上。有兴趣的小伙伴可以尝试一下。
Etcd和ZooKeeper,究竟谁在watch的功能表现更好?的更多相关文章
- 探索etcd,Zookeeper和Consul一致键值数据存储的性能
这篇博文是探索三个分布式.一致性键值数据存储软件性能的系列文章中的第一篇:etcd.Zookeeper和Consul,由etcd团队所写,可以让我们全面地了解如何评估三个分布式一致存储软件的性能.翻译 ...
- etcd 与 Zookeeper、Consul 等其它 kv 组件的对比
基于etcd的分布式配置中心 etcd docs | etcd versus other key-value stores https://etcd.io/docs/v3.4.0/learning/w ...
- Zookeeper分布式集群原理与功能
Zookeeper功能简介 ZooKeeper 是一个开源的分布式协调服务,由雅虎创建,是 Google Chubby 的开源实现. 分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅 ...
- 使用小记:Zookeeper中动态改变节点的功能
Zookeeper 3.5+提供了reconfig功能实现动态配置节点,官方的说明是,"你再也不需要进行全部节点重启"就可以应用所有的修改: http://zookeeper.ap ...
- 服务发现:Zookeeper vs etcd vs Consul
[编者的话]本文对比了Zookeeper.etcd和Consul三种服务发现工具,探讨了最佳的服务发现解决方案,仅供参考. 如果使用预定义的端口,服务越多,发生冲突的可能性越大,毕竟,不可能有两个服务 ...
- Zookeeper vs etcd vs Consul
Zookeeper vs etcd vs Consul [编者的话]本文对比了Zookeeper.etcd和Consul三种服务发现工具,探讨了最佳的服务发现解决方案,仅供参考. 如果使用预定义的端口 ...
- 服务发现:Zookeeper vs etcd vs Consul 参考自http://dockone.io/article/667
服务发现:Zookeeper vs etcd vs Consul [编者的话]本文对比了Zookeeper.etcd和Consul三种服务发现工具,探讨了最佳的服务发现解决方案,仅供参考. 如果使用预 ...
- 使用Python进行分布式系统协调 (ZooKeeper/Consul/etcd)
来源:naughty 链接:my.oschina.net/taogang/blog/410864 笔者之前的博文提到过,随着大数据时代的到来,分布式是解决大数据问题的一个主要手段,随着越来越多的分布式 ...
- 服务注册选型比较:Consul vs Zookeeper vs Etcd vs Eureka
zookeeper基于paxos的化简版zab,etcd基于raft算法.consul也是基于raft算法.etcd和consul作为后起之秀,并没有因为已经有了zookeeper而放弃自己,而是采用 ...
随机推荐
- HDU 2546 饭卡(带限制的01背包变形)
思路:有几个解法,如下 1)先拿出5块买最贵的菜,剩下的菜再进行01背包.如何证明正确性?设最贵的菜价e,次贵的菜价s,设减去5后的余额为x,会不会产生这样的情况,假设用5元买了e,余额最多能买到x- ...
- (转)SQL注入攻击简介
如果你是做Javaweb应用开发的,那么必须熟悉那声名狼藉的SQL注入式攻击.去年Sony就遭受了SQL注入攻击,被盗用了一些Sony play station(PS机)用户的数据.在SQL注入攻击里 ...
- 不喜欢SAP GUI?那试试用Eclipse进行ABAP开发吧
Jerry和SAP成都研究院一些新同事聊天时,谈到ABAP和SAP GUI这个话题.很多新同事在加入SAP成都之前,是做Java和C++开发的,习惯了Eclipse/IntelliJ IDEA/Vis ...
- 使用nodejs和Java访问远程服务器的服务
既然这篇文章用的是nodejs和Java访问远程服务器的服务,那么咱们先用另一门编程语言,SAP的ABAP(我日常工作使用得最多的编程语言)来开发一个服务吧. 这是我用ABAP编程语言实现服务的类:Z ...
- VS2013常用快捷键[转] 注释 Ctrl+K - C 和 k - u
VS2013常用快捷键你敢不会? F1 帮助文档 F5 运行 F12 跳转到定义 F11 单步调试 Shift+F5 停止调试 Ctrl+滚轮 放大缩小当前视图 Ctrl+L 删除当前行 Ctrl ...
- python基础一 day15 复习
迭代器和生成器迭代器 可迭代协议 —— 含有iter方法的都是可迭代的 迭代器协议 —— 含有next和iter的都是迭代器 特点 节省内存空间 方便逐个取值,一个迭代器只能取一次.生成器 —— 迭代 ...
- python面试笔试题汇总
Python面试攻略(嗨谈篇) 110道python面试笔试题汇总,你能答对几道? Python 面试问答 Top 25 2018 年最常见的 Python 面试题 & 答案
- 字符编码:WideCharToMultiByte
WideCharToMultiByte 编辑 目录 1基本介绍及功能 2相关变量 1基本介绍及功能编辑 WideCharToMultiByte 函数功能:该函数映射一个unicode字符串 ...
- 使用notepad++远程编辑Linux文档
上一篇中,我写了如何使用使用ftp服务器实现很方便的通信,这一篇我分享一个使用notepad++的一个NPPFTP插件远程编辑Linux中的文档的小技巧. 首先要确保你的Linux的ftp服务已经打开 ...
- 《队长说得队》第八次团队作业Alpha冲刺
项目 内容 这个作业属于哪个课程 >>2016级计算机科学与工程学院软件工程(西北师范大学) 这个作业的要求在哪里 >>实验十二 团队作业8:软件测试与ALPHA冲刺 团队名称 ...