阿巩
防范区的骄傲
小区从管控区调整为防范区了,40多天的封闭后终于可以光明正大地下楼遛狗了!许愿能尽快吃上平价麦当劳,而且每顿都有可口可乐!日拱一卒,让我们开始吧!(长文预警哦)
Etcd是一个高可用的分布式键值(key-value)数据库,Etcd也是云原生架构中重要的基础组件之一,它在微服务和Kubernates集群中不仅可以作为服务注册与发现,还可以作为key-value存储的中间件,供应用程序读取和写入数据。
作为一个高度一致的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。它可以优雅地处理网络分区期间的 leader 选举,以应对机器的故障,即使是在 leader 节点发生故障时。
Etcd的存储有如下特点:
在分布式环境中,我们发现对于服务注册与发现涉及到三个主要的角色:服务请求者、服务提供者和服务注册中心。
我们将各个服务启动时注册到 Etcd 上,同时为这些服务配置键的 TTL 时间,定时保持服务的心跳以达到监控健康状态的效果。为了确保连接,我们可以在每个服务机器上都部署一个 Proxy 模式的 Etcd,这样就可以确保访问 Etcd 集群的服务都能够互相连接。由于Etcd 基于 Raft 算法,实现分布式集群的一致性,存储到 Etcd 集群中的值必然是全局一致的。
对于分布式锁有两种使用方式:保持独占和控制时序。
在上一期的最后,我们看到了watcher调用Etcd提供的clientv3.Watcher接口的方法。
这期接着上期来深入探究下Etcd的核心API是如何实现的。
处理Etcd键值的重要服务包括:
发送到Etcd服务的每个API请求都是一个gRPC远程过程调用,Etcd3中的所有RPC都遵循相同的格式。
KV service
我们拿出etcdserver/etcdserverpb下的rpc.proto来看下,首先是KV service:
service KV {
// 从键值存储中获取范围内的key.
rpc Range(RangeRequest) returns (RangeResponse) {
option (google.api.http) = {
post: "/v3beta/kv/range"
body: "*"
};
}
// 放置给定key到键值存储.
// put请求增加键值存储的修订版本并在事件历史中生成一个事件.
rpc Put(PutRequest) returns (PutResponse) {
option (google.api.http) = {
post: "/v3beta/kv/put"
body: "*"
};
}
// 从键值存储中删除给定范围。
// 删除请求增加键值存储的修订版本并在事件历史中为每个被删除的key生成一个删除事件.
rpc DeleteRange(DeleteRangeRequest) returns (DeleteRangeResponse) {
option (google.api.http) = {
post: "/v3beta/kv/deleterange"
body: "*"
};
}
// 在单个事务中处理多个请求。
// 一个 txn 请求增加键值存储的修订版本并为每个完成的请求生成带有相同修订版本的事件。
// 不容许在一个txn中多次修改同一个key.
rpc Txn(TxnRequest) returns (TxnResponse) {
option (google.api.http) = {
post: "/v3beta/kv/txn"
body: "*"
};
}
// 压缩在etcd键值存储中的事件历史。
// 键值存储应该定期压缩,否则事件历史会无限制的持续增长.
rpc Compact(CompactionRequest) returns (CompactionResponse) {
option (google.api.http) = {
post: "/v3beta/kv/compaction"
body: "*"
};
}
}
message RangeRequest {
enum SortOrder {
NONE = 0; // 默认,不排序
ASCEND = 1; // 正序,低的值在前
DESCEND = 2; // 倒序,高的值在前
}
enum SortTarget {
KEY = 0;
VERSION = 1;
CREATE = 2;
MOD = 3;
VALUE = 4;
}
// key 是 range 的第一个 key。如果 range_end 没有给定,请求仅查找这个 key
bytes key = 1;
// range_end 代表请求的上限,如果 range_end 是 '\0',范围是大于等于 key 的所有key
bytes range_end = 2;
// 请求返回的key的数量限制
int64 limit = 3;
// revision 修订版本作于 range 键值对存储的时间点。如果 revision 小于或等于零,范围是在最新的键值对存储上。
int64 revision = 4;
// 指定返回结果的排序顺序
SortOrder sort_order = 5;
// 用于排序的键值字段
SortTarget sort_target = 6;
// serializable 设置 range 请求使用串行化成员本地读。
bool serializable = 7;
// 设置仅返回key而不需要value
bool keys_only = 8;
// 设置仅仅返回范围内key的数量
bool count_only = 9;
// min_mod_revision 是返回 key 的 mod revision 的下限;更低 mod revision 的所有 key 都将被过滤掉
int64 min_mod_revision = 10;
// max_mod_revision 是返回 key 的 mod revision 的上限;更高 mod revision 的所有 key 都将被过滤掉
int64 max_mod_revision = 11;
// min_create_revision 是返回 key 的 create revision 的下限;更低 create revision 的所有 key 都将被过滤掉
int64 min_create_revision = 12;
// max_create_revision 是返回 key 的 create revision 的上限;更高 create revision 的所有 key 都将被过滤掉
int64 max_create_revision = 13;
}
响应RangeResponse的结构如下:
message RangeResponse {
ResponseHeader header = 1;
// kvs 是匹配 range 请求的键值对列表
// 当 count 时是空的
repeated mvccpb.KeyValue kvs = 2;
// more 代表在被请求的范围内是否还有更多的 key
bool more = 3;
// count 被设置为在范围内的 key 的数量
int64 count = 4;
}
Etcd所有响应都包含一个响应头ResponseHeader,其结构如下:
message ResponseHeader {
// 产生响应的集群的ID
uint64 cluster_id = 1;
// 产生响应的成员的ID
uint64 member_id = 2;
// 产生响应时键值存储的修订版本号
int64 revision = 3;
// 产生响应时,成员的Raft称谓
uint64 raft_term = 4;
}
应用服务可以通过 Cluster_ID 和 Member_ID 字段来确保,当前与之通信的正是预期的那个集群或者成员。应用服务可以使用 Raft_Term 来检测集群何时完成一个新的 leader 选举。
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;
// version 是 key 的版本。删除会重置版本为0,而任何 key 的修改会增加它的版本。
int64 version = 4;
// value 是 key 持有的值,bytes 格式。
bytes value = 5;
// lease 是附加给 key 的租约 id。
// 当附加的租约过期时,key 将被删除。
// 如果 lease 为0,则没有租约附加到 key。
int64 lease = 6;
}
PutRequest结构如下:
message PutRequest {
// byte 数组形式的 key,用来保存到键值对存储
bytes key = 1;
// byte 数组形式的 value,在键值对存储中和 key 关联
bytes value = 2;
// 在键值存储中和 key 关联的租约id。0代表没有租约。
int64 lease = 3;
// 如果 prev_kv 被设置,etcd 获取改变之前的上一个键值对。
// 上一个键值对将在 put 应答中被返回
bool prev_kv = 4;
// 如果 ignore_value 被设置, etcd 使用它当前的 value 更新 key.
// 如果 key 不存在,返回错误.
bool ignore_value = 5;
// 如果 ignore_lease 被设置, etcd 使用它当前的租约更新 key.
// 如果 key 不存在,返回错误.
bool ignore_lease = 6;
}
PutResponse结构如下:
message PutResponse {
ResponseHeader header = 1;
// 如果请求中的 prev_kv 被设置,将会返回上一个键值对
mvccpb.KeyValue prev_kv = 2;
}
message DeleteRangeRequest {
// key是要删除的范围的第一个key
bytes key = 1;
// range_end 是要删除范围[key, range_end)的最后一个key
// 如果 range_end 没有给定,范围定义为仅包含 key 参数
// 如果 range_end 比给定的 key 大1,则 range 是以给定 key 为前缀的所有 key
// 如果 range_end 是 '\0', 范围是所有大于等于参数 key 的所有 key。
bytes range_end = 2;
// 如果 prev_kv 被设置,etcd获取删除之前的上一个键值对。
// 上一个键值对将在 delete 应答中被返回
bool prev_kv = 3;
}
message DeleteRangeResponse {
ResponseHeader header = 1;
// 被范围删除请求删除的 key 的数量
int64 deleted = 2;
// 如果请求中的 prev_kv 被设置,将会返回上一个键值对
repeated mvccpb.KeyValue prev_kvs = 3;
}
message TxnRequest {
// compare 是断言列表,体现为条件的联合。
// 如果比较成功,那么成功请求将被按顺序处理,而应答将按顺序包含他们对应的应答。
// 如果比较失败,那么失败请求将被按顺序处理,而应答将按顺序包含他们对应的应答。
repeated Compare compare = 1;
// 成功请求列表,当比较评估为 true 时将被应用。
repeated RequestOp success = 2;
// 失败请求列表,当比较评估为 false 时将被应用。
repeated RequestOp failure = 3;
}
其中Compare消息体:
message Compare {
enum CompareResult {
EQUAL = 0;
GREATER = 1;
LESS = 2;
NOT_EQUAL = 3;
}
enum CompareTarget {
VERSION = 0;
CREATE = 1;
MOD = 2;
VALUE= 3;
}
// result 是这个比较的逻辑比较操作
CompareResult result = 1;
// target 是比较要检查的键值字段
CompareTarget target = 2;
// key 是用于比较操作的主题key
bytes key = 3;
oneof target_union {
// version 是给定 key 的版本
int64 version = 4;
// create_revision 是给定 key 的创建修订版本
int64 create_revision = 5;
// mod_revision 是给定 key 的最后修改修订版本
int64 mod_revision = 6;
// value 是给定 key 的值,以 bytes 的形式
bytes value = 7;
}
}
RequestOp消息体:
message RequestOp {
// request 是可以被事务接受的请求类型的联合
oneof request {
RangeRequest request_range = 1;
PutRequest request_put = 2;
DeleteRangeRequest request_delete_range = 3;
}
}
应答的消息体TnxResponse:
message TxnResponse {
ResponseHeader header = 1;
// 如果比较评估为true则succeeded被设置为true,否则是false
bool succeeded = 2;
// 应答列表,如果 succeeded 是 true 则对应成功请求,如果 succeeded 是 false 则对应失败请求
repeated ResponseOp responses = 3;
}
ResponseOp消息体:
message ResponseOp {
// response 是事务返回的应答类型的联合
oneof response {
RangeResponse response_range = 1;
PutResponse response_put = 2;
DeleteRangeResponse response_delete_range = 3;
}
}
请求的消息体是 CompactionRequest, CompactionRequest 压缩键值对存储到给定修订版本。所有修订版本比压缩修订版本小的键都将被删除:
message CompactionRequest {
// 键值存储的修订版本,用于比较操作
int64 revision = 1;
// physical设置为 true 时 RPC 将会等待直到压缩物理性的应用到本地数据库,到这程度被压缩的项将完全从后端数据库中移除。
bool physical = 2;
}
message CompactionResponse {
ResponseHeader header = 1;
}
Watch service
Watch service提供观察键值对变化的支持。在 rpc.proto 中 Watch service 定义如下:
service Watch {
// Watch 观察将要发生或者已经发生的事件。
// 输入和输出都是流;输入流用于创建和取消观察,而输出流发送事件。
// 一个观察 RPC 可以在一次性在多个key范围上观察,并为多个观察流化事件。
// 整个事件历史可以从最后压缩修订版本开始观察。
rpc Watch(stream WatchRequest) returns (stream WatchResponse) {
option (google.api.http) = {
post: "/v3beta/watch"
body: "*"
};
}
}
WatchRequest请求体:
message WatchRequest {
// request_union 要么是创建新的观察者的请求,要么是取消一个已经存在的观察者的请求
oneof request_union {
WatchCreateRequest create_request = 1;
WatchCancelRequest cancel_request = 2;
}
}
创建新的观察者的请求 WatchCreateRequest:
message WatchCreateRequest {
// key 是注册要观察的 key
bytes key = 1;
// range_end 是要观察的范围 [key, range_end) 的终点。
// 如果 range_end 没有设置,则只有参数 key 被观察。
// 如果 range_end 等同于 '\0', 则大于等于参数 key 的所有 key 都将被观察
// 如果 range_end 比给定 key 大1, 则所有以给定 key 为前缀的 key 都将被观察
bytes range_end = 2;
// start_revision 是可选的开始(包括)观察的修订版本。不设置 start_revision 则表示 "现在".
int64 start_revision = 3;
// 设置 progress_notify ,这样如果最近没有事件,etcd 服务器将定期的发送不带任何事件的 WatchResponse 给新的观察者。
// 当客户端希望从最近已知的修订版本开始恢复断开的观察者时有用。
// etcd 服务器将基于当前负载决定它发送通知的频率。
bool progress_notify = 4;
enum FilterType {
// 过滤掉 put 事件
NOPUT = 0;
// 过滤掉 delete 事件
NODELETE = 1;
}
// 过滤器,在服务器端发送事件给回观察者之前,过滤掉事件。
repeated FilterType filters = 5;
// 如果 prev_kv 被设置,被创建的观察者在事件发生前获取上一次的KV。
// 如果上一次的KV已经被压缩,则不会返回任何东西
bool prev_kv = 6;
}
取消已有观察者的 WatchCancelRequest :
message WatchCancelRequest {
// watch_id 是要取消的观察者的id,这样就不再有更多事件传播过来了。
int64 watch_id = 1;
}
WatchResponse响应消息体:
message WatchResponse {
ResponseHeader header = 1;
// watch_id 是和应答相关的观察者的ID
int64 watch_id = 2;
// 如果应答是用于创建观察者请求的,则 created 设置为 true。
// 客户端应该记录 watch_id 并期待从同样的流中为创建的观察者接收事件。
// 所有发送给被创建的观察者的事件将附带同样的 watch_id
bool created = 3;
// 如果应答是用于取消观察者请求的,则 canceled 设置为true。
// 不会再有事件发送给被取消的观察者。
bool canceled = 4;
// compact_revision 被设置为最小 index,如果观察者试图观察被压缩的 index。
// 当在被压缩的修订版本上创建观察者或者观察者无法追上键值对存储的进展时发生。
// 客户端应该视观察者为被取消,并不应该试图再次创建任何带有相同 start_revision 的观察者。
int64 compact_revision = 5;
// cancel_reason 指出取消观察者的理由.
string cancel_reason = 6;
repeated mvccpb.Event events = 11;
}
mvccpb.Event 的消息体:
message Event {
enum EventType {
PUT = 0;
DELETE = 1;
}
// type 是事件的类型。
// 如果类型是 PUT,表明新的数据已经存储到 key。
// 如果类型是 DELETE, 表明 key 已经被删除。
EventType type = 1;
// kv 为事件持有 KeyValue。
// PUT 事件包含当前的kv键值对
// kv.Version=1 的 PUT 事件表明 key 的创建
// DELETE/EXPIRE 事件包含被删除的 key,它的修改修订版本设置为删除的修订版本
KeyValue kv = 2;
// prev_kv 持有在事件发生前的键值对
KeyValue prev_kv = 3;
}
Lease service
Lease service 提供租约的支持,在 rpc.proto 文件中 Lease service 定义如下:
service Lease {
// LeaseGrant 创建一个租约,当服务器在给定 time to live 时间内没有接收到 keepAlive 时租约过期。
// 如果租约过期则所有附加在租约上的key将过期并被删除。
// 每个过期的key在事件历史中生成一个删除事件。
rpc LeaseGrant(LeaseGrantRequest) returns (LeaseGrantResponse) {}
// LeaseRevoke 撤销一个租约。
// 所有附加到租约的key将过期并被删除。
rpc LeaseRevoke(LeaseRevokeRequest) returns (LeaseRevokeResponse) {}
// LeaseKeepAlive 通过从客户端到服务器端的流化的 keep alive 请求和从服务器端到客户端的流化的 keep alive 应答来维持租约.
rpc LeaseKeepAlive(stream LeaseKeepAliveRequest) returns (stream LeaseKeepAliveResponse) {}
// LeaseTimeToLive 获取租约信息。
rpc LeaseTimeToLive(LeaseTimeToLiveRequest) returns (LeaseTimeToLiveResponse) {}
}
message LeaseGrantRequest {
// TTL 是建议的以秒为单位的 time-to-live
int64 TTL = 1;
// ID 是租约的请求ID。如果ID设置为0, 则出租人(也就是etcd server)选择一个ID。
int64 ID = 2;
}
message LeaseGrantResponse {
ResponseHeader header = 1;
// ID 是承认的租约的ID
int64 ID = 2;
// TTL 是服务器选择的以秒为单位的租约time-to-live
int64 TTL = 3;
string error = 4;
}
message LeaseRevokeRequest {
// ID是要取消的租约的ID。
// 当租约被取消时,所有关联的key将被删除
int64 ID = 1;
}
message LeaseRevokeResponse {
ResponseHeader header = 1;
}
message LeaseKeepAliveRequest {
// ID 是要继续存活的租约的 ID
int64 ID = 1;
}
message LeaseKeepAliveResponse {
ResponseHeader header = 1;
// ID 是从继续存活请求中得来的租约 ID
int64 ID = 2;
// TTL是租约新的 time-to-live
int64 TTL = 3;
}
message LeaseTimeToLiveRequest {
// ID 是租约的 ID.
int64 ID = 1;
// keys 设置为 true 可以查询附加到这个租约上的所有 key
bool keys = 2;
}
message LeaseTimeToLiveResponse {
ResponseHeader header = 1;
// ID 是来自请求的 ID.
int64 ID = 2;
// TTL 是租约剩余的 TTL,单位为秒;租约将在接下来的 TTL + 1 秒之后过期
int64 TTL = 3;
// GrantedTTL 是租约创建/续约时初始授予的时间,单位为秒
int64 grantedTTL = 4;
// keys 是附加到这个租约的 key 的列表
repeated bytes keys = 5;
}
Lock service
Lock service 提供分布式共享锁的支持,在v3lock.proto 中 Lock service 定义如下:
// Lock service 以 gRPC 接口的方式暴露客户端锁机制。
service Lock {
// 在给定命令锁上获得分布式共享锁。
// 成功时,将返回一个唯一 key,在调用者持有锁期间会一直存在。
// 这个 key 可以和事务一起工作,以安全的确保对 etcd 的更新仅仅发生在持有锁时。
// 锁被持有直到在 key 上调用解锁或者和所有者关联的租约过期。
rpc Lock(LockRequest) returns (LockResponse) {}
// Unloke 使用 Lock 返回的 key 并释放对锁的持有。
// 下一个在等待这个锁的 Lock 的调用者将被唤醒并给予锁的所有权。
rpc Unlock(UnlockRequest) returns (UnlockResponse) {}
}
message LockRequest {
// name 是要获取的分布式共享锁的标识
bytes name = 1;
// lease 是将要附加到锁所有权的租约的 ID。如果租约过期或者撤销时正持有锁,则锁将自动释放。
// 使用相同的租约调用锁将视为单次获取;使用同样租约的第二次锁定将是空操作。
int64 lease = 2;
}
应答的信息体 LockResponse:
message LockResponse {
etcdserverpb.ResponseHeader header = 1;
// key 是在 Lock 调用者拥有锁期间存在于 etcd 上的 key。
// 用户不可以修改这个 key,否者锁将不能正常工作
bytes key = 2;
}
message UnlockRequest {
// key 是通过 Lock 方法得到的锁所有权 key
bytes key = 1;
}
应答的信息体UnlockResponse:
message UnlockResponse {
etcdserverpb.ResponseHeader header = 1;
}
Election service
Election service 提供观察键值对变化的支持,在 v3election.proto 中 Election service 定义如下:
// Election service 以 gRPC 接口的方式暴露客户端选举机制。
service Election {
// Campaign 等待获得选举的领导地位,如果成功返回 LeaderKey 代表领导地位。
// 然后 LeaderKey 可以用来在选举时发起新的值,在依然持有领导地位时事务性的守护 API 请求,
// 还有从选举中辞职。
rpc Campaign(CampaignRequest) returns (CampaignResponse) {}
// Proclaim 用新值更新领导者的旧值
rpc Proclaim(ProclaimRequest) returns (ProclaimResponse) {}
// Leader 返回当前的选举公告,如果有。
rpc Leader(LeaderRequest) returns (LeaderResponse) {}
// Observe 以流的方式返回选举公告,和被选举的领导者发布的顺序一致。
rpc Observe(LeaderRequest) returns (stream LeaderResponse) {}
// Resign 放弃选举领导地位,以便其他参选人可以在选举中获得领导地位。
rpc Resign(ResignRequest) returns (ResignResponse) {}
}
message CampaignRequest {
// name 是选举的标识符,用来参加竞选
bytes name = 1;
// lease is the ID of the lease attached to leadership of the election. If the
// lease expires or is revoked before resigning leadership, then the
// leadership is transferred to the next campaigner, if any.
// lease 是附加到选举领导地位的租约的ID。如果租约过期或者在放弃领导地位之前取消,
// 则领导地位转移到下一个竞选者,如果有。
int64 lease = 2;
// value 是竞选者赢得选举时设置的初始化公告值。
bytes value = 3;
}
message CampaignResponse {
etcdserverpb.ResponseHeader header = 1;
// leader 描述用于持有选举的领导地位的资源
LeaderKey leader = 2;
}
其中LeaderKey 消息体的内容如下:
message LeaderKey {
// name 是选举标识符,和领导地位 key 对应
bytes name = 1;
// key 是不透明的 key ,代表选举的领导地位。
// 如果 key 被删除,则领导地位丢失
bytes key = 2;
// rev 是 key 的创建修订版本。它可以用来在事务期间测验选举的领导地位,通过测验 key 的创建修订版本匹配 rev
int64 rev = 3;
// lease 是选举领导者的租约 ID
int64 lease = 4;
}
message ProclaimRequest {
// leader 是在选举上持有的领导地位
LeaderKey leader = 1;
// value 是打算用于覆盖领导者当前值的更新。
bytes value = 2;
}
message ProclaimResponse {
etcdserverpb.ResponseHeader header = 1;
}
message LeaderRequest {
// name 是选举标识符,用于查询领导地位信息的
bytes name = 1;
}
message LeaderResponse {
etcdserverpb.ResponseHeader header = 1;
// kv 是键值对,体现最后的领导者更新
mvccpb.KeyValue kv = 2;
}
其中mvccpb.KeyValue 来自 kv.proto,消息体定义为:
message KeyValue {
// key 是 bytes 格式的 key。不容许 key 为空。
bytes key = 1;
// create_revision 是这个 key 最后一次创建的修订版本
int64 create_revision = 2;
// mod_revision 是这个 key 最后一次修改的修订版本
int64 mod_revision = 3;
// version 是 key 的版本。删除会重置版本为0,而任何 key 的修改会增加它的版本。
int64 version = 4;
// value 是 key 持有的值,bytes 格式。
bytes value = 5;
// lease 是附加给 key 的租约 id。
// 当附加的租约过期时,key 将被删除。
// 如果 lease 为0,则没有租约附加到 key。
int64 lease = 6;
}
message ResignRequest {
// leader 是要放弃的领导地位
LeaderKey leader = 1;
}
message ResignResponse {
etcdserverpb.ResponseHeader header = 1;
}
参考:
etcd doc
https://etcd.io/docs/v3.5/
还不了解etcd?一文带你快速入门
https://mp.weixin.qq.com/s/f9loVQr7jbjnDKqRWXz8sQ
彻底搞懂etcd系列文章(一):初识etcd
https://mp.weixin.qq.com/s/cD9NZ3vPIY9nF8jGTgT7Cw
Etcd官方文档
https://www.bookstack.cn/read/etcd/documentation-dev-guide-embed_etcd.md
the end