etcd API

etcd API 核心设计概述

本文旨在概述 v3 etcd API 的核心设计,不应将其与已在 etcd v3.5 中弃用的 etcd v2 API 混淆。本文内容并非面面俱到,而是专注于理解 etcd 所需的基本概念,避免被较少使用的 API 调用分散注意力。所有 etcd API 均在 gRPC 服务 中定义,这些服务对 etcd 服务器所支持的远程过程调用(RPC)进行了分类。所有 etcd RPC 的完整列表可在 gRPC API 列表 的 Markdown 文档中找到。

gRPC 服务

发送到 etcd 服务器的每个 API 请求都是一次 gRPC 远程过程调用。etcd 中的 RPC 根据功能被划分为不同的服务。

用于操作 etcd 键空间的重要服务包括:

  • KV - 创建、更新、获取和删除键值对。
  • Watch - 监控键的变更。
  • Lease - 用于处理客户端保活消息的原语。

用于管理集群本身的服务包括:

  • Auth - 基于角色的用户认证机制。
  • Cluster - 提供成员信息和配置功能。
  • Maintenance - 创建恢复快照、对存储进行碎片整理,并返回每个成员的状态信息。

请求和响应

etcd 中的所有 RPC 都遵循相同的格式。每个 RPC 包含一个名为 Name 的函数,该函数以 NameRequest 作为参数,并返回 NameResponse 作为响应。例如,以下是 Range RPC 的描述:

service KV {
  Range(RangeRequest) returns (RangeResponse)
  ...
}

响应头

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_IDMember_ID 字段,以确保其正在与预期的集群(成员)通信。

应用程序可以使用 Revision 字段来了解键值存储的最新修订版本。当应用程序指定一个历史修订版本进行 时间旅行查询 并希望知道请求发生时的最新修订版本时,这一点尤其有用。

应用程序可以使用 Raft_Term 来检测集群何时完成新的领导者选举。

键值 API

键值 API 用于操作存储在 etcd 中的键值对。大多数发往 etcd 的请求通常是键值请求。

系统原语

键值对

键值对是键值 API 能够操作的最小单位。每个键值对包含多个字段,以protobuf 格式定义:

message KeyValue {
  bytes key = 1;
  int64 create_revision = 2;
  int64 mod_revision = 3;
  int64 version = 4;
  bytes value = 5;
  int64 lease = 6;
}
  • Key - 以字节表示的键。不允许空键。
  • Value - 以字节表示的值。
  • Version - 键的版本。删除操作会将版本重置为零,任何对键的修改都会使其版本递增。
  • Create_Revision - 键最后一次创建时的修订版本。
  • Mod_Revision - 键最后一次修改时的修订版本。
  • Lease - 附加到该键的租约 ID。如果租约为 0,则表示该键未附加任何租约。

除了键和值之外,etcd 还会将额外的修订元数据作为键消息的一部分进行附加。这些修订信息按创建和修改的时间顺序对键进行排序,这对于管理分布式同步中的并发非常有用。etcd 客户端的分布式共享锁利用创建修订来等待获取锁的所有权。类似地,修改修订被用于检测软件事务内存读集冲突以及等待领导者选举的更新。

修订版本

etcd 维护一个 64 位的集群范围计数器——存储修订版本(store revision),每次键空间被修改时该计数器都会递增。这个修订版本充当全局逻辑时钟,对所有存储更新进行顺序排序。由新修订版本表示的变化是增量式的;与某个修订版本关联的数据即为更改存储的数据。在内部,新修订版本意味着将变更写入后端的 B+ 树中,并以递增后的修订版本作为键。

