etcd是一个可靠的分布式key-value存储服务,常用于存储分布式系统的关键数据,它具有以下特点:
etcd是用Go编写的,在Raft下通过复制日志确保一致性。
etcd被许多公司用于生产环境,并部署在关键场景中,它经常与Kubernetes,locksmith,vulcand,Doorman等服务配合使用。它通过测试确保可靠性。
更详细介绍请参照官方文档
在releases页面可以获取etcd预构建版本二进制文件,它包含了OSX,Linux,Windows和Docker的版本。
- 下载releases页面中名为
etcd-*-windows-amd64.zip
文件 - 解压后直接运行
etcd.exe
就可以零配置开启一个etcd-server,默认监听127.1:2379
. - 如果想要在命令行中练习,需要在当前目录下打开命令行工具,通过
./etcdctl.exe h
查看帮助或执行合适的命令. - 在命令行设置
export ETCDCTL_API=3
(PowerShell下为$Env:ETCDCTL_API=3
) 可以使etcdctl使用v3版本的API - 为了方便起见,可以将
etcdctl.exe
所在的目录添加到PATH下,这样就可以使用etcdctl xxx
执行命令了
TODO:
TODO:
TODO:
TODO:
$ etcdctl version etcdctl version: 3.3.13 API version: 3.3
注: 该命令在API version: 2版本中是set
应用通过put
来储存kv到 etcd 中。etcd集群间通过 Raft 协议复制到所有成员来实现一致性和可靠性。
设置键 foo
的值为 bar
$ etcdctl put foo bar
OK
而且可以通过附加租约的方式为key设置过期时间。
将键 foo1 的值设置为 bar1 并在10s后过期的命令:
# 授予租约,TTL为10秒
$ etcdctl lease grant 10
lease 32695410dcc0ca06 granted with TTL(10s)
# 将租约附加到foo1
$ etcdctl put foo1 bar1 --lease=32695410dcc0ca06
OK
注意: 上面命令中的租约id 32695410dcc0ca06
是创建租约时返回的.
应用可以从 etcd 集群中查询key对应的value。查询可以是单个 key,也可以是某个范围的key。
假设 etcd 集群存储有下面的kv:
foo = bar
foo1 = bar1
foo2 = bar2
foo3 = bar3
读取键 foo
的值
$ etcdctl get foo
foo
bar
以16进制格式读取
$ etcdctl get foo --hex
\x66\x6f\x6f # 键
\x62\x61\x72 # 值
只显示键 foo 的值
$ etcdctl get foo --print-value-only
bar
选取以foo
为前缀的所有键
$ etcdctl get foo --prefix
foo
bar
foo1
bar1
foo2
bar2
foo3
bar3
通过get的前缀特性,我们可以模拟目录结构和选取目录操作
首先,我们创建如下目录结构
/ └─home ├─root ├─mysql └─etcd └─history
#此时value不重要,在本示例中表示目录深度
$ etcdctl put / 0
$ etcdctl put /home 1
$ etcdctl put /home/root 2
$ etcdctl put /home/mysql 2
$ etcdctl put /home/etcd 2
$ etcdctl put /home/etcd/history 3
选取/home下的所有目录
$ etcdctl get /home/ --keys-only --prefix
/home/etcd
/home/etcd/history
/home/mysql
/home/root
#注意 /home 和 /home/ 选取结果的区别
$ etcdctl get /home --keys-only --prefix
/home
/home/etcd
/home/etcd/history
/home/mysql
/home/root
删除单个键,返回删除的数量
$ etcdctl del foo
1
同样,del
命令也可以指定--prefix
参数
$ etcdctl del foo --prefix
3
lease
被用来创建租约,租约可以被绑定到任意数量的key上.创建租约时需要指定租约的TTL.当租约的TTL到期时,租约与绑定了该租约的key将一起过期(删除)
创建20s的租约,并绑定到zoo键上
# 创建一个20s的租约
$ etcdctl lease grant 20
lease 694d6ac0b271ee2e granted with TTL(20s)
# 使用租约的id 进行 put 操作
$ etcdctl put zoo 1 --lease=694d6ac0b271ee2e
OK
#尝试获取zoo
$ etcdctl get zoo
zoo
1
# 20s后get发现 key被删除了
$ etcdctl get zoo
# 空
watch
用来监视key的变化,搭配lease
机制可以实现,服务配置中心常用的服务注册与服务发现
我们打开两个shell窗口,$
提示符后紧跟的数字表示它们执行命令的顺序:
A窗口代表配置中心服务
#1 配置中心开始监视/dc/下的所有节点
# 此处使用--prefix表示监视以/dc/开头的键,执行后会阻塞输出信息:
$ etcdctl watch /dc/ --prefix
# 在执行 #3 操作后屏幕输出
PUT
/dc/beijing1
13557
# 在执行 #5 约60s后屏幕输出
DELETE
/dc/beijing1
B窗口代表/dc/下名为beijing1的数据中心
#2 beijing1的数据中心上线,需要注册名为/dc/beijing1的键,并添加60s的自动续租lease
$ etcdctl lease grant 60
lease 694d6ac0b271ee69 granted with TTL(60s)
#3 将lease绑定到/dc/beijing1上,此处value可以自定义
$ etcdctl put /dc/beijing1 13557 --lease=694d6ac0b271ee69
#4 使用自动续租命令,etcdctl会阻塞并在指定租约到期前自动续租,退出etcdctl后续租停止
$ etcdctl lease keep-alive 694d6ac0b271ee69
lease 694d6ac0b271ee6c keepalived with TTL(60)
lease 694d6ac0b271ee6c keepalived with TTL(60)
...
#5 ctrl+c退出etcdctl,模拟数据中心异常下线
^C
etcd的API有两种调用方式(API 2 和 API 3),在v3版本客户端以前使用http restful的方式调用,v3版本官方只提供了grpc接口,需要自己手动封装一层grpc <==> http的调用.**注意:使用restful保存的数据和grpc保存的数据不是互通的.**也就是说使用restful设置的键值,使用grpc是读取不到的,反之亦然.
etcd的API如同redis一样拥有高度的简洁性,我们通过一个实战就能快速学习到它的使用方式.
完整版代码包括一个简单的服务注册中心,一个服务端示例,一个客户端示例,代码托管在[示例目录](golang example)下,可以从[README](golang example/README.md)了解使用方法
在微服务系统中,每个服务的状态需要在注册中心动态更新,服务注册就是一个微服务上线后通知注册中心自己处于可用状态,同时上报自己的地址、可用API、负载状态等其他信息,供体系中其他服务调用.
//初始化etcd链接
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: timeout,
Username: uname,
Password: pwd,
})
//设置keep秒租约(过期时间)
leaseResp, err := cli.Lease.Grant(context.Background(), keep)
//拿到租约id
leaseID := leaseResp.ID
//设置一个ctx用于取消自动续租,取消时调用cancleFunc()即可
ctx, cancleFunc := context.WithCancel(context.Background())
//开启自动续租,存在开启自动续租前,leaseResp就到期的可能,实际使用可以多加几个判断
keepChan, err := cli.Lease.KeepAlive(ctx, leaseID)
//KeepAlive respond需要丢弃,否则会在标准错误打警告信息
go func() {
for range keepChan {
// eat messages until keep alive channel closes
}
}()
//put 操作,同时附加租约,程序挂掉后续约也会自动停止,正常退出时最好还是调用cancleFunc()
_, err = cli.Put(context.Background(), key, val, clientv3.WithLease(leaseID))
服务发现指的是客户端周期性的向服务注册中心获取某一类服务的所有可用节点,缓存在本地,然后在需要时通过负载均衡策略调用其中的一个可用实例.这也被称为服务发现的客户端模式
由于etcd提供了watch机制,使得服务基于etcd的服务发现更为高效简洁,我们不用手动获取服务节点,而是在启动时告诉服务注册中心我们关心哪一类服务,当这一类服务状态发生变化时,服务注册中心会主动将变化推送到客户端.基于这一点,etcd也成了实现服务发现技术的首选方案
//Get操作 获取某一类服务所有可用节点,同时确定了watch的版本号
rangeResp, err := cli.Get(context.Background(), serverType, clientv3.WithPrefix())
//watch时我们将监听curRevision之后的事件
curRevision := rangeResp.Header.GetRevision()
//监听后续的PUT与DELETE事件,cancel用于取消监听
ctx, cancel := context.WithCancel(context.Background())
//开始watch 以serverType开头的key
watchChan := cli.Watch(ctx, serverType, clientv3.WithPrefix(), clientv3.WithRev(curRevision))
// 首先将curRevision版本时的服务节点列表更新到本地
go func() {
for _, kv := range rangeResp.Kvs {
//kv.Key, kv.Value ...
}
}()
// 其次需要监听后续变化
go func() {
for watchResp := range watchChan {
if watchResp.Canceled || watchResp.Err() != nil {
//被关闭或发生错误
//log.Errorln(...)
break
}
//将所有变化的节点更新到本地
for _, event := range watchResp.Events {
switch event.Type {
case mvccpb.PUT:
if event.IsCreate() {
//节点被创建,意味着服务上线
//kv.Key, kv.Value ...
} else {
//event.IsModify
//节点被修改,意味着服务信息有变更
//kv.Key, kv.Value ...
}
case mvccpb.DELETE:
//节点被删除,意味着服务下线
//kv.Key
}
}
}
}()
程序中我们常需要使用锁机制竞用共享的资源,常见的有互斥锁和读写锁.微服务体系下,服务间竞用共享资源时也需要加锁,这被称为分布式锁,顾名思义它就是可以用于分布式服务的互斥锁
官方提供了golang版本的etcd分布式锁实现,仅需简单的封装即可使用,如果服务在持有锁时挂掉,最多60s后该锁会被释放,服务没有挂掉时可以无限期持有锁
//Lock 获取分布式锁(阻塞超时方式)
//key 锁的key
//timeout 获取锁的最长等待时间,0为永久阻塞等待
//return func():调用可以unlock.
func Lock(key string, timeout time.Duration) (func(), error) {
s, err := concurrency.NewSession(conn.etcd)
if err != nil {
return nil, err
}
m := concurrency.NewMutex(s, key)
var ctx context.Context
if timeout == 0*time.Second {
//阻塞抢锁
ctx = context.Background()
} else {
//超时等待抢锁
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), timeout) //设置超时
defer cancel()
}
if err = m.Lock(ctx); err != nil {
if err == context.DeadlineExceeded {
//抢锁超时...
}
return nil, err
}
//抢锁成功,返回的func()用于解锁操作
return func() {
m.Unlock(context.Background())
}, nil
}
Raft Understandable Distributed Consensus能够带你快速了解Raft协议,它通过动画展示了Raft协议在各种情境下的动作.