0 专辑概述
etcd 是云原生架构中重要的基础组件,由 CNCF 孵化托管。etcd 在微服务和 Kubernates 集群中不仅可以作为服务注册与发现,还可以作为 key-value 存储的中间件。
《彻底搞懂 etcd 系列文章》将会从 etcd 的基本功能实践、API 接口、实现原理、源码分析,以及实现中的踩坑经验等几方面具体展开介绍 etcd。预计会有 20 篇左右的文章,笔者将会每周持续更新,欢迎关注。
1 Etcd API 概述
本文将会开始介绍 etcd3 API 的核心设计,主要针对常见的 API 接口服务。对于理解 etcd 基本思想有很大的帮助。所有 etcd3 API 均在 gRPC 服务中定义,该服务对 etcd 服务器可以理解的远程过程调用(RPC)进行分类。
2 gRPC 服务
发送到etcd服务器的每个API请求都是一个gRPC远程过程调用。etcd3 中的 RPC 接口定义根据功能分类到服务中。
处理 etcd 键值的重要服务包括:
- KV 服务,创建,更新,获取和删除键值对。
- 监视,监视键的更改。
- 租约,消耗客户端保持活动消息的基元。
- 锁,etcd 提供分布式共享锁的支持。
- 选举,暴露客户端选举机制。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICMyYTMvw1dvwlMvwlM3VWaWV2Zh1Wa-cmbw5yakl2aqVWcvxmMvw1N1MjN0QTMtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.png)
2.1 请求和响应
etcd3 中的所有 RPC 都遵循相同的格式。每个 RPC 都有一个函数名,该函数将 NameRequest 作为参数并返回 NameResponse 作为响应。例如,这是 Range RPC 描述:
service KV {
Range(RangeRequest) returns (RangeResponse)
...
}
复制
2.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。
- Revision - 产生响应时键值存储的修订版本号。
- Raft_Term - 产生响应时,成员的 Raft 称谓。
应用服务可以通过 Cluster_ID 和 Member_ID 字段来确保,当前与之通信的正是预期的那个集群或者成员。
应用服务可以使用修订号字段来知悉当前键值存储库最新的修订号。当应用程序指定历史修订版以进行时程查询并希望在请求时知道最新修订版时,此功能特别有用。
应用服务可以使用 Raft_Term 来检测集群何时完成一个新的 leader 选举。
3 键值对服务
3.1 KV service 定义
大多数对etcd的请求通常是键值请求。KV service提供对键值对操作的支持。在 rpc.proto 文件中 KV service 定义如下:
//rpc.proto
service KV {
rpc Range(RangeRequest) returns (RangeResponse) {}
rpc Put(PutRequest) returns (PutResponse) {}
rpc DeleteRange(DeleteRangeRequest) returns (DeleteRangeResponse) {}
rpc Txn(TxnRequest) returns (TxnResponse) {}
rpc Compact(CompactionRequest) returns (CompactionResponse) {}
}
复制
各个函数的介绍如下:
- Range,从键值存储中获取范围内的 key;
- Put,设置给定 key 到键值存储,put 请求增加键值存储的修订版本并在事件历史中生成一个事件;
- DeleteRange,从键值存储中删除给定范围,删除请求增加键值存储的修订版本并在事件历史中为每个被删除的key生成一个删除事件;
- Txn,在单个事务中处理多个请求,一个 txn 请求增加键值存储的修订版本并为每个完成的请求生成带有相同修订版本的事件。不容许在一个txn中多次修改同一个key;
- Compact,压缩在etcd键值存储中的事件历史。键值存储应该定期压缩,否则事件历史会无限制的持续增长。
下面我们具体分析每个接口方法。
3.2 Range方法
Range 方法从键值存储中获取范围内的 key,定义如下:
rpc Range(RangeRequest) returns (RangeResponse) {}
复制
需要注意的是没有操作单个 key 的方法,即使是存取单个 key,也需要使用 Range 方法。请求的消息体 RangeRequest 如下所示:
message RangeRequest {
enum SortOrder {
NONE = 0; // 默认,不排序
ASCEND = 1; // 正序,低的值在前
DESCEND = 2; // 倒序,高的值在前
}
enum SortTarget {
KEY = 0;
VERSION = 1;
CREATE = 2;
MOD = 3;
VALUE = 4;
}
bytes key = 1;
// range_end 是请求范围的上限[key, range_end)
bytes range_end = 2;
// 请求返回的key的数量限制
int64 limit = 3;
int64 revision = 4;
// 指定返回结果的排序顺序
SortOrder sort_order = 5;
// 用于排序的键值字段
SortTarget sort_target = 6;
bool serializable = 7;
// 设置仅返回key而不需要value
bool keys_only = 8;
// 设置仅仅返回范围内key的数量
bool count_only = 9;
}
复制
如上请求结构体的定义中,需要注意 key 是 range 的第一个 key。如果 range_end 没有给定,请求仅查找这个 key。range_end 代表请求的上限,如果 range_end 是 '\0',范围是大于等于 key 的所有key;如果 range_end 比给定的 key 长一个 bit,那么 range 请求获取所有带有前缀(给定的 key)的 key;如果 key 和 range_end 都是'\0',则范围查询返回所有key。revision 修订版本作于 range 键值对存储的时间点。如果 revision 小于或等于零,范围是在最新的键值对存储上。如果修订版本已经被压缩,返回 ErrCompacted 作为应答。serializable 设置 range 请求使用串行化成员本地读。range 请求默认是线性化的,线性化请求相比串行化请求有更高的延迟和低吞吐量,但是反映集群当前的一致性。为了更好的性能,以可能脏读为代价,串行化 range 请求在本地处理,无需和集群中的其他节点达到一致。
应答的消息体 RangeResponse 定义如下:
message RangeResponse {
ResponseHeader header = 1;
// kvs是匹配范围请求的键值对列表
// 当请求数量时是空的
repeated mvccpb.KeyValue kvs = 2;
// more代表在被请求的范围内是否还有更多的 key
bool more = 3;
// 被请求范围内 key 的数量
int64 count = 4;
}
复制
header 就是在前面小结提及的通用响应头。其中 mvccpb.KeyValue 的消息体定义如下:
message KeyValue {
// key 是 bytes 格式的 key。不容许 key 为空
bytes key = 1;
// create_revision 是这个 key 最后创建的修订版本
int64 create_revision = 2;
// mod_revision 是这个 key 最后修改的修订版本
int64 mod_revision = 3;
int64 version = 4;
// value 是 key 持有的值,bytes 格式。
bytes value = 5;
int64 lease = 6;
}
复制
version 是 key 的版本。删除会重置版本为 0,任何 key 的修改会增加它的版本。lease 是附加给 key 的租约 id。当附加的租约过期时,key 将被删除。如果 lease 为 0,则没有租约附加到 key。
3.3 Put 方法
Put 方法,用于存储给定 key 到数据库。Put 方法增加键值存储的修订版本并在事件历史中生成一个事件。
rpc Put(PutRequest) returns (PutResponse) {}
复制
请求的消息体是 PutRequest:
message PutRequest {
// byte数组形式的key,用来放置到键值对存储
bytes key = 1;
// byte 数组形式的 value,在键值对存储中和 key 关联
bytes value = 2;
int64 lease = 3;
bool prev_kv = 4;
}
复制
lease,在键值存储中和 key 关联的租约 id,0 代表没有租约。如果 prev_kv 被设置,etcd 获取改变之前的上一个键值对。上一个键值对将在 put 应答中被返回。
应答的消息体 PutResponse 定义如下:
message PutResponse {
ResponseHeader header = 1;
mvccpb.KeyValue prev_kv = 2;
}
复制
header 代表通用的响应头,如果请求中的 prev_kv 被设置,将会返回上一个键值对。
3.4 DeleteRange 方法
DeleteRange 方法从键值存储中删除给定范围。删除请求增加键值存储的修订版本并在事件历史中为每个被删除的key生成一个删除事件。
rpc DeleteRange(DeleteRangeRequest) returns (DeleteRangeResponse)
复制
请求的消息体 DeleteRangeRequest 定义如下:
message DeleteRangeRequest {
bytes key = 1;
bytes range_end = 2;
bool prev_kv = 3;
}
复制
key 代表要删除的范围的开始。range_end 是要删除范围[key, range_end)的最后一个key。如果 range_end 没有给定,范围定义为仅包含key参数;如果 range_end 是 '\0', 范围是所有大于等于参数key的所有key。如果 prev_kv 被设置,etcd 获取删除之前的上一个键值对。上一个键值对将在 delete 应答中被返回。应答的消息体 DeleteRangeResponse 定义如下:
message DeleteRangeResponse {
ResponseHeader header = 1;
// 被范围删除请求删除的key的数量
int64 deleted = 2;
repeated mvccpb.KeyValue prev_kvs = 3;
}
复制
如果请求中的 prev_kv 被设置,将会返回上一个键值对。
4 小结
本篇主要介绍了 Etcd API 中涉及的 KV 服务 和 gRPC 请求响应相关的内容,了解这些是我们学习 Etcd RPC 调用的重要前提。下面的文章我们将继续介绍 etcd 中这几个重要的服务和接口。
参考
etcd docs