当考虑到 etcd 的多版本并发控制后端时,修订版本的价值更加突出。MVCC 模型意味着可以从过去的修订版本查看键值存储,因为历史键修订会被保留。集群管理员可以配置此历史记录的保留策略,以实现细粒度的存储管理;通常 etcd 会定时丢弃旧的键修订。典型的 etcd 集群会保留被取代的键数据数小时之久。这也能可靠地处理长时间的客户端断开连接,而不仅仅是短暂的网络中断:观察者只需从上次观察到的历史修订版本恢复即可。同样,若要从特定时间点读取存储内容,可以在读取请求中标注一个修订版本,从而返回该修订版本提交时键空间状态下的键。

键范围

etcd 数据模型将所有键索引在一个扁平的二进制键空间上。这与其他使用分层目录结构组织键的键值存储系统不同。etcd 不按目录列出键,而是通过键区间 [a, b) 来列出键。

这些区间在 etcd 中通常被称为“范围”(ranges)。相比对目录的操作,基于区间的操作功能更强大。类似于分层存储,可以通过 [a, a+1) 的形式进行单个键的查找(例如,[‘a’, ‘a\x00’) 可以查找键 ‘a’),并通过按目录深度编码键的方式来实现目录查找。除了这些操作外,区间还可以表示前缀;例如,区间 ['a', 'b') 可以查找所有以字符串 ‘a’ 为前缀的键。

按照惯例,请求中的范围由字段 keyrange_end 表示。key 字段是范围内的第一个键,不应为空。range_end 是范围中最后一个键之后的键。如果未提供或为空,则该范围仅包含 key 参数所指定的单个键。如果 range_end 等于 key 加一(例如,“aa”+1 == “ab”,“a\xff”+1 == “b”),则该范围表示所有以 key 为前缀的键。如果 keyrange_end 均为 ‘\0’,则表示涵盖所有键。如果 range_end 为 ‘\0’,则该范围表示所有大于等于 key 参数的键。

范围

通过调用 Range API 从键值存储中获取键,该调用接收一个 RangeRequest 参数:

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;
}
  • Key, Range_End - 要获取的键的范围。
  • Limit - 请求返回的最大键数量。当 limit 设置为 0 时,表示无限制。
  • Revision - 指定键值存储的时间点版本用于此次查询。如果 revision 小于或等于零,则使用最新的键值存储状态。如果指定的 revision 已被压缩,则返回 ErrCompacted 错误。
  • Sort_Order - 指定排序请求的顺序。
  • Sort_Target - 指定用于排序的键值字段。
  • Serializable - 设置范围请求使用可序列化的本地成员读取。默认情况下,Range 操作是线性一致的,反映集群当前的共识状态。为了获得更好的性能和可用性,可以接受可能过时的数据,使用可序列化的范围请求可在本地直接响应,而无需与其他节点达成共识。
  • Keys_Only - 仅返回键,不返回值。
  • Count_Only - 仅返回范围内键的数量。
  • Min_Mod_Revision - 修改版本号的下限;过滤掉修改版本号较小的键。
  • Max_Mod_Revision - 修改版本号的上限;过滤掉修改版本号较大的键。
  • Min_Create_Revision - 创建版本号的下限;过滤掉创建版本号较小的键。
  • Max_Create_Revision - 创建版本号的上限;过滤掉创建版本号较大的键。

客户端通过 Range 调用接收到一个 RangeResponse 消息:

message RangeResponse {
  ResponseHeader header = 1;
  repeated mvccpb.KeyValue kvs = 2;
  bool more = 3;
  int64 count = 4;
}
  • Kvs - 匹配范围请求的键值对列表。当设置了 Count_Only 时,Kvs 为空。
  • More - 如果设置了 limit,此字段指示所请求的范围内是否还有更多键未返回。
  • Count - 满足范围请求的键的总数。

写入

通过发送 Put 调用来将键保存到键值存储中,该调用接收一个 PutRequest 参数:

