ETCD API V3
- ETCD V3 vs V2
- gRPC
- 序列化与反序列化
- 减少TCP连接
- 租约机制
- 观察者模式
- 数据存储模型
- mini事务
- 快照
- 大规模watch
- gRPC服务
- KV API
- Range
- Put
- DeleteRange
- Txn
- Compact
- watch API
- Lease API
- 获取租约
- 撤销租约
- KeepAlives
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 sortingASCEND = 1; // lowest target value firstDESCEND = 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 IDuint64 member_id = 2;// 生成该响应的memberIDint64 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没有,则表示只是为了查询keybytes range_end = 2;// [key,range_end) ,当range_end为'\0'表示大于key的所有键,如果key和range_end都是'\0',请求返回所有的键,如果range_end是键加1(例如:key=“aa”,range_end=“ab”),则表示以这个key为前缀的所有keyint64 limit = 3;// 限制返回的key的数量,如果limit=0,则表示不限制int64 revision = 4;// 如果小于等于0,表示最新的键值存储,如果被压缩,则响应ErrCompactedSortOrder 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;// 表示写入的keybytes value = 2;// 表示写入的valueint64 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为前缀的所有keybool 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; // 待比较的keyoneof 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的IDbool created = 3;// 若reponse对应于一个创建watch的请求,则设为true。客户端应该记录watch id并且期望在流上接受该watch的Event。所有发给新创建的watcher客户端的event都是同一个watch_idbool 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;// 如果监听到响应被拆分为多个响应,则为truerepeated mvccpb.Event events = 11;
}message Event {enum EventType {/PUT = 0;// PUT类型表明新的数据已经存储到相应的keyDELETE = 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 revisionKeyValue 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值,单位sint64 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继续存在的时间
}