本文作者:陈进坚
个人博客:https://jian1098.github.io
CSDN博客:https://blog.csdn.net/c_jian
:https://www.jianshu.com/u/8ba9ac5706b6
联系方式:[email protected]
关于etcd
简介
etcd
是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和发现。
特点
- 完全复制:集群中的每个节点都可以使用完整的存档
- 高可用性:Etcd可用于避免硬件的单点故障或网络问题
- 一致性:每次读取都会返回跨多主机的最新写入
- 简单:包括一个定义良好、面向用户的API(gRPC)
- 安全:实现了带有可选的客户端证书身份验证的自动化TLS
- 快速:每秒10000次写入的基准速度
- 可靠:使用Raft算法实现了强一致、高可用的服务存储目录
集群
etcd 作为一个高可用键值存储系统,天生就是为集群化而设计的。由于 Raft 算法在做决策时需要多数节点的投票,所以 etcd 一般部署集群推荐奇数个节点,推荐的数量为 3、5 或者 7 个节点构成一个集群。
服务发现
服务发现要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。要解决服务发现的问题,需要有下面三大支柱,缺一不可
强一致性、高可用的服务存储目录。基于 Raft 算法的 etcd 就是一个强一致性高可用的服务存储目录。
一种注册服务和监控服务健康状态的机制。用户可以在 etcd 中注册服务,并且对注册的服务设置
key TTL
,定时保持服务的心跳以达到监控健康状态的效果。一种查找和连接服务的机制。通过在 etcd 指定的主题(由服务名称构成的服务目录)下注册的服务也能在对应的主题下查找到。
核心组件
HTTP Server:用于处理用户发送的 API 请求以及其它 etcd 节点的同步与心跳信息请求。
Store:用于处理 etcd 支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是 etcd 对用户提供的大多数 API 功能的具体实现。
Raft:Raft 强一致性算法的具体实现,是 etcd 的核心。
WAL:Write Ahead Log(预写式日志),是 etcd 的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd 就通过 WAL 进行持久化存储。WAL 中,所有的数据提交前都会事先记录日志。Snapshot 是为了防止数据过多而进行的状态快照;Entry 表示存储的具体日志内容。
安装etcd
从https://github.com/etcd-io/etcd/releases
获取最新版本,下载解压得到etcd
以及etcdctl
两个程序(linux和windows相同)。其中etcd
就是运行etcd服务的二进制文件,etcdctl
是官方提供的命令行etcd客户端,使用etcdctl
可以在命令行中访问etcd服务。
为了方便操作可以将两个文件添加软连接到系统环境变量中
ln -fs /root/eosio/2.0/bin/etcd /usr/local/bin/etcd
ln -fs /root/eosio/2.0/bin/etcdctl /usr/local/bin/etcdctl
查看版本
etcd --version
启动etcd
etcd
可选参数:
-name
节点名称,默认是UUID
-data-dir
保存日志和快照的目录,默认为当前工作目录
-addr
公布的ip地址和端口。 默认为127.0.0.1:2379
-bind-addr
用于客户端连接的监听地址,默认为-addr配置
-peers
集群成员逗号分隔的列表,例如 127.0.0.1:2380,127.0.0.1:2381
-peer-addr
集群服务通讯的公布的IP地址,默认为 127.0.0.1:2380.
-peer-bind-addr
集群服务通讯的监听地址,默认为-peer-addr配置
上述配置也可以设置配置文件,默认为etcd目录/etcd.conf
键值库操作
写(put)
etcdctl put name "hello world" //新增和更新都是put
读(get)
etcdctl get name
根据前缀查询
etcdctl get name --prefix //查找前缀为name的
删除(del)
etcdctl del name
事务(txn)
etcd中事务是原子执行的,只支持类似if … then … else …这种表达
//先赋值
etcdctl put user1 bad
//开启事务
etcdctl txn --interactive
compares:
// 输入以下内容,注意=号两边要有空格,输入结束按两次回车
value("user1") = "bad"
//如果 user1 = bad,则执行 get user1
success requests (get, put, del):
get user1
//如果 user1 != bad,则执行 put user1 good
failure requests (get, put, del):
put user1 good
// 运行结果,执行 success
SUCCESS
user1
bad
监听(watch)
// 当 stock1 的数值改变( put 方法)的时候,watch 会收到通知,在这之前进程会阻塞
$ etcdctl watch stock1
// 新打开终端etcd 多台服务器集群
$ export ETCDCTL_API=3
$ etcdctl put stock1 1000
//在watch 终端显示
PUT
stock1
1000
$ etcdctl watch stock --prefix
$ etcdctl put stock1 10
$ etcdctl put stock2 20
租约(lease)
lease 可以设置访问的失效时间
$ etcdctl lease grant 300 //创建一个300秒的租约
# lease 326963a02758b527 granted with TTL(300s)
$ etcdctl put sample value -- lease=326963a02758b527 //写入操作时将租约id为326963a02758b527的租约分配给sample
OK
$ etcdctl get sample
$ etcdctl lease keep-alive 326963a02758b520 //续约
$ etcdctl lease revoke 326963a02758b527 //手动释放租约
lease 326963a02758b527 revoked
# or after 300 seconds //自动释放租约
$ etcdctl get sample
Lease提供了几个功能:
- Grant:分配一个租约。
- Revoke:释放一个租约。
- TimeToLive:获取剩余TTL时间。
- Leases:列举所有etcd中的租约。
- KeepAlive:自动定时的续约某个租约。
- KeepAliveOnce:为某个租约续约一次。
- Close:貌似是关闭当前客户端建立的所有租约。
分布式锁(lock)
分布式锁,一个人操作的时候,另外一个人只能看,不能操作
# 第一终端
$ etcdctl lock mutex1
mutex1/326963a02758b52d
# 第二终端
$ etcdctl lock mutex1
# 当第一个终端结束了,第二个终端会显示
mutex1/326963a02758b53
选举(elect)
选举节点为leader,只有leader节点才有写入的权限,普通节点只有读权限,保证数据一致性;leader节点会定时向普通节点发送心跳,当普通节点收不到心跳时会自动选举新的leader
$ etcdctl elect one p1
one/326963a02758b539
p1
# another client with the same name blocks
$ etcdctl elect one p2
//结束第一终端,第二终端显示
one/326963a02758b53e
p2
集群状态监控(endpoint)
集群健康状态检查
$ etcdctl --write-out=table endpoint status
$ etcdctl endpoint health
快照(snapshot)
用于保存etcd数据库的快照
etcdctl snapshot save my.db
Snapshot saved at my.db
etcdctl --write-out=table snapshot status my.db
集群成员管理(Member)
用于查看、添加,删除,更新成员
export ENDPOINTS=127.0.0.1:2379,127.0.0.1:2479,127.0.0.1:2579 //windows下export换成set
etcdctl member list -w table //成员列表-w可省略
etcdctl --endpoints=$ENDPOINTS member remove b9057cfdc8ff17ce //删除成员
etcdctl --endpoints=$ENDPOINTS member add cd3 --peer-urls=http://127.0.0.1:2180 //添加成员cd3为成员名
启动新节点
etcd --name cd3 --listen-client-urls http://127.0.0.1:2179 --advertise-client-urls http://127.0.0.1:2179 --listen-peer-urls http://127.0.0.1:2180 --initial-advertise-peer-urls http://127.0.0.1:2180 --initial-cluster-state existing --initial-cluster cd2=http://127.0.0.1:2580,cd0=http://127.0.0.1:2380,cd3=http://127.0.0.1:2180,cd1=http://127.0.0.1:2480 --initial-cluster-token etcd-cluster-1
go语言操作etcd
连接
下载驱动包
go get github.com/coreos/etcd/clientv3
连接服务
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
// Endpoints: []string{"localhost:2379", "localhost:22379", "localhost:32379"}
DialTimeout: 5 * time.Second,
})
读写
写
第一个参数是goroutine
的上下文Context
,后面两个参数分别是key和value。
kv := clientv3.NewKV(cli)
putResp, err := kv.Put(context.TODO(),"/test/key1", "Hello etcd!")
// PutResp: &{cluster_id:14841639068965178418 member_id:10276657743932975437 revision:3 raft_term:7 }
普通查询
getResp, err := kv.Get(context.TODO(), "/test/key1")
返回结构体
type RangeResponse struct {
Header *ResponseHeader `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"`
// kvs is the list of key-value pairs matched by the range request.
// kvs is empty when count is requested.
Kvs []*mvccpb.KeyValue `protobuf:"bytes,2,rep,name=kvs" json:"kvs,omitempty"`
// more indicates if there are more keys to return in the requested range.
More bool `protobuf:"varint,3,opt,name=more,proto3" json:"more,omitempty"`
// count is set to the number of keys within the range when requested.
Count int64 `protobuf:"varint,4,opt,name=count,proto3" json:"count,omitempty"`
}
Kvs字段,保存了本次Get查询到的所有k-v对,因为上述例子只Get了一个单key,所以只需要判断一下len(Kvs)是否等于1即可知道key是否存在。
按前缀查询
rangeResp, err := kv.Get(context.TODO(), "/test/", clientv3.WithPrefix())
分页查询
RangeResponse.More
和Count
,当我们使用withLimit()
等选项进行Get
时会发挥作用,相当于翻页查询。
op操作
Op字面意思就是”操作”,Get和Put都属于Op,只是为了简化用户开发而开放的特殊API。
其参数Op是一个抽象的操作,可以是Put/Get/Delete…;而OpResponse是一个抽象的结果,可以是PutResponse/GetResponse…
可以通过Client中定义的一些方法来创建Op:
- func OpDelete(key string, opts …OpOption) Op
- func OpGet(key string, opts …OpOption) Op
- func OpPut(key, val string, opts …OpOption) Op
- func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op
其实和直接调用KV.Put,KV.GET没什么区别。
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: dialTimeout,
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
ops := []clientv3.Op{
clientv3.OpPut("put-key", "123"),
clientv3.OpGet("put-key"),
clientv3.OpPut("put-key", "456")}
for _, op := range ops {
if _, err := cli.Do(context.TODO(), op); err != nil {
log.Fatal(err)
}
}
租约
创建一个租约
grantResp, err := lease.Grant(context.TODO(), 10)
分配租约
kv.Put(context.TODO(), "/test/vanish", "vanish in 10s", clientv3.WithLease(grantResp.ID))
如果在Put之前Lease已经过期了,那么这个Put操作会返回error,此时你需要重新分配Lease
续约
keepResp, err := lease.KeepAliveOnce(context.TODO(), grantResp.ID)
如果在执行之前Lease就已经过期了,那么需要重新分配Lease。etcd并没有提供API来实现原子的Put with Lease,需要我们自己判断err重新分配Lease。
事务
txn := kv.Txn(context.TODO())
kv.Txn(context.TODO()).If(
clientv3.Compare(clientv3.Value(k1), ">", v1),
clientv3.Compare(clientv3.Version(k1), "=", 2)
).Then(
clientv3.OpPut(k2,v2), clentv3.OpPut(k3,v3)
).Else(
clientv3.OpPut(k4,v4), clientv3.OpPut(k5,v5)
).Commit()
类似于clientv3.Value()\用于指定key属性的,有这么几个方法:
- func CreateRevision(key string) Cmp:key=xxx的创建版本必须满足…
- func LeaseValue(key string) Cmp:key=xxx的Lease ID必须满足…
- func ModRevision(key string) Cmp:key=xxx的最后修改版本必须满足…
- func Value(key string) Cmp:key=xxx的创建值必须满足…
- func Version(key string) Cmp:key=xxx的累计更新次数必须满足…
监控
Watch用于监听某个键的变化, Watch
调用后返回一个WatchChan
,它的类型声明如下:
type WatchChan <-chan WatchResponse
type WatchResponse struct {
Header pb.ResponseHeader
Events []*Event
CompactRevision int64
Canceled bool
Created bool
}
参考文章
https://juejin.im/post/5dba5bedf265da4d461eb8ff#heading-3
https://zhuanlan.zhihu.com/p/38300827