message PutRequest {
  bytes key = 1;
  bytes value = 2;
  int64 lease = 3;
  bool prev_kv = 4;
  bool ignore_value = 5;
  bool ignore_lease = 6;
}
  • Key - 要存入键值存储的键名称。
  • Value - 要与键关联的值,以字节形式表示。
  • Lease - 要与键关联的租约 ID。租约值为 0 表示不设置租约。
  • Prev_Kv - 当设置时,响应包含此 Put 请求更新前的键值对数据。
  • Ignore_Value - 当设置时,更新键但不改变其当前值。如果键不存在,则返回错误。
  • Ignore_Lease - 当设置时,更新键但不改变其当前租约。如果键不存在,则返回错误。

客户端从 Put 调用接收到一个 PutResponse 消息:

message PutResponse {
  ResponseHeader header = 1;
  mvccpb.KeyValue prev_kv = 2;
}
  • Prev_Kv - 被 Put 操作覆盖的键值对,前提是 PutRequest 中设置了 Prev_Kv

删除范围

使用 DeleteRange 调用删除键的范围,该调用接收一个 DeleteRangeRequest

message DeleteRangeRequest {
  bytes key = 1;
  bytes range_end = 2;
  bool prev_kv = 3;
}
  • Key, Range_End - 要删除的键范围。
  • Prev_Kv - 当设置时,返回被删除的键值对内容。

客户端从 DeleteRange 调用接收到一个 DeleteRangeResponse 消息:

message DeleteRangeResponse {
  ResponseHeader header = 1;
  int64 deleted = 2;
  repeated mvccpb.KeyValue prev_kvs = 3;
}
  • Deleted - 被删除的键的数量。
  • Prev_Kv - 由 DeleteRange 操作删除的所有键值对的列表。

事务

事务是一种在键值存储上实现的原子性 If/Then/Else 结构。它提供了一种将请求组合成原子块(即 then/else)的基本机制,其执行受键值存储内容的条件控制(即 if)。事务可用于保护键免受意外并发更新、构建比较并交换操作,以及开发更高级别的并发控制机制。

一个事务可以在单个请求中原子地处理多个请求。对于键值存储的修改,这意味着存储的版本号仅递增一次,且事务生成的所有事件都将具有相同的版本号。然而,禁止在单个事务内多次修改同一个键。

所有事务都由一组比较条件联合保护,类似于 If 语句。每个比较检查存储中的单个键,可以检查值的存在或缺失、与给定值进行比较,或检查键的修订版本或版本号。两个不同的比较可以作用于相同或不同的键。所有比较都是原子执行的;如果所有比较结果均为真,则事务被视为成功,etcd 将应用事务的 then / success 请求块;否则事务失败,并应用 else / failure 请求块。

每个比较被编码为一个 Compare 消息:

message Compare {
  enum CompareResult {
    EQUAL = 0;
    GREATER = 1;
    LESS = 2;
    NOT_EQUAL = 3;
  }
  enum CompareTarget {
    VERSION = 0;
    CREATE = 1;
    MOD = 2;
    VALUE= 3;
  }
  CompareResult result = 1;
  // target is the key-value field to inspect for the comparison.
  CompareTarget target = 2;
  // key is the subject key for the comparison operation.
  bytes key = 3;
  oneof target_union {
    int64 version = 4;
    int64 create_revision = 5;
    int64 mod_revision = 6;
    bytes value = 7;
  }
}
  • Result - 逻辑比较操作的类型(例如,等于、小于等)。
  • Target - 要比较的键值字段。可以是键的版本、创建修订号、修改修订号或值。
  • Key - 用于比较的键。
  • Target_Union - 用户指定的用于比较的数据。

在处理完比较块后,事务将应用一个请求块。一个块是 RequestOp 消息的列表:

message RequestOp {
  // request is a union of request types accepted by a transaction.
  oneof request {
    RangeRequest request_range = 1;
    PutRequest request_put = 2;
    DeleteRangeRequest request_delete_range = 3;
  }
}
  • Request_Range - 一个 RangeRequest
  • Request_Put - 一个 PutRequest。键必须唯一,且不能与其他 Put 或 Delete 操作共享键。
  • Request_Delete_Range - 一个 DeleteRangeRequest。它不能与任何 Put 或 Delete 请求共享键。

