ETCD API V3
ETCD V3 vs V2
etcd V3做出的改进和优化。
- 使用gRPC+protobuf取代HTTP+JSON通信,提高通信效率;另外通过gRPC gateway来继续保持对HTTPJSON接口的支持
- 使用更加轻量级的基于租约的key自动过期机制,取代了基于TTL的 key的自动过期时间
- 观察者(watcher)机制进行了重新设计。etcd v2的观察者机制是基于HTTP长连接的时间驱动机制;而etcd v3是基于HTTP/2 的server push,并且对事件进行了多路复用(multiplexing)优化
- 数据模型发生了较大的改变。v2是一个简单的key-value的内存数据库,而v3是支持事务和多版本并发控制的磁盘数据库。v2数据不直接落盘,落盘的日志和快照文件只是数据的中间格式而非最终格式,系统通过回放日志文件来构建数据的最终形式。v3罗盘的是数据的最终形态,日志和快照的主要作用是进行分布式的复制。
gRPC
gRPC是Google开源的一个高性能,跨语言的RPC框架,基于HTTP/2协议实现。使用protobuf作为序列化和反序列化协议,基于protobuf来声明数据模型和RPC接口服务
序列化与反序列化
protobuf的效率远高于JSON。v3的gRPC序列化和反序列化速度是v2的两倍多
减少TCP连接
v2通信协议使用HTTP1.1,gRPC支持HTTP/2,HTTP/2对HTTP通信进行了多路复用,可共享一个TCP连接。因此v3大大减少了客户端和服务端的连接数,一个客户端只需要与服务端建立一个TCP连接即可。v2,一个客户端需要与服务端建立多个TCP连接,每个HTTP请求都需要建立一个连接
租约机制
v2 是在每个key上设置TTL,v3是租约机制,然后key绑定租约,如果需要更新key的过期时间,可以直接更新租约(lease),多个key绑定到一个租约上,需要更新每个key的过期时间时,v3减少了客户端请求数量。
观察者模式
v2通过索引的方式支持连续的watch,客户端每次watch都可以带上之前的key的索引,然后服务端会返回比上一次watch更新的数据。然而v2的服务端对于每个客户端的每个watch请求都维护一个HTTP长连接,如果数钱个客户端watch了数千个key,那么v2服务端的socket和内存等资源会很快耗尽。
v3改进方法是对于同一个客户端的watch请求进行了多路复用,同一个客户端只需要与服务端维护一个TCP连接,减轻了服务端的压力。
数据存储模型
v2是一个key-value数据库,只保存key的最新的value,之前的value直接覆盖,但是会维护1000个所有key的变更记录,如果在短时间频繁写操作,那么变更记录会很快超过1000,如果watch过慢就会无法得到之前的变更,带来后果就是watch丢失事件。
v3为了支持多记录,抛弃不稳定的“滑动窗口”的设计,通过引入MVCC,采用了从历史记录为主索引的存储接口,保存了key的所有历史记录。
由于v3实现了MVCC,保存了每个key-value pair的历史版本,数据大了很多,不能将整个数据库放内存了,因此v3摒弃了内存数据库,转为磁盘数据库,整个数据库都存储在磁盘上,底层的存储引擎使用的是BoltDB
mini事务
v2只支持对单个key的CAS(Compare-And-Swap)操作,会对key的版本号或值比较,然后进行操作。
v3引入迷你事务,可以包含一系列的条件语句,只有在满足条件时事务才会执行成功
快照
v2与其他类似的开源一致性系统一样,最多只能存储十万级别的key,主要原因是一致性系统都采用了基于log的复制,log不能无限增长,所以某一时刻系统需要做一个完整的快照,并且将快照存储到磁盘。在存储快照后将之前的log丢弃。
v3通过Raft和存储系统的重构,支持增量快照和传输相对较大的快照,v3可以存储百万到千万级别的key
大规模watch
v2中每一个watcher都会占用一个TCP资源和一个goroutine资源,大概小号30~40KB。
v3利用TCP多路复用,只需要一个TCP连接,不同的watcher只消耗一个goroutine,减轻了etcd服务器的资源消耗。
gRPC服务
etcd v3的每个API请求均为gRPC远程调用,gRPC的proto文件包含了service,method,message三个部分,例如:
service KV {
rpc Range(RangeRequest) returns (RangeResponse) {
option (google.api.http) = {
post: "/v3/kv/range"
body: "*"
};
}
}
message RangeRequest {
enum SortOrder {
NONE = 0; // default, no sorting
ASCEND = 1; // lowest target value first
DESCEND = 2; // highest target value first
}
enum SortTarget {
KEY = 0;
VERSION = 1;
CREATE = 2;
MOD = 3;
VALUE = 4;
}
bytes key = 1;
bytes range_end = 2;
int64 limit = 3;
int64 revision = 4;
SortOrder sort_order = 5;
SortTarget sort_target = 6;
bool serializable = 7;
bool keys_only = 8;
bool count_only = 9;
int64 min_mod_revision = 10;
int64 max_mod_revision = 11;
int64 min_create_revision = 12;
int64 max_create_revision = 13;
}
etcd v3所一定不同服务,包含了键值(KV),集群(Cluster),维护(Maintenance),认证/鉴权(Auth),观察(Watch)与租约(Lease)六大类。(具体可见源码:https://github.com/etcd-io/etcd/blob/main/api/etcdserverpb/rpc.proto)
- KV:创建,更新,获取,删除键值对
- Cluster:集群中增加或删除成员,更新成员配置,得到集群包含所有成员的列表
- Maintenance:启动或停止警报,查询警报,查询成员状态,为成员后端的数据库整理碎片,在client的流中发送某成员的完整后端快照等
- Auth:增删用户,更改用户密码,查询用户信息,获取用户列表,授予撤销用户角色,增删查角色,角色授予撤销某个特定的key
- Watch:监听key的变化
- Lease:消耗客户端keep-alive消息的原语
从proto的响应中,可见都包含了
message ResponseHeader {
uint64 cluster_id = 1;
// 生成该响应的cluster ID
uint64 member_id = 2;
// 生成该响应的memberID
int64 revision = 3;
// 生成该响应的键值存储的版本
uint64 raft_term = 4;
// 生成该响应的member所处的Raft协议任期(term)
}
客户端通过检查cluster_id或member_id字段的值来确认是否正在与目标集群或节点通信。
KV API
Range
请求参数:
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;
// 表示bytes类型,不允许为空,如果range_end没有,则表示只是为了查询key
bytes range_end = 2;
// [key,range_end) ,当range_end为'\0'表示大于key的所有键,如果key和range_end都是'\0',请求返回所有的键,如果range_end是键加1(例如:key=“aa”,range_end=“ab”),则表示以这个key为前缀的所有key
int64 limit = 3;
// 限制返回的key的数量,如果limit=0,则表示不限制
int64 revision = 4;
// 如果小于等于0,表示最新的键值存储,如果被压缩,则响应ErrCompacted
SortOrder sort_order = 5;
// 表示排序顺序
SortTarget sort_target = 6;
// 排序方式
bool serializable = 7;
// 设置使用可序列化的成员本地读取,默认是可线性化的,可线性化更高延迟更低吞吐量,但是反应的是整个集群的共识,为了更好的性能,以换取可能陈旧的读取,可串行化的范围请求在本地提供服务,不需要与其他节点达成共识
bool keys_only = 8;
// 被设置时,只返回键不返回值
bool count_only = 9;
// 被设置时,只返回键的数量
int64 min_mod_revision = 10;
// 返回key的mod_revision的下界,小于这个的会被过滤掉
int64 max_mod_revision = 11;
// 返回key的mod_revision的上界,大于这个的会被过滤掉
int64 min_create_revision = 12;
// 返回key的create_revision的下界,小于这个的会被过滤掉
int64 max_create_revision = 13;
// 返回key的create_revision的上界,大于这个的会被过滤掉
}
响应参数:
message RangeResponse {
ResponseHeader header = 1;
repeated mvccpb.KeyValue kvs = 2;
// 表示符合range请求的key-value对列表,如果count_only设置为true,则kvs就为空
bool more = 3;
//表明是否还有更多的key没有在响应结果中返回
int64 count = 4;
// 满足reamge reqiest的key的总数
}
message KeyValue {
bytes key = 1;
int64 create_revision = 2;
int64 mod_revision = 3;
int64 version = 4;
bytes value = 5;
int64 lease = 6;
}
Put
请求参数:
message PutRequest {
bytes key = 1;
// 表示写入的key
bytes value = 2;
// 表示写入的value
int64 lease = 3;
// 表示关联在key上的租约ID,如果lease的值为0,代表没有租约
bool prev_kv = 4;
// 设置后,会返回该PUT请求修改前的key=value对数据
bool ignore_value = 5;
// 设置后,值更新key,但不修改当前的value,如果key不存在,则返回一个错误
bool ignore_lease = 6;
// 设置后,PUT操作更新key时不改变当前的租约,如果key不存在,则返回一个错误
}
响应参数:
message PutResponse {
ResponseHeader header = 1;
mvccpb.KeyValue prev_kv = 2;
}
DeleteRange
请求参数
message DeleteRangeRequest {
bytes key = 1;
// 表示删除范围的左端
bytes range_end = 2;
// 表示删除范围的右端, [key,range_end) ,当range_end没有,则表示只是删除这个key;如果为'\0'表示删除大于这个key的所有键;如果range_end是键加1(例如:key=“aa”,range_end=“ab”),则表示以这个key为前缀的所有key
bool prev_kv = 3;
// 如果被设置成true,则会在response中返回所有被删除的键值对。
}
响应参数
message DeleteRangeResponse {
ResponseHeader header = 1;
int64 deleted = 2;
// 被删除的key的数目
repeated mvccpb.KeyValue prev_kvs = 3;
// 所有被删除的键值对列表
}
Txn
事务就是一个原子的,针对key-value存储操作的If/Then/Else结构。事务可以用来保护key不受其他并发更新操作的修改,也可以构造CAS(Compare And Swap)操作,并以此作为更高层次并发控制的基础。
一次事务,后台存储的revision只增长一次,而且该事务所有操作产生的事件都拥有同样的revision。
在一个事务中多次修改同一个key是被禁止的。
请求参数
message TxnRequest {
repeated Compare compare = 1;// 比较
repeated RequestOp success = 2; // 待处理的请求列表,如果所有的比较测试的结果均为真,则执行
repeated RequestOp failure = 3;// 待处理的请求列表,只要任意一个比较测试的结果返回为假,则执行
}
message Compare {
enum CompareResult {
EQUAL = 0;
GREATER = 1;
LESS = 2;
NOT_EQUAL = 3;
}
enum CompareTarget {
VERSION = 0;
CREATE = 1;
MOD = 2;
VALUE = 3;
LEASE = 4;
}
CompareResult result = 1; // 该逻辑比较操作的类型,例如,等于,小于,小于,不等于
CompareTarget target = 2; // 带比较的key-value字段。可以是key的version,create revision,mod revision,value。
bytes key = 3; // 待比较的key
oneof target_union { // 用户比较的用户相关的数据
int64 version = 4;
int64 create_revision = 5;
int64 mod_revision = 6;
bytes value = 7;
int64 lease = 8;
}
bytes range_end = 64; // 给定目标与[key,range_end)中的所有键进行比较
}
message RequestOp {
oneof request {
RangeRequest request_range = 1;
PutRequest request_put = 2;
DeleteRangeRequest request_delete_range = 3;
TxnRequest request_txn = 4;
}
}
响应参数
message TxnResponse {
ResponseHeader header = 1;
bool succeeded = 2; // 表明Compare的结果是否为真
repeated ResponseOp responses = 3;// 表示一个响应体列表,对应于success模块或failure模块的结果
}
message ResponseOp {
oneof response {
RangeResponse response_range = 1;
PutResponse response_put = 2;
DeleteRangeResponse response_delete_range = 3;
TxnResponse response_txn = 4;
}
}
Compact
请求参数
message CompactionRequest {
int64 revision = 1;
// 用于比较操作的键值存储的修订版本
bool physical = 2;
// 设置为true时,RPC会等待知道压缩物理性地应用到本地数据库,之后被压缩的项将完全从后端数据库中移除
}
响应参数
message CompactionResponse {
ResponseHeader header = 1;
}
watch API
watch API提供基于事件的接口,用于异步检测key的变化。etcd v3的watch机制会针对某一给定的revision进行连续检测,等待key的变化出现,并最终将key的更新信息返回client。这里的revision既可以采用当前的revision,也可以采用历史的revision
请求参数:
message WatchRequest {
oneof request_union {
WatchCreateRequest create_request = 1;
WatchCancelRequest cancel_request = 2;
WatchProgressRequest progress_request = 3;
}
}
message WatchCreateRequest {
bytes key = 1;// 与range_end组成watch的key的范围[key,range_end)
bytes range_end = 2;
int64 start_revision = 3;
//指定从该revision起开始连续watch,如果不指定,就从watch流中建立响应头revision开始watch,
//如果最后一次压缩后的版本开始,则能watch所有Event历史
bool progress_notify = 4;
//设置后,如果近期无Event,则watch将周期性的接收到无Event的WatchResponse消息
// 这在客户端希望从最后一个已知revision出恢复断开的watcher的时候非常有用。
// 至于多久发送一次通知,则取决于etcd Server的当前负载
enum FilterType {
NOPUT = 0;
NODELETE = 1;
}
repeated FilterType filters = 5;//过滤Event类型的列表
bool prev_kv = 6;//设置后,watch可接受到Event发生前的key-value数据
int64 watch_id = 7;
// 如果watch_id非零,它将被分配给这个监视器。
// 由于在etcd中创建一个watcher不是同步操作,当在同一个流上创建多个watchers时,可以使用该命令确保顺序正确
// 创建一个ID在stream已经存在的监视器会导致返回错误
bool fragment = 8;//设置后拆分大的revision为多个watch响应
}
message WatchCancelRequest {
int64 watch_id = 1; // client对watch停止接收Event
}
message WatchProgressRequest {
}
响应参数:
message WatchResponse {
ResponseHeader header = 1;
int64 watch_id = 2; // 对应watch response的ID
bool created = 3;// 若reponse对应于一个创建watch的请求,则设为true。客户端应该记录watch id并且期望在流上接受该watch的Event。所有发给新创建的watcher客户端的event都是同一个watch_id
bool canceled = 4;// 若response对应于取消watch请求,则设置为true。以后不再有Event发送到一个已经取消的watcher上
int64 compact_revision = 5;// 如果watcher尝试watch一个已经被压缩的历史版本,那么compact_revision就会被设置成一个当前etcd可用的最小历史版本,并且watcher会被取消。当watcher无法跟上etcd键值存储的处理速度时,会发生以上情况。两个相同的start_revision的watch最多只会成功一个
string cancel_reason = 6;// 表示取消watcher的原因
bool fragment = 7;// 如果监听到响应被拆分为多个响应,则为true
repeated mvccpb.Event events = 11;
}
message Event {
enum EventType {/
PUT = 0;// PUT类型表明新的数据已经存储到相应的key
DELETE = 1;// DELETE类型表明key已经被删除了。
}
EventType type = 1;/ Event的类型,分为PUT类型和DELETE类型。
KeyValue kv = 2;
// 与Event关联的key-value
//一个PUT Event包含当前的KV,一个Version=1的PUT Event表明这个key是新建的。
//一个DELETE Event包含被删除的key和该key被删除时的modification revision
KeyValue prev_kv = 3;
// 该key在发生此Event之前最近一刻revision的key-value对,
//为了节约带宽,只有在watch请求中显式地启用该选项时才会相应返回该值
}
watch操作是长期持续存在的请求,它使用gRPC流来传输Event数据,这里的watch流是双向流。
client通过写入流来创建watch;client通过读取流接受watch到的Event。
单个watch流可以通过使用pre-watch标识符来标记Event,已达到在一个watch流中多路传输多个不同的watch Event的目的。多路复用watch流能帮助减少etcd集群内存占用与连接开销。
etcd的watch机制确保了检测到Event具有有序,可靠与原子性的特点:
- 有序:Event按照revision排序,后发生的Event不会在前面的Event之前出现在watch流中
- 可靠:某个事件序列不会遗漏其中任意的子序列,例如,发生事件a,b,c,而如果watch接受到a和c,那么就能保证b也已经被接收了
- 原子性:Event列表确保包含完整的revision,在相同revision的多个key上,更新不会分裂为几个事件列表。
Lease API
租约是一种检测客户端活跃度(liveness)的机制,租约是有生存时间的,集群为租约授予一个TTL(time-to-live)。Lease的实际TTL不低于最小TTL,该最小TTL由etcd集群选择。当Lease的TTL到期时,所有与之相关联的key都将被删除。如果etcd集群在一个给定TTL周期没有收到一个keepAlive消息来维持租约,那么该租约将过期。
获取租约
请求参数:
message LeaseGrantRequest {
int64 TTL = 1;// TTL值,单位s
int64 ID = 2;// 默认是0,如果置空,那么etcd均为该Lease选择一个ID
}
响应参数
message LeaseGrantResponse {
ResponseHeader header = 1;
int64 ID = 2;
int64 TTL = 3;
string error = 4;
}
撤销租约
当撤销该Lease时,所有关联的key都会自动删除
请求参数:
message LeaseRevokeRequest {
int64 ID = 1;
}
响应参数:
message LeaseRevokeResponse {
ResponseHeader header = 1;
}
KeepAlives
请求参数:
message LeaseKeepAliveRequest{
int64 ID = 1;
}
响应参数:
message LeaseKeepAliveResponse {
ResponseHeader header = 1;
int64 ID = 2;
int64 TTL = 3; // 表示一个新的TTL值,单位是s,表示该Lease继续存在的时间
}