0
点赞
收藏
分享

微信扫一扫

ETCD API V3

code_balance 2023-06-29 阅读 71
etcd

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继续存在的时间
}
举报

相关推荐

0 条评论