总体而言,事务通过 Txn API 调用发出,该调用接收一个 TxnRequest

message TxnRequest {
  repeated Compare compare = 1;
  repeated RequestOp success = 2;
  repeated RequestOp failure = 3;
}
  • Compare - 表示用于保护事务的一组合取谓词的列表。
  • 成功 - 如果所有比较测试都为真,则处理的请求列表。
  • 失败 - 如果任意比较测试为假,则处理的请求列表。

客户端从 Txn 调用接收到一个 TxnResponse 消息:

message TxnResponse {
  ResponseHeader header = 1;
  bool succeeded = 2;
  repeated ResponseOp responses = 3;
}
  • Succeeded(是否成功) - 表示 Compare 的求值结果为真或假。
  • Responses(响应) - 一个响应列表,对应于当 succeeded 为 true 时应用 Success 块的结果,或当 succeeded 为 false 时应用 Failure 块的结果。

Responses 列表对应于已应用的 RequestOp 列表的结果,每个响应被编码为一个 ResponseOp

message ResponseOp {
  oneof response {
    RangeResponse response_range = 1;
    PutResponse response_put = 2;
    DeleteRangeResponse response_delete_range = 3;
  }
}

每个内部响应中包含的 ResponseHeader 不应被解释为任何含义。如果客户端需要获取最新的修订版本,则应始终检查 TxnResponse 中顶层的 ResponseHeader

监听 API

Watch API 提供了一个基于事件的接口,用于异步监控键的变化。etcd 的 watch 会从给定的修订版本(当前或历史)持续监听键的变更,并将键的更新流式传输回客户端。

事件

每个键的每次更改都通过 Event 消息表示。Event 消息同时提供更新的数据以及更新类型:

message Event {
  enum EventType {
    PUT = 0;
    DELETE = 1;
  }
  EventType type = 1;
  KeyValue kv = 2;
  KeyValue prev_kv = 3;
}
  • Type(类型) - 事件的种类。PUT 类型表示有新数据被存储到该键;DELETE 类型表示该键已被删除。
  • KV - 与事件相关联的 KeyValue。PUT 事件包含当前的 kv 对。当 kv.Version=1 的 PUT 事件表示键的创建。DELETE 事件包含被删除的键,其修改版本号设置为删除时的修订版本。
  • Prev_KV - 事件发生前上一个修订版本中的键值对。为了节省带宽,仅当 watch 显式启用了此功能时才会填充该字段。

监听流

Watch 是长期运行的请求,使用 gRPC 流来传输事件数据。Watch 流是双向的:客户端向流中写入以建立 watch,从流中读取以接收 watch 事件。单个 watch 流可以通过为事件打上独立的 watch 标识符来复用多个不同的 watch,这种复用有助于减少核心 etcd 集群的内存占用和连接开销。

有关 watch 事件保证的详细信息,请阅读 etcd API 保证

客户端通过在 Watch 返回的流上发送一个 WatchCreateRequest 来创建 watch:

message WatchCreateRequest {
  bytes key = 1;
  bytes range_end = 2;
  int64 start_revision = 3;
  bool progress_notify = 4;

  enum FilterType {
    NOPUT = 0;
    NODELETE = 1;
  }
  repeated FilterType filters = 5;
  bool prev_kv = 6;
}
  • Key, Range_End - 要监听的键范围。
  • Start_Revision - 可选参数,指定开始监听的起始修订版本(包含)。若未指定,则将从 watch 创建响应头中的修订版本之后开始流式传输事件。可以从最近一次压缩的修订版本开始监听完整的可用事件历史。
  • Progress_Notify - 启用后,如果没有新的事件,watch 将周期性地接收到不包含事件的 WatchResponse。这在客户端希望从最近已知的修订版本恢复断开连接的 watcher 时非常有用。etcd 服务器根据当前负载决定发送通知的频率。
  • Filters - 在服务器端过滤掉的事件类型列表。
  • Prev_Kv - 启用后,watch 将接收到事件发生前的键值数据。这对于了解被覆盖的数据非常有用。

