跳转到主要内容

etcd 客户端设计

客户端架构决策及其实现细节

etcd 客户端设计

Gyuho Lee (github.com/gyuho, Amazon Web Services, Inc.), Joe Betz (github.com/jpbetz, Google Inc.)

简介

etcd 服务器通过多年的故障注入测试验证了其稳健性。大多数复杂的应用逻辑已由 etcd 服务器及其数据存储处理(例如,集群成员关系对客户端透明,提案通过 Raft 层转发至领导者)。尽管服务器组件本身正确,但其与客户端的组合需采用一组复杂的协议,以确保在故障条件下仍能保证正确性和高可用性。理想情况下,etcd 服务器为多台物理机器提供单一逻辑集群视图,客户端则实现副本间的自动故障转移。本文档描述客户端的架构决策及其具体实现细节。

术语表

clientv3:etcd v3 API 的官方 Go 客户端。

clientv3-grpc1.0:官方客户端实现,包含 grpc-go v1.0.x ,在最新版 etcd v3.1 中使用。

clientv3-grpc1.7:官方客户端实现,包含 grpc-go v1.7.x ,用于最新版 etcd v3.2 和 v3.3。

clientv3-grpc1.23:官方客户端实现,包含 grpc-go v1.23.x ,在最新版 etcd v3.4 中使用。

负载均衡器:etcd 客户端负载均衡器,实现重试和故障转移机制。etcd 客户端应能自动在多个端点之间均衡负载。

端点:客户端可连接的 etcd 服务器端点列表。通常为 etcd 集群的 3 个或 5 个客户端 URL。

固定端点:当配置多个端点时,v3.3 及更早版本的客户端负载均衡器仅选择一个端点建立 TCP 连接,以减少与 etcd 集群的总连接数。在 v3.4 版本中,负载均衡器对固定端点采用轮询策略,每个请求均轮询不同端点,从而实现更均衡的负载分布。

客户端连接:通过 gRPC Dial 建立的与 etcd 服务器的 TCP 连接。

子连接:gRPC SubConn 接口。每个子连接包含一个地址列表。负载均衡器根据解析后的地址列表创建子连接。gRPC 客户端连接可以映射到多个子连接(例如,example.com 解析为 10.10.10.1 和 10.10.10.2 两个子连接)。etcd v3.4 负载均衡器采用内部解析器,为每个端点建立一个子连接。

瞬时断开连接:当 gRPC 服务器返回状态错误 code Unavailable 时。

客户端要求

正确性。在服务器发生故障时,请求可能会失败。然而,这绝不会违反一致性保证:全局顺序性、永不写入损坏数据、可变操作的至多一次语义、监听不会观察到部分事件等。

存活性。服务器可能会短暂地发生故障或断开连接。客户端应能在这两种情况下均继续推进。客户端应 永不发生死锁 ,等待服务器从离线状态恢复,除非已配置为如此。理想情况下,客户端通过 HTTP/2 ping 检测不可用的服务器,并向其他节点进行故障转移,同时提供明确的错误信息。

有效性。客户端应以最少资源高效运行:在端点切换后,先前的 TCP 连接应 优雅关闭 。故障转移机制应能有效预测下一个要连接的副本,避免对已失败节点进行无谓重试。

可移植性。官方客户端应有清晰的文档说明,其实现应适用于其他语言绑定。不同语言绑定之间的错误处理应保持一致。由于 etcd 完全致力于 gRPC,实现应与 gRPC 长期设计目标紧密对齐(例如,可插拔重试策略应与 gRPC retry 兼容)。两个客户端版本之间的升级应为非中断式。

客户端概述

etcd 客户端实现了以下组件:

  • 用于与 etcd 集群建立 gRPC 连接的负载均衡器,
  • 用于向 etcd 服务器发送 RPC 的 API 客户端,以及
  • 决定是否重试失败请求或切换端点的错误处理程序。

不同语言在建立初始连接(例如配置 TLS)、编码并发送 Protocol Buffer 消息至服务器、处理流式 RPC 等方面可能存在差异。然而,etcd 服务器返回的错误将保持一致。因此,错误处理和重试策略也应保持一致。

例如,etcd 服务器可能返回 "rpc error: code = Unavailable desc = etcdserver: request timed out",该错误为瞬态错误,应进行重试。或返回 rpc error: code = InvalidArgument desc = etcdserver: key is not provided,表示请求无效,不应重试。Go 客户端可使用 google.golang.org/grpc/status.FromError 解析错误,Java 客户端可使用 io.grpc.Status.fromThrowable。

clientv3-grpc1.0:负载均衡器概述

clientv3-grpc1.0 在配置多个 etcd 端点时,会维持多个 TCP 连接。随后选择一个地址,并使用该地址发送所有客户端请求。所固定的地址会一直保持,直至客户端对象关闭(参见 图 1)。当客户端收到错误时,会随机选择另一个地址并重试。

client-balancer-figure-01.png

clientv3-grpc1.0:负载均衡器限制

clientv3-grpc1.0 同时打开多个 TCP 连接可提供更快的负载均衡器故障转移,但需要更多资源。负载均衡器不了解节点的健康状态或集群成员关系,因此可能出现负载均衡器卡在某个已失败或分区的节点上的情况。

clientv3-grpc1.7:负载均衡器概述

