1. Client 定义
Client
定义如下:
type Client struct {
Cluster
KV
Lease
Watcher
Auth
Maintenance
// 认证的用户名
Username string
// 认证的密码
Password string
}
这里显示的都是可导出的模块结构字段,代表了客户端能够使用的几大核心模块,具体功能介绍如下。
Cluster
:向集群里增加etcd
服务端节点之类,属于管理员操作;KV
:我们主要使用的功能,即操作K-V
;Lease
:租约相关操作,比如申请一个TTL=10
秒的租约;Watcher
:观察订阅,从而监听最新的数据变化;Auth
:管理etcd
的用户和权限,属于管理员操作;Maintenance
:维护etcd
,比如主动迁移etcd
的leader
节点,属于管理员操作;
2. gRPC 服务
etcd v3
的通信基于 gRPC
,proto
文件是定义服务端和客户端通信接口的标准。包括:
- 客户端该传什么样的参数
- 服务端该返回什么参数
- 客户端该怎么调用
- 是阻塞还是非阻塞
- 是同步还是异步
gRPC
推荐使用 proto3
消息格式,proto3
是原有 Protocol Buffer 2
(被称为 proto2
)的升级版本,删除了一部分特性,优化了对移动设备的支持。
发送到 etcd
服务器的每个 API
请求都是一个 gRPC
远程过程调用。etcd
中的 RPC
接口定义根据功能分类到服务中。
处理 etcd
键值的重要服务包括:
KV Service
:创建、更新、获取和删除键值对;Watch Service
:监视键的更改;Lease Service
:实现键值对过期,客户端用来续租、保持心跳;Lock Service
:etcd
提供分布式共享锁的支持;Election Service
:暴露客户端选举机制;
3. 请求和响应
3.1 请求
etcd3
中的所有 RPC
都遵循相同的格式。每个 RPC
都有一个函数名,该函数将 NameRequest
作为参数并返回 NameResponse
作为响应。例如,这是 Range RPC
描述:
service KV {
Range(RangeRequest) returns (RangeResponse)
...
}
3.2 响应头
etcd API
的所有响应都有一个附加的响应标头,其中包括响应的集群元数据:
message ResponseHeader {
uint64 cluster_id = 1;
uint64 member_id = 2;
int64 revision = 3;
uint64 raft_term = 4;
}
其中:
Cluster_ID
:产生响应的集群的ID
;Member_ID
:产生响应的成员的 ID;
应用服务可以通过 Cluster_ID
和 Member_ID
字段来确保当前与之通信的正是预期的那个集群或者成员。
Revision
:产生响应时键值存储的修订版本号;
应用服务可以使用修订号字段来获得当前键值存储库最新的修订号。应用程序指定历史修订版以进行查询,如果希望在请求时知道最新修订版,此功能特别有用。
Raft_Term
:产生响应时,成员的Raft
称谓。
应用服务可以使用 Raft_Term
来检测集群何时完成一个新的 leader
选举。
4. etcd clientv3 客户端
4.1 初始化
我们根据指定的 etcd
节点,建立客户端与 etcd
集群的连接:
config := clientv3.Config{
Endpoints:[]string{"localhost:2379", "localhost:2379"},
DialTimeout: 5 * time.Second,
}
client, err := clientv3.New(config)
// etcd clientv3 >= v3.2.10, grpc/grpc-go >= v1.7.3
if client == nil || err == context.DeadlineExceeded {
// handle errors
fmt.Println(err)
panic("invalid connection!")
}
// 客户端断开连接
defer client.Close()
如上的代码实例化了一个 client
,这里需要传入两个参数。
-
Endpoints
:etcd
的多个节点服务地址; -
DialTimeout
:创建client
的首次连接超时时间,这里传了 5 秒,如果 5 秒都没有连接成功就会返回err
。
需要注意的是,一旦 client
创建成功,我们就不用再关心后续底层连接的状态了,client
内部会重连。
4.2 KV 存储
KV
对象的实例获取通过如下的方式:
kv := clientv3.NewKV(client)
KV
接口的具体定义:
type KV interface {
Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
// 检索 keys
Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
// 删除 key,可以使用 WithRange(end), [key, end) 的方式
Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)
// 压缩给定版本之前的 KV 历史
Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)
// 指定某种没有事务的操作
Do(ctx context.Context, op Op) (OpResponse, error)
// Txn 创建一个事务
Txn(ctx context.Context) Txn
}
从 KV
对象的定义我们可知,它就是一个接口对象,包含以下几个主要的 KV
操作方法。
4.2.1 Put
Put
的定义如下:
Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
其中的参数
ctx
:Context
包对象,用来跟踪上下文,比如超时控制;key
:存储对象的key
;val
:存储对象的value
;opts
:可变参数,额外选项;
使用示例:
putResp, err := kv.Put(context.TODO(),"aa", "hello-world!")
4.2.2 Get
Get
的定义如下:
Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
OpOption
为可选的函数传参:
- 传参为
WithRange(end)
时,Get
将返回[key,end)
范围内的键; - 传参为
WithFromKey()
时,Get
返回大于或等于key
的键; - 当通过
rev> 0
传递WithRev(rev)
时,Get
查询给定修订版本的键; - 如果压缩了所查找的修订版本,则返回请求失败,并显示
ErrCompacted
; - 传递
WithLimit(limit)
时,返回的key
数量受limit
限制; - 传参为
WithSort
时,将对键进行排序;
对应的使用方法如下:
getResp, err := kv.Get(context.TODO(), "aa")
从上面可以看出,Put
返回 PutResponse
,Get
返回 GetResponse
。注意:不同的 KV
操作对应不同的 Response
结构,定义如下:
type (
CompactResponse pb.CompactionResponse
PutResponse pb.PutResponse
GetResponse pb.RangeResponse
DeleteResponse pb.DeleteRangeResponse
TxnResponse pb.TxnResponse
)
下面我们分别来看一看 PutResponse
和 GetResponse
映射的 RangeResponse
结构的定义:
type PutResponse struct {
Header *ResponseHeader `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"`
// 请求中如有 prev_kv,响应时也会携带 prev_kv
PrevKv *mvccpb.KeyValue `protobuf:"bytes,2,opt,name=prev_kv,json=prevKv" json:"prev_kv,omitempty"`
}
//Header 里保存的主要是本次更新的 revision 信息
type RangeResponse struct {
Header *ResponseHeader `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"`
// kvs 是一个匹配 range 请求的键值对列表
Kvs []*mvccpb.KeyValue `protobuf:"bytes,2,rep,name=kvs" json:"kvs,omitempty"`
// more 用以分页
More bool `protobuf:"varint,3,opt,name=more,proto3" json:"more,omitempty"`
// count 表示 range 的键值对数量
Count int64 `protobuf:"varint,4,opt,name=count,proto3" json:"count,omitempty"`
}
Kvs
字段,保存了本次 Get
查询到的所有 KV
对,我们继续看一下 mvccpb.KeyValue
对象的定义:
type KeyValue struct {
Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
// create_revision 是当前 key 的最后创建版本
CreateRevision int64 `protobuf:"varint,2,opt,name=create_revision,json=createRevision,proto3" json:"create_revision,omitempty"`
// mod_revision 是指当前 key 的最新修订版本
ModRevision int64 `protobuf:"varint,3,opt,name=mod_revision,json=modRevision,proto3" json:"mod_revision,omitempty"`
// key 的版本,每次更新都会增加版本号
Version int64 `protobuf:"varint,4,opt,name=version,proto3" json:"version,omitempty"`
Value []byte `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"`
// 绑定了 key 的租期 Id,当 lease 为 0 ,则表明没有绑定 key;租期过期,则会删除 key
Lease int64 `protobuf:"varint,6,opt,name=lease,proto3" json:"lease,omitempty"`
}
至于 RangeResponse.More
和 Count
,当我们使用 withLimit()
选项进行 Get
时会发挥作用,相当于分页查询。
通过一个特别的 Get
选项,获取 aa
目录下的所有子目录:
rangeResp, err := kv.Get(context.TODO(), "/aa", clientv3.WithPrefix())
WithPrefix()
用于查找以 /aa
为前缀的所有 key
,因此可以模拟出查找子目录的效果。我们知道 etcd
是一个有序的 KV
存储,因此 /aa
为前缀的 key
总是顺序排列在一起。
WithPrefix
实际上会转化为范围查询,它根据前缀 /aa
生成了一个 key range
,[“/aa/”, “/aa0”)
,这是因为比 /
大的字符是 0
,所以以 /aa0
作为范围的末尾,就可以扫描到所有的 /aa/
打头的 key
了。
4.3 事务 Txn
Txn
方法在单个事务中处理多个请求。Txn
请求增加键值存储的修订版本,并为每个完成的请求生成带有相同修订版本的事件,etcd
不容许在一个 Txn
中多次修改同一个 key
。
Txn
接口定义如下:
rpc Txn(TxnRequest) returns (TxnResponse) {}
4.4 Compact
Compact
方法压缩 etcd
键值对存储中的事件历史。键值对存储应该定期压缩,否则事件历史会无限制地持续增长。
Compact
接口定义如下:
rpc Compact(CompactionRequest) returns (CompactionResponse) {}
请求的消息体是 CompactionRequest
, CompactionRequest
压缩键值对存储到给定修订版本,所有修订版本比压缩修订版本小的键都将被删除。
4.5 Watch
Watch API
提供了一个基于事件的接口,用于异步监视键的更改。etcd
监视程序通过给定的修订版本(当前版本或历史版本)持续监视 key
更改,并将 key
更新流回客户端。
在 rpc.proto
中 Watch Service
定义如下:
service Watch {
rpc Watch(stream WatchRequest) returns (stream WatchResponse) {}
}
Watch
观察将要发生或者已经发生的事件。输入和输出都是流,输入流用于创建和取消观察,而输出流发送事件。一个观察 RPC
可以一次性在多个 key
范围上观察,并为多个观察流化事件。整个事件历史可以从最后压缩修订版本开始观察。Watch Service
只有一个 Watch
方法。
4.6 Lease Service
Lease Service
提供租约的支持。Lease
是一种检测客户端存活状况的机制。集群授予客户端具有生存时间的租约。如果 etcd
集群在给定的 TTL
时间内未收到 keepAlive
,则租约到期。
为了将租约绑定到键值存储中,每个 key
最多可以附加一个租约。当租约到期或被撤销时,该租约依附的所有 key
都将被删除,每个过期的密钥都会在事件历史记录中生成一个删除事件。
在 rpc.proto
中 Lease Service
定义的接口如下:
service Lease {
rpc LeaseGrant(LeaseGrantRequest) returns (LeaseGrantResponse) {}
rpc LeaseRevoke(LeaseRevokeRequest) returns (LeaseRevokeResponse) {}
rpc LeaseKeepAlive(stream LeaseKeepAliveRequest) returns (stream LeaseKeepAliveResponse) {}
rpc LeaseTimeToLive(LeaseTimeToLiveRequest) returns (LeaseTimeToLiveResponse) {}
}
其中:
LeaseGrant
创建一个租约;LeaseRevoke
撤销一个租约;LeaseKeepAlive
用于维持租约;LeaseTimeToLive
获取租约信息;
4.7 Lock Service
Lock Service
提供分布式共享锁的支持。Lock Service
以 gRPC
接口的方式暴露客户端锁机制。
在 v3lock.proto
中 Lock Service
定义如下:
service Lock {
rpc Lock(LockRequest) returns (LockResponse) {}
rpc Unlock(UnlockRequest) returns (UnlockResponse) {}
}
其中:
Lock
方法,在给定命令锁上获得分布式共享锁;Unlock
使用Lock
返回的key
并释放对锁的持有;
参考:
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=613#/detail/pc?id=6403