作为对 WatchCreateRequest 的响应,或当某个已建立的 watch 有新事件时,客户端会接收到一个 WatchResponse

message WatchResponse {
  ResponseHeader header = 1;
  int64 watch_id = 2;
  bool created = 3;
  bool canceled = 4;
  int64 compact_revision = 5;

  repeated mvccpb.Event events = 11;
}
  • Watch_ID - 与响应对应的 watch 的 ID。
  • Created - 如果响应对应的是创建 watch 请求,则设置为 true。客户端应存储该 ID,并期望在流上接收与此 watch 相关的事件。发送给此 watcher 的所有事件都将具有相同的 watch_id。
  • Canceled - 如果响应对应的是取消 watch 请求,则设置为 true。此后将不再向已取消的 watcher 发送任何事件。
  • Compact_Revision - 如果 watcher 尝试从已被压缩的版本开始监听,该字段会被设置为 etcd 中可用的最小历史版本。这种情况可能发生在使用已被压缩的版本创建 watcher 时,或 watcher 无法跟上键值存储的进度时。该 watcher 将被取消;使用相同 start_revision 创建新 watcher 的请求也将失败。
  • Events - 按顺序排列的、与给定 watch ID 对应的新事件列表。

如果客户端希望停止接收某个 watch 的事件,它应发送一个 WatchCancelRequest

message WatchCancelRequest {
   int64 watch_id = 1;
}
  • Watch_ID - 要取消的 watch 的 ID,以停止传输更多事件。

租约 API

Lease(租约)是一种检测客户端存活状态的机制。集群会授予具有生存时间(TTL)的租约。如果 etcd 集群在指定的 TTL 时间内未收到 keepAlive 请求,租约将过期。

为了将租约与键值存储关联起来,每个键最多可绑定一个租约。当租约过期或被撤销时,所有绑定到该租约的键都将被删除。每个被删除的键都会在事件历史中生成一条删除事件。

获取租约

通过调用 LeaseGrant API 获取租约,该调用接收一个 LeaseGrantRequest

message LeaseGrantRequest {
  int64 TTL = 1;
  int64 ID = 2;
}
  • TTL - 建议的生存时间,单位为秒。
  • ID - 请求的租约 ID。如果 ID 设置为 0,etcd 将自动选择一个 ID。

客户端通过 LeaseGrant 调用接收到一个 LeaseGrantResponse

message LeaseGrantResponse {
  ResponseHeader header = 1;
  int64 ID = 2;
  int64 TTL = 3;
}
  • ID - 已授予租约的租约 ID。
  • TTL - 服务器选定的租约生存时间,单位为秒。
message LeaseRevokeRequest {
  int64 ID = 1;
}
  • ID - 要撤销的租约 ID。当租约被撤销时,所有与其关联的键都将被删除。

保活

租约通过使用 LeaseKeepAlive API 调用创建的双向流进行刷新。当客户端希望刷新租约时,它会在该流上发送一个 LeaseKeepAliveRequest

message LeaseKeepAliveRequest {
  int64 ID = 1;
}
  • ID - 需要保持活跃状态的租约 ID。

keep alive 流将返回一个 LeaseKeepAliveResponse

message LeaseKeepAliveResponse {
  ResponseHeader header = 1;
  int64 ID = 2;
  int64 TTL = 3;
}
  • ID - 已被刷新并获得新 TTL 的租约 ID。
  • TTL - 租约剩余的新生存时间,单位为秒。

最后更新于 2025 年 6 月 3 日:递归地将 v3.6 的内容复制到 v3.7(a90b2a6)