// Client provides and manages an etcd v3 client session.
type Client struct {
Cluster // 向集群里增加 etcd 服务端节点之类,属于管理员操作。
KV //我们主要使用的功能,即操作 K-V。
Lease //租约相关操作,比如申请一个 TTL=10 秒的租约。
Watcher //观察订阅,从而监听最新的数据变化。
Auth //管理 etcd 的用户和权限,属于管理员操作。
Maintenance //维护 etcd,比如主动迁移 etcd 的 leader 节点,属于管理员操作。
conn *grpc.ClientConn
cfg Config
creds grpccredentials.TransportCredentials
resolver *resolver.EtcdManualResolver
mu *sync.RWMutex
ctx context.Context
cancel context.CancelFunc
// Username is a user name for authentication.
Username string
// Password is a password for authentication.
Password string
authTokenBundle credentials.Bundle
callOpts []grpc.CallOption
lgMu *sync.RWMutex
lg *zap.Logger
}
etcd v3 的通信基于 gRPC,proto 文件是定义服务端和客户端通信接口的标准。包括:
客户端该传什么样的参数
服务端该返回什么参数
客户端该怎么调用
是阻塞还是非阻塞
是同步还是异步
gRPC 推荐使用 proto3 消息格式,在进行核心 API 的学习之前,我们需要对 proto3 的基本语法有初步的了解。proto3 是原有 Protocol Buffer 2(被称为 proto2)的升级版本,删除了一部分特性,优化了对移动设备的支持。
发送到 etcd 服务器的每个 API 请求都是一个 gRPC 远程过程调用。etcd 中的 RPC 接口定义根据功能分类到服务中。
处理 etcd 键值的重要服务包括:
KV Service,创建、更新、获取和删除键值对。
Watch Service,监视键的更改。
Lease Service,实现键值对过期,客户端用来续租、保持心跳。
Lock Service,etcd 提供分布式共享锁的支持。
Election Service,暴露客户端选举机制。
etcd3 中的所有 RPC 都遵循相同的格式。每个 RPC 都有一个函数名,该函数将 NameRequest 作为参数并返回 NameResponse 作为响应。例如,这是 Range RPC 描述:
service KV {
Range(RangeRequest) returns (RangeResponse)
...
}
etcd API 的所有响应都有一个附加的响应标头,其中包括响应的集群元数据:
type ResponseHeader struct {
// cluster_id is the ID of the cluster which sent the response.
//产生响应的集群的 ID。
ClusterId uint64 `protobuf:"varint,1,opt,name=cluster_id,json=clusterId,proto3" json:"cluster_id,omitempty"`
// member_id is the ID of the member which sent the response.
//产生响应的成员的 ID。
//应用服务可以通过 Cluster_ID 和 Member_ID 字段来确保,当前与之通信的正是预期的那个集群或者成
// 员。
MemberId uint64 `protobuf:"varint,2,opt,name=member_id,json=memberId,proto3" json:"member_id,omitempty"`
// revision is the key-value store revision when the request was applied.
// For watch progress responses, the header.revision indicates progress. All future events
// recieved in this stream are guaranteed to have a higher revision number than the
// header.revision number.
//产生响应时键值存储的修订版本号。
//应用服务可以使用修订号字段来获得当前键值存储库最新的修订号。应用程序指定历史修订版以进行查询,如果希望在请求时知道最新修订版,此功能特别有用。
Revision int64 `protobuf:"varint,3,opt,name=revision,proto3" json:"revision,omitempty"`
// raft_term is the raft term when the request was applied.
//产生响应时,成员的 Raft 称谓。
//应用服务可以使用 Raft_Term 来检测集群何时完成一个新的 leader 选举。
RaftTerm uint64 `protobuf:"varint,4,opt,name=raft_term,json=raftTerm,proto3" json:"raft_term,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
etcd 客户端 clientv3 接入的示例将会以 Go 客户端为主,你需要准备好基本的开发环境。
首先是 etcd clientv3 的初始化,我们根据指定的 etcd 节点,建立客户端与 etcd 集群的连接:
client/v3/doc.go
// expect dial time-out on ipv4 blackhole
_, err := clientv3.New(clientv3.Config{
Endpoints: []string{"http://254.0.0.1:12345"},
DialTimeout: 2 * time.Second,
})
如上的代码实例化了一个 client,这里需要传入两个参数。
Endpoints:etcd 的多个节点服务地址,因为我是单点本机测试,所以只传 1 个。
DialTimeout:创建 client 的首次连接超时,这里传了 5 秒,如果 5 秒都没有连接成功就会返回 err。需要注意的是,一旦 client 创建成功,我们就不用再关心后续底层连接的状态了,client 内部会重连。
// 使用client v3 测试连接到etcd集群
func TestClient(t *testing.T) {
var (
config Config
client *Client
err error
)
// 客户端配置
config = Config{
// 节点配置
Endpoints: []string{"192.168.70.100:22379"},
DialTimeout: 5 * time.Second,
}
// 建立连接
if client, err = New(config); err != nil {
fmt.Println(err)
} else {
// 输出集群信息
fmt.Println(client.Cluster.MemberList(context.TODO()))
}
client.Close()
}
etcd 中几个重要的服务和接口:
KV 对象的实例获取通过如下的方式:
kv := clientev3.NewKV(client)
KV 接口的具体定义:
type KV interface {
// Put puts a key-value pair into etcd.
// Note that key,value can be plain bytes array and string is
// an immutable representation of that bytes array.
// To get a string of bytes, do string([]byte{0x10, 0x20}).
Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
// Get retrieves keys.
// By default, Get will return the value for "key", if any.
// When passed WithRange(end), Get will return the keys in the range [key, end).
// When passed WithFromKey(), Get returns keys greater than or equal to key.
// When passed WithRev(rev) with rev > 0, Get retrieves keys at the given revision;
// if the required revision is compacted, the request will fail with ErrCompacted .
// When passed WithLimit(limit), the number of returned keys is bounded by limit.
// When passed WithSort(), the keys will be sorted.
Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
// Delete deletes a key, or optionally using WithRange(end), [key, end).
Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)
// Compact compacts etcd KV history before the given rev.
Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)
// Do applies a single Op on KV without a transaction.
// Do is useful when creating arbitrary operations to be issued at a
// later time; the user can range over the operations, calling Do to
// execute them. Get/Put/Delete, on the other hand, are best suited
// for when the operation should be issued at the time of declaration.
Do(ctx context.Context, op Op) (OpResponse, error)
// Txn creates a transaction.
Txn(ctx context.Context) Txn
}
从 KV 对象的定义我们可知,它就是一个接口对象,包含以下几个主要的 KV 操作方法。
Put 的定义如下:
Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
其中的参数
ctx:Context 包对象,用来跟踪上下文,比如超时控制。
key:存储对象的 key。
val:存储对象的 value。
opts:可变参数,额外选项。
Put 将一个键值对放入 etcd 中。请注意,键值可以是纯字节数组,字符串是该字节数组的不可变表示形式。要获取字节字符串,请执行string([] byte {0x10,0x20})
。
Put 的使用方法如下所示:
putResp, err := kv.Put(context.TODO(),"aa", "hello-world!")
现在可以对存储的数据进行取值了。默认情况下,Get 将返回“key”对应的值:
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,proto3" json:"header,omitempty"`
// if prev_kv is set in the request, the previous key-value pair will be returned.
//请求中如有 prev_kv,响应时也会携带 prev_kv
PrevKv *mvccpb.KeyValue `protobuf:"bytes,2,opt,name=prev_kv,json=prevKv,proto3" json:"prev_kv,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
type RangeResponse struct {
Header *ResponseHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
// kvs is the list of key-value pairs matched by the range request.
// kvs is empty when count is requested.
kvs 是一个匹配 range 请求的键值对列表
Kvs []*mvccpb.KeyValue `protobuf:"bytes,2,rep,name=kvs,proto3" 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"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
KVS 字段,保存了本次 Get 查询到的所有 KV 对,我们继续看一下 mvccpb.KeyValue 对象的定义:
type KeyValue struct {
// key is the key in bytes. An empty key is not allowed.
Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
// create_revision is the revision of last creation on this key.
// create_revision 是当前 key 的最后创建版本
CreateRevision int64 `protobuf:"varint,2,opt,name=create_revision,json=createRevision,proto3" json:"create_revision,omitempty"`
// mod_revision is the revision of last modification on this key.
// mod_revision 是指当前 key 的最新修订版本
ModRevision int64 `protobuf:"varint,3,opt,name=mod_revision,json=modRevision,proto3" json:"mod_revision,omitempty"`
// version is the version of the key. A deletion resets
// the version to zero and any modification of the key
// increases its version.
// key 的版本,每次更新都会增加版本号
Version int64 `protobuf:"varint,4,opt,name=version,proto3" json:"version,omitempty"`
// value is the value held by the key, in bytes.
Value []byte `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"`
// lease is the ID of the lease that attached to key.
// When the attached lease expires, the key will be deleted.
// If lease is 0, then no lease is attached to the key.
// 绑定了 key 的租期 Id,当 lease 为 0 ,则表明没有绑定 key;租期过期,则会删除 key
Lease int64 `protobuf:"varint,6,opt,name=lease,proto3" json:"lease,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
至于 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 了。
// 使用client v3 测试连接到etcd集群
func TestClient(t *testing.T) {
rootContext := context.Background()
var (
config Config
client *Client
err error
)
// 客户端配置
config = Config{
// 节点配置
Endpoints: []string{"192.168.70.100:22379"},
DialTimeout: 5 * time.Second,
}
// 建立连接
if client, err = New(config); err != nil {
fmt.Println(err)
} else {
// 输出集群信息
fmt.Println(client.Cluster.MemberList(context.TODO()))
}
defer client.Close()
kv := NewKV(client)
//设置值
str := time.Now().String()
fmt.Println("key aa new value is:", str)
ctx1, _ := context.WithTimeout(rootContext, 2*time.Second)
_, err = kv.Put(ctx1, "aa", str)
if err!=nil{
fmt.Println(err)
return
}
//获取值
ctx, _ := context.WithTimeout(rootContext, 2*time.Second)
resp, err := kv.Get(ctx, "aa")
if err != nil {
fmt.Println(err)
return
}
kvs := resp.Kvs
if len(kvs) > 0 {
fmt.Println("key aa last value is:", string(kvs[0].Value))
} else {
fmt.Println("key aa value is empty")
}
//删除值
delResp,err:=kv.Delete(ctx,"aa")
if err!=nil{
fmt.Println(err)
}
fmt.Println("delete key:cc:",delResp.Deleted>0)
}
输出结果:
其他常用的接口还有 Txn、Compact、Watch、Lease、Lock 等。我们依次看看这些接口的定义。
事务 Txn
Txn 方法在单个事务中处理多个请求。Txn 请求增加键值存储的修订版本,并为每个完成的请求生成带有相同修订版本的事件,etcd 不容许在一个 Txn 中多次修改同一个 key。Txn 接口定义如下:
rpc Txn(TxnRequest) returns (TxnResponse) {}
Compact
Compact 方法压缩 etcd 键值对存储中的事件历史。键值对存储应该定期压缩,否则事件历史会无限制地持续增长。Compact 接口定义如下:
rpc Compact(CompactionRequest) returns (CompactionResponse) {}
请求的消息体是 CompactionRequest, CompactionRequest 压缩键值对存储到给定修订版本,所有修订版本比压缩修订版本小的键都将被删除。
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 方法。
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,获取租约信息。
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 并释放对锁的持有。