1 - 发现服务协议
发现服务协议有助于新 etcd 成员在集群引导阶段,通过共享的发现令牌和端点列表,发现集群中的所有其他成员。
发现服务协议仅在集群引导阶段使用,不可用于运行时重配置或集群监控。
该协议使用新的发现令牌来引导一个唯一的 etcd 集群。请注意,一个发现令牌只能代表一个 etcd 集群。只要该令牌的发现协议已启动,即使中途失败,也不得用于引导另一个 etcd 集群。
本文其余部分将通过示例介绍发现过程,这些示例对应自托管发现集群。
请注意,本文仅适用于 v3 发现机制。有关 v2 发现 的更多详情,请参阅前文文档。
协议工作流
发现协议的核心思想是利用一个内部 etcd 集群来协调新集群的引导过程。首先,所有新成员与发现服务交互,协助生成预期的成员列表。随后,每个新成员使用该列表引导其服务器,这一操作实现的功能与 -initial-cluster 标志相同。
在以下示例工作流程中,我们将使用 etcdctl 命令列出协议的每一步,以便于理解,并假设 http://example.com:2379 主机提供 etcd 集群作为发现服务。
按照惯例,etcd 发现协议使用键前缀 /_etcd/registry。
创建新的发现令牌
生成一个唯一令牌,用于标识新集群。该令牌将在后续步骤中作为发现键空间的唯一前缀使用。一种简便的方法是使用 uuidgen:
指定预期集群规模
发现令牌需要指定集群大小,该大小必须明确提供。发现服务使用此大小来判断是否已找到将初始组成集群的所有成员。
通常,集群大小为 3、5 或 7。请参阅 optimal cluster size 以获取更多详细信息。
启动 etcd 进程
将发现令牌 ${UUID} 设置为 --discovery-token 标志,并将支持发现服务的 etcd 集群端点设置为 --discovery-endpoints 标志。这将启用 v3 发现以引导 etcd 集群。
如果给定 --discovery-token 和 --discovery-endpoints 标志,每个 etcd 进程将内部遵循以下步骤。
如果发现服务启用了客户端证书身份认证,请配置以下标志。其使用方式与使用 etcdctl 与 etcd 集群通信完全相同。
如果发现服务启用了基于角色的身份认证,请配置以下标志。其使用方式与使用 etcdctl 与 etcd 集群通信完全相同。
默认的超时时间值也可通过以下标志进行修改,其使用方式与使用 etcdctl 与 etcd 集群通信时完全相同。
自我注册
每个 etcd 进程首先会将自身注册为指定新集群中的成员。这是通过在完整注册键中创建成员 ID 作为键来实现的。
检查状态
它检查预期的集群大小和注册状态,并决定下一步操作。
如果已注册的成员仍不足,系统将等待其他成员出现。
如果注册的成员数量大于预期的集群大小 N,则将前 N 个注册的成员视为集群的成员列表。如果该成员自身位于成员列表中,发现过程成功,并通过成员列表获取所有对等成员。如果不在成员列表中,发现过程将以集群已满的失败结果结束。
成员可在注册自身之前检查集群状态。因此,如果集群已满,成员可能迅速失败。
等待所有成员就位
等待过程将持续监听键前缀 /_etcd/registry/${UUID}/members,直至发现所有成员。
2 - 日志记录约定
etcd 使用 zap 库记录应用程序输出,输出按 级别 进行分类。日志消息的级别依据以下约定确定:
DebugLevel 日志通常非常庞大,通常在生产环境中禁用。
- 示例:
- 向远程对等成员发送一条普通消息
- 将日志条目写入磁盘
- 示例:
InfoLevel 为默认的日志优先级。
- 示例:
- 启动配置
- 开始创建快照
- 向集群中添加新节点
- 向认证子系统中添加新用户
- 示例:
警告级别日志比信息级别日志更为重要,但无需逐条人工审查。
- 示例:
- 无法向远程对等成员发送 Raft 消息
- 在配置的选举超时时间内未收到心跳消息
- 示例:
ErrorLevel 日志为高优先级。若应用程序运行正常,不应产生任何 ErrorLevel 日志。
- 示例:
- WAL 无法分配磁盘空间
- 示例:
PanicLevel 会记录一条消息,然后触发 panic。
- 示例:
- Raft 消息编码失败
- 示例:
FatalLevel 会记录一条消息,然后调用 os.Exit(1)。
- 示例:
- Raft 快照保存失败
- 示例:
3 - Go 模块
etcd 项目(自版本 3.5 起)采用多个 golang 模块 ,托管于 单个仓库 中。
以下是各个模块:
go.etcd.io/etcd/api/v3 - 包含 API 定义 (如 protos 与 proto 生成的库),定义了 etcd 客户端与服务器之间的通信协议。
go.etcd.io/etcd/pkg/v3 - etcd 使用的通用工具包集合,不针对 etcd 本身。仅当某个包未来可能被移出至独立仓库时,才应归入此处。请避免在此处添加依赖关系复杂的代码,因为这些依赖会自动成为客户端库的依赖(我们希望客户端库保持轻量)。
go.etcd.io/etcd/client/v3 - 通过网络(gRPC)与 etcd 通信所使用的客户端库。建议所有新的 etcd 使用场景均采用此库。
go.etcd.io/etcd/client/v2 - 用于通过 HTTP 协议与 etcd 通信的旧版客户端库。已弃用。所有新用法应依赖 /v3 库。
go.etcd.io/etcd/raft/v3 - 分布式共识协议的实现。不应包含与 etcd 相关的特定代码。
go.etcd.io/etcd/server/v3 - etcd 实现。 该包中的代码为 etcd 内部实现,外部项目不应使用。包的结构和 API 可能在小版本内发生变更。
go.etcd.io/etcd/etcdctl/v3 - 用于访问和管理 etcd 的命令行工具。
go.etcd.io/etcd/tests/v3 - 包含 etcd 所有集成测试的模块。 请注意:所有单元测试(快速且不依赖跨模块依赖)应保留在被测试代码所在的本地模块中。
go.etcd.io/bbolt - 持久化 b 树的实现。 托管于独立的仓库中:https://github.com/etcd-io/bbolt.
运维
所有 etcd 模块应以相同版本发布,例如:
go.etcd.io/etcd/client/v3@v3.5.10必须依赖go.etcd.io/etcd/api/v3@v3.5.10。版本的持续更新可通过以下方式执行:
发布的模块应根据 https://golang.org/ref/mod#vcs-version 规则进行标记,即每个模块应拥有独立的标签。可通过以下方式执行标记:
所有 etcd 模块应依赖相同版本的底层依赖项。 可通过以下方式验证:
go.mod 文件中不得包含未使用的依赖项,且必须符合
go mod tidy格式要求。 验证方式如下:若要在所有模块中触发操作(例如自动格式化所有文件),请使用或扩展以下脚本:
未来
作为指引方向的北极星,我们希望基于以下模型评估 etcd 模块:
本文假设:
- 将 etcdmigrate/etcdadm 从 etcdctl 二进制文件中分离。 由此 etcdctl 将明确成为网络客户端 API 的命令行封装, 而 etcdmigrate/etcdadm 则支持对 etcd 存储文件的直接物理操作。
- 将 etcd-proxy 从 ./etcd 二进制文件中分离,因其包含更多实验性代码, 故带来额外风险与依赖。
- 废弃对 v2 协议的支持。