12.故障处理与灾难恢复
故障场景分析
在分布式系统中,故障是常态而非异常。作为 etcd 的使用者和维护者,理解 etcd 在各种故障场景下的行为模式,是保障系统稳定性的第一步。etcd 基于 Raft 共识算法构建,其设计目标之一就是能够在特定的故障范围内自我修复和维持可用性。然而,超出设计边界的故障则需要人工介入进行灾难恢复。
少数 Follower 故障
当集群中少于半数的 Follower 节点发生故障时,etcd 集群通常能够保持正常运行,不会产生显著的服务中断。例如,在一个由 5 个成员组成的集群中,如果有 2 个 Follower 宕机,集群依然拥有 3 个可用节点,满足 (N-1)/2 的容错要求,Leader 选举和日志复制可以照常进行。
对于客户端而言,连接到故障节点的请求会失败。优秀的客户端库应当具备自动重试和故障转移机制,将请求重新路由到健康的节点上,从而对上层应用屏蔽这种短暂的连接中断。需要注意的是,随着部分节点的离线,剩余健康节点的负载会相应增加,运维人员应监控这些节点的资源使用情况。
Leader 故障
Leader 节点在集群中扮演着核心角色,负责处理所有写请求并协调日志复制。当 Leader 发生故障时,集群会触发新一轮的选举过程。
选举并非瞬间完成。由于 etcd 使用基于超时的故障检测机制,集群需要等待一个选举超时周期(通常在几百毫秒级别)才能检测到 Leader 缺失并开始选举。在此期间,集群无法处理任何写请求,所有写操作都会被客户端或服务端排队等待。
对于已经发送给旧 Leader 但尚未提交(commit)的写请求,这些请求可能会丢失。新选举出的 Leader 有权重写旧 Leader 未提交的日志条目。从用户视角看,部分写请求可能会因为超时而失败。但是,所有已经提交的写操作都是安全的,不会丢失。
此外,新 Leader 会自动延长所有 Lease(租约)的超时时间。这一机制确保了即使 Lease 是由旧 Leader 颁发的,也不会因为 Leader 切换而导致 Lease 提前过期。
多数节点故障
当集群中的多数节点(Majority)同时发生故障时,etcd 集群将失去仲裁(Quorum),无法继续提供写服务。这是 etcd 最严重的故障模式之一。
集群只有在多数成员恢复可用后,才能自动恢复服务。如果多数成员永久性损坏且无法恢复,那么就必须启动灾难恢复流程,利用备份的快照(Snapshot)来重建集群。
一旦多数节点恢复工作,集群会自动选举新的 Leader 并恢复到健康状态。同样,新 Leader 会延长所有 Lease 的超时时间,防止因服务端不可用导致 Lease 意外过期。
网络分区
网络分区(Network Partition)是指集群节点之间的网络通信被切断,导致集群分裂成两个或多个无法互通的子集。网络分区的行为表现类似于少数节点故障或 Leader 故障。
假设一个 5 节点集群被分割成 3 节点和 2 节点两部分:
- 拥有 3 个节点的多数派子集可以继续工作,选举出新的 Leader 并处理写请求。
- 拥有 2 个节点的少数派子集无法达到多数,因此无法选举 Leader,也无法处理写请求,处于不可用状态。
etcd 不会出现“脑裂”(Split-Brain)现象,因为集群成员的变更(增删节点)必须经过当前多数成员的批准。少数派无法在未经多数派同意的情况下自立门户。
当网络分区恢复后,少数派子集会自动识别出多数派子集中的 Leader,并同步自己的状态以追上最新数据。
启动期间的故障
集群的引导(Bootstrap)过程需要所有初始成员成功启动才能完成。如果在引导期间发生任何故障(例如,某个节点启动失败),最佳实践是清除所有节点上的数据目录,并使用新的集群 Token 或发现令牌重新引导集群。
虽然也可以像处理运行中集群的故障一样去恢复一个引导失败的集群,但这通常比重新引导一个新集群更加耗时且复杂,因为引导失败的集群通常没有需要恢复的有效数据。
快照备份创建
数据备份是灾难恢复的基石。etcd 提供了两种主要方式来创建数据快照:一种是通过 etcdctl 命令行工具从运行中的集群进行热备份,另一种是直接复制数据目录中的物理文件。
使用 etcdctl 进行热备份
这是推荐的备份方式,因为它可以在不中断服务的情况下获取一致性的数据快照。
ETCDCTL_API=3 etcdctl --endpoints https://127.0.0.1:2379 snapshot save snapshot.db
执行该命令后,etcdctl 会连接到指定的 etcd 端点,创建一个一致性快照并将其保存到本地的 snapshot.db 文件中。这个文件包含了在快照创建时刻整个键空间的状态。
直接复制数据文件
另一种方式是直接从 etcd 的数据目录(通常由 --data-dir 参数指定)中复制 member/snap/db 文件。这种方式虽然简单,但存在一个风险:直接复制文件可能会遗漏那些已经写入到预写日志(WAL, Write-Ahead Log)但尚未刷新到数据库文件中的数据。因此,这种方式不如 etcdctl snapshot save 安全。
检查快照状态
创建完快照后,我们可以使用 etcdutl 工具来查看快照的元数据信息,确认其包含的修订版本和数据规模。
$ etcdutl snapshot status snapshot.db -w table
+---------+----------+------------+------------+
| HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
+---------+----------+------------+------------+
| 7ef846e | 485261 | 11642 | 94 MB |
+---------+----------+------------+------------+
输出信息清晰地展示了快照的哈希值、包含的最高修订版本(Revision)、键的总数以及文件大小。这对于评估备份的新旧程度非常有帮助。
集群恢复流程
当发生灾难性故障,例如集群多数节点永久丢失时,我们需要利用之前创建的快照来恢复一个全新的 etcd 集群。
恢复前的考量:修订版本回退问题
在执行恢复操作之前,必须理解一个关键概念:修订版本(Revision)回退。
快照是某个时间点的数据状态。如果我们将一个一周前的快照恢复到一个新集群中,这个新集群的最高修订版本会比当前生产环境中的旧集群低很多。对于依赖 etcd Watch API 的客户端(如 Kubernetes 的控制器和 Operator),它们通常使用本地缓存和监听机制。如果集群的修订版本突然大幅回退,这些客户端可能无法正确刷新缓存,导致控制器行为异常或数据不一致。
因此,在恢复快照时,特别是当有客户端依赖 Watch API 或本地缓存时,强烈建议使用“修订版本 bump”功能,人为地将新集群的起始修订版本抬高,确保它永远不低于恢复前的旧集群修订版本。
执行恢复操作
恢复集群需要一个快照数据库文件(snapshot.db)。使用 etcdutl snapshot restore 命令可以创建新的 etcd 数据目录。
$ etcdutl snapshot restore snapshot.db --data-dir /var/lib/etcd/new-data
这个命令会基于 snapshot.db 的内容在 /var/lib/etcd/new-data 目录下生成新的 etcd 数据文件。
重要提示:etcdutl snapshot restore 会重写快照中的元数据,例如成员 ID 和集群 ID。这意味着恢复后的新成员将拥有全新的身份,它不会意外地加入到旧的、可能仍然存在的集群中。因此,通过这种方式恢复的集群必须被视为一个全新的逻辑集群。
完整性检查
如果快照是通过 etcdctl snapshot save 创建的,它会包含一个完整性哈希。etcdutl snapshot restore 默认会检查这个哈希以确保快照文件未被损坏。如果快照是通过直接复制 member/snap/db 文件获得的,它不包含完整性哈希,此时需要使用 --skip-hash-check 参数来跳过哈希检查。
带修订版本 bump 的恢复
为了应对修订版本回退问题,我们可以在恢复时使用 --bump-revision 参数。
$ etcdutl snapshot restore snapshot.db --data-dir /var/lib/etcd/new-data --bump-revision=1000000000
--bump-revision 接受一个 64 位整数,表示要在快照当前修订版本的基础上增加多少。这个值应该足够大,以覆盖从快照创建到恢复期间可能产生的所有新修订。例如,假设 etcd 每秒处理 1500 次写操作,那么一周(604800秒)大约会产生 9 亿次修订。将 bump 值设置为 10 亿可以确保新集群的修订版本远高于旧集群,从而避免客户端缓存问题。
数据损坏检测
etcd 内置了自动化的数据损坏检测机制,用于防止集群成员之间的状态出现分歧。启用这些检测机制是保障数据一致性的关键。
启用数据损坏检测
etcd 提供了三种方式来检测数据损坏:
初始检查:通过
--experimental-initial-corrupt-check标志启用。此检查在 etcd 成员启动期间执行,成员会将其持久化状态与其他成员进行比较,如果发现不匹配,成员将退出启动过程。周期性检查:在集群运行期间,由 Leader 节点执行。
- 压缩修订版本哈希检查:通过
--experimental-compact-hash-check-enabled标志启用。此检查在每次压缩(compaction)时计算哈希值,供集群成员间比对。 - 最新修订版本哈希检查:通过
--experimental-corrupt-check-time标志启用。此检查会扫描整个 etcd 内容并计算指定修订版本的校验和。
- 压缩修订版本哈希检查:通过
检查机制详解
压缩修订版本哈希检查:执行频率很高(默认每分钟一次,可通过
--experimental-compact-hash-check-time调整),性能开销极小,因为它不需要额外的数据库扫描,但要求集群定期进行压缩操作。它能有效发现慢速 Follower 的数据不一致问题。最新修订版本哈希检查:性能开销较大,因为它需要扫描整个 etcd 的内容。建议设置较长的执行周期(如几小时一次),以平衡性能和检测时效性。
当周期性检查发现数据不一致时,Leader 会触发 CORRUPT ALARM 警报。
成员故障替换
当检测到某个成员数据损坏或硬件故障无法恢复时,我们需要将其从集群中移除并添加一个新成员来替换它。
替换成员的步骤
- 停止故障实例:首先,停止故障的 etcd 实例。
- 备份数据:在进行任何破坏性操作前,务必备份故障节点的数据目录,以备后续分析或恢复。
- 移除故障成员:使用
etcdctl member remove命令将故障成员从集群元数据中移除。这需要提供故障成员的 ID。etcdctl member remove <member-id> - 添加新成员:使用
etcdctl member add命令添加一个新成员。需要指定新成员的名称和客户端 URL。etcdctl member add <new-member-name> --peer-urls=http://<new-member-ip>:2380 - 启动新成员:在新节点上启动 etcd 服务。启动命令中需要指定
--initial-cluster-state=existing,并且--initial-cluster参数必须包含集群中所有现有成员以及刚刚添加的新成员的信息。
新成员启动后,会从 Leader 节点同步最新的数据快照,最终加入集群并开始提供服务。
仲裁丢失恢复
当集群因多数节点故障导致仲裁丢失(Quorum Lost)时,集群将无法写入。此时,必须通过恢复快照来重建集群。
恢复流程
- 创建快照:如果可能,从一个仍然存活的、数据相对完整的节点上创建一个快照。如果所有节点都已宕机,则使用最近一次的备份快照。
- 准备新数据目录:为每个计划加入新集群的节点准备一个空的数据目录。
- 恢复数据:在每个节点上使用
etcdutl snapshot restore命令恢复数据。务必使用--initial-cluster参数指定新集群的成员列表,并使用--initial-cluster-token为新集群设置一个唯一的 Token。etcdutl snapshot restore snapshot.db \ --data-dir /var/lib/etcd/new-data \ --initial-cluster-token etcd-cluster-1 \ --initial-cluster new-member1=http://192.168.1.10:2380,new-member2=http://192.168.1.11:2380,new-member3=http://192.168.1.12:2380 \ --initial-cluster-state new - 启动新集群:使用恢复后的数据目录启动 etcd 实例。此时,这些节点将组成一个全新的、基于快照数据的集群。
通过这种方式,即使整个集群完全损毁,只要有一个有效的快照备份,我们就能完整地恢复 etcd 的数据,将损失降到最低。
本章我们深入探讨了 etcd 的故障处理与灾难恢复策略,从常见的故障模式分析,到数据备份、集群恢复、数据损坏检测以及成员替换等具体操作。这些知识是保障 etcd 在生产环境中稳定运行的最后防线。在下一章中,我们将转向生产环境的部署策略,讨论如何在 Kubernetes、Docker 等现代基础设施上高可靠地部署 etcd 集群。