clientv3-grpc1.7 仅与选定的 etcd 服务器维持一个 TCP 连接。当提供多个集群端点时,客户端会尝试连接所有端点。一旦建立任一连接,负载均衡器即固定该地址,并关闭其他连接(参见 图 2)。该固定地址需保持至客户端对象关闭。若发生服务器错误或客户端网络故障,错误将被发送至客户端错误处理程序(参见 图 3)。

client-balancer-figure-02.pngclient-balancer-figure-03.png

客户端错误处理程序接收来自 gRPC 服务器的错误,并根据错误码和错误信息决定是在同一端点重试,还是切换到其他地址(参见 图 4 和 图 5)。

client-balancer-figure-04.pngclient-balancer-figure-05.png

流式 RPC,例如监听(Watch)和保活(KeepAlive),通常在无超时的情况下发起请求。客户端可定期发送 HTTP/2 保活 ping 来检查已绑定端点的状态;若服务器未响应 ping,负载均衡器将切换至其他端点(参见 图 6)。

client-balancer-figure-06.png

clientv3-grpc1.7:负载均衡器限制

clientv3-grpc1.7 负载均衡器通过 HTTP/2 保活机制检测流式请求的断连。这是一种简单的 gRPC 服务器 ping 机制,不涉及集群成员关系的判断,因此无法检测网络分区。由于被分区的 gRPC 服务器仍可响应客户端 ping,负载均衡器可能陷入与分区节点的连接中。理想情况下,保活 ping 应在请求超时前检测到分区并触发端点切换(参见 etcd#8673 和 图 7)。

client-balancer-figure-07.png

clientv3-grpc1.7 负载均衡器维护一个不健康端点列表。断开连接的地址会被加入“不健康”列表,并在等待时长过后才被视为可用,该等待时长硬编码为连接超时时间,默认值为 5 秒。负载均衡器可能对哪些端点不健康产生误判。例如,端点 A 可能在被标记为黑名单后立即恢复,但在接下来的 5 秒内仍不可用(参见 图 8)。

clientv3-grpc1.0 遇到了上述相同的问题。

client-balancer-figure-08.png

上游 gRPC Go 已经迁移到新的负载均衡接口。例如,clientv3-grpc1.7 的底层负载均衡实现使用了新的 gRPC 负载均衡机制,并努力与旧负载均衡行为保持一致。尽管兼容性已得到合理维护,etcd 客户端仍然 遭受了细微的破坏性变更 。此外,gRPC 维护者建议 不要依赖旧的负载均衡接口 。通常而言,为获得上游更好的支持,最好与最新的 gRPC 发布版本保持同步。此外,新功能(如重试策略)可能不会回溯到 gRPC 1.7 分支。因此,etcd 服务器和客户端都必须迁移到最新的 gRPC 版本。

clientv3-grpc1.23:负载均衡器概述

clientv3-grpc1.7 与旧版 gRPC 接口耦合过于紧密,导致每次 gRPC 依赖升级都会破坏客户端行为。大部分开发与调试工作都用于修复这些客户端行为变更。结果,其实现变得过于复杂,并基于对服务器连接性的错误假设。

clientv3-grpc1.23 的主要目标是简化负载均衡器的故障转移逻辑:当客户端与当前端点断开连接时,不再维护一个可能已过时的不健康端点列表,而是直接轮询下一个端点。该机制不假设端点状态,因此无需再进行复杂的健康状态跟踪(参见 图 8 及以上内容)。升级至 clientv3-grpc1.23 不应存在问题;所有变更均为内部实现,同时保持了全部向后兼容性。

内部,当提供多个端点时,clientv3-grpc1.23 会创建多个子连接(每个端点对应一个子连接),而 clientv3-grpc1.7 仅连接到一个固定的端点(参见 图 9)。例如,在 5 节点集群中,clientv3-grpc1.23 负载均衡器需要 5 个 TCP 连接,而 clientv3-grpc1.7 仅需一个。通过保持 TCP 连接池,clientv3-grpc1.23 可能消耗更多资源,但能提供更具灵活性的负载均衡器,并具备更优的故障转移性能。默认的负载均衡策略为轮询,但可轻松扩展以支持其他类型的负载均衡器(例如,两倍选择、选择领导者等)。clientv3-grpc1.23 使用 gRPC 解析器组并实现负载均衡选择器策略,将复杂的负载均衡工作交由上游 gRPC 处理。另一方面,clientv3-grpc1.7 手动处理每个 gRPC 连接及负载均衡器故障转移,这增加了实现的复杂性。clientv3-grpc1.23 在 gRPC 拦截器链中实现重试机制,可自动处理 gRPC 内部错误,并支持更高级的重试策略(如指数退避),而 clientv3-grpc1.7 则需手动解析 gRPC 错误以实现重试。

client-balancer-figure-09.png

clientv3-grpc1.23:负载均衡器限制

可通过缓存每个端点的状态来提升性能。例如,负载均衡器可预先 ping 每个服务器,以维护健康候选端点列表,并在轮询时使用该信息。或在断开连接时,负载均衡器可优先选择健康端点。这可能会增加负载均衡器实现的复杂性,因此可留待后续版本再行处理。

客户端保活 ping 仍不考虑网络分区情况。流式请求在与分区节点通信时可能陷入停滞。需实现高级健康检查服务以准确理解集群成员关系(详见 etcd#8673 )。

client-balancer-figure-07.png

目前,重试逻辑需手动作为拦截器处理。可通过 官方 gRPC 重试 简化此过程。