土法炼钢 · 系统与基础设施

【etcd】单 Raft 组与服务器角色:Leader、Follower、Learner 与 request 路由

文章导航

分类入口
distributedkubernetes
标签入口
#etcd#raft#leader#follower#learner#etcdserver#single-raft-group#v3.5.33

目录

Raft 深度重写 已把 PreVote、ReadIndex、ConfChange V2 等工程补丁钉到 go.etcd.io/raft/v3 源码精度;Raft 实现拆解 走过 raft.Node 的消息循环。但 etcd 服务器如何把 gRPC 请求接到 Raft、Follower 收到写请求后如何转发、Learner 为何不参与 quorum——仍需要单独一篇,否则排障时会把「Raft 库行为」与「etcdserver 路由策略」混为一谈。

本文钉三件事:整个集群只有一个 Raft 组EtcdServer 与 raft.Node 的职责边界Leader / Follower / Learner 三种角色下的 request 路由。与 distributed/13 的分工口令:13 讲 Raft 算法与库;本篇讲 etcd 如何嵌入该库。

本篇在系列中的位置

篇目 核心内容
第 1 篇 · 生产全景 缺口、五轴、16 篇路线
第 2 篇 · 单 Raft 组与服务器角色 Leader/Follower/Learner;request 路由
第 3 篇 · WAL 与快照 WAL 段、commit vs applied

版本锚定:etcd v3.5.33(源码 tag v3.5.33)。EtcdServer 定义见 server/etcdserver/server.go;Raft 嵌入见 server/etcdserver/raft.go;成员 Learner 标记见 server/etcdserver/api/membership/member.goRaftAttributes.IsLearner无真实集群则不粘贴伪造 etcdctl endpoint status 的 Raft 字段。


一、单 Raft 组:与 Multi-Raft 的分叉点

etcd 集群中所有 key-value 数据同一个 Raft 实例管理。无论集群是 3 节点还是 5 节点,日志条目顺序全局唯一;MVCC 的 main revision 递增也建立在这份全局日志之上。

这与 TiKV 的 Multi-Raft(每个 Region 独立 Raft 组)形成鲜明对比——也是 分布式 KV 对比 里 etcd 写吞吐有硬顶的根本原因:写路径无法按 key 分片并行复制。

flowchart TB
  subgraph cluster ["Single Raft Group"]
    L["Leader"]
    F1["Follower"]
    F2["Follower"]
    LR["Learner optional"]
  end
  C["gRPC clients"] --> L
  C --> F1
  C --> F2
  L -->|"MsgApp replicate"| F1
  L -->|"MsgApp replicate"| F2
  L -->|"replicate no vote"| LR

工程后果

属性 单 Raft 组收益 代价
全局 Revision 全序 Watch、Txn、resourceVersion 语义简单 写吞吐不随节点数线性扩展
运维模型 member add/remove 一套流程 大 keyspace 仍共享一份 WAL
故障域 quorum 丢失即整集群不可写 不能按租户隔离 Raft 组

Li et al.(;login: 2017)把 etcd 定位为配置与服务发现的小规模 KV——单 Raft 组是刻意的设计选择,不是「还没做分片」。当 quota 与单写者(bbolt)同时触顶时,应评估迁 Multi-Raft 系统(TiKV)或拆分 etcd 集群,而不是继续加 Follower 期望写性能提升。


二、EtcdServer 与 go.etcd.io/raft/v3 的边界

EtcdServer 是 etcd 进程的核心状态机宿主。v3.5.33 中其关键字段包括:

// etcd-io/etcd v3.5.33, server/etcdserver/server.go(节选)
type EtcdServer struct {
    appliedIndex   uint64 // MVCC 已应用到的 Raft index
    committedIndex uint64 // Raft 已提交的 index
    term           uint64
    lead           uint64 // 当前 Leader 的 member ID
    r              raftNode
    kv             mvcc.WatchableKV
    lessor         lease.Lessor
    be             backend.Backend
    // ...
}

分工表——排障时先判断问题落在哪一侧:

层级 包/组件 职责 典型失败
gRPC/API server/etcdserver/v3_server.go 鉴权、请求校验、路由 请求过大、auth 拒绝
共识 go.etcd.io/raft/v3 via raftNode 选举、日志复制、ReadIndex no leader、term 跳变
持久化 server/walsnap.Snapshotter WAL 段、快照 corrupt WAL(见第 3 篇)
状态机 applyV3server/mvcc 把已提交 Entry 应用到 KV apply lag、quota
成员 membership.RaftCluster member 元数据、Learner 标记 join 卡住、promote 失败

raftNodeserver/etcdserver/raft.go)是 etcdserver 与 raft.Node 之间的适配层:它消费 Ready() 中的 EntriesHardStateSnapshot,写入 WAL,经 rafthttp 与 peer 交换消息,再把可 apply 的条目交给 EtcdServer

边界口令

Ongaro & Ousterhout(2014)的复制状态机模型在这里落地:Raft 保证日志一致;etcd 的 MVCC store 是状态机实现。


三、Leader、Follower、Learner 三种角色

3.1 Leader

Leader 是唯一接受 propose 的节点(成员变更 ConfChange 亦然)。客户端 gRPC 连到 Leader 时,写路径为:

KV/Txn/Compaction request
  → EtcdServer 序列化为 raftpb.Entry
  → raftNode.Propose
  → 复制到 quorum
  → committedIndex 推进
  → apply 协程 → MVCC/bbolt

Leader 还负责 ReadIndex 线性一致读(第 8 篇):在返回读结果前,需确认自己仍是 Leader 且已应用至 ReadIndex 指定的 commit 点。

EtcdServer.lead 字段(atomic)保存当前 Leader 的 member ID;term 保存当前 Raft term。etcdctl endpoint status有真实环境时可对照 Raft TermRaft IndexLeader 字段——本篇不粘贴伪造输出。

3.2 Follower

Follower 接收 Leader 的 MsgApp,持久化到 WAL,回复 ACK。它直接 propose 客户端写请求。

当 gRPC 写请求落到 Follower 时,etcdserver 会转发给 Leader(经 rafthttp 或 gRPC 内部路径,取决于请求类型与版本配置)。客户端可能感知到额外 RTT,但不会破坏线性一致性。

Follower 可读:

3.3 Learner

Learner 是 non-voting member(Raft 论文扩展的工程化)。v3.5.33 在 membership.RaftAttributes 中标记:

// server/etcdserver/api/membership/member.go(节选)
type RaftAttributes struct {
    PeerURLs  types.URLs
    IsLearner bool `json:"isLearner,omitempty"`
}
能力 Voting member Learner
接收日志复制
参与 quorum 计数
参与 Leader 选举投票
典型用途 生产副本 新节点 catch-up、跨地域观测副本

Learner 的设计动机是 member add 不扰动 quorum:先把新节点以 Learner 身份拉入并复制完全量日志,再 promote 为 voting member——避免 Ongaro 论文 §6 成员变更中「临时双 majority」的经典坑(ConfChange V2 在 distributed/13 已述)。

排障提示:Learner 节点 etcdctl endpoint status 可能显示较高 Raft Applied Index lag 但仍不参与选举;把它当 Follower 做 quorum 计算会误判集群健康。


四、Request 路由:写、读、Watch、Lease

sequenceDiagram
  participant C as Client
  participant S as EtcdServer
  participant R as raftNode
  participant MVCC as MVCC store
  C->>S: Put / Txn
  alt local is Leader
    S->>R: Propose entry
    R->>R: replicate to quorum
    R->>S: committed entries
    S->>MVCC: apply
  else local is Follower
    S->>S: forward to Leader
  end
  C->>S: Watch
  S->>MVCC: watchableStore subscribe
  Note over MVCC: no Raft for stream setup
  C->>S: Lease KeepAlive
  S->>S: lessor renew local path
  Note over S: TTL path see part 10

4.1 写路径(Put / Txn / Delete / Compaction)

所有 mutating v3 请求最终变成 一条或多条 Raft Entry(Txn 是单个 Entry 内多个 op)。必须经过 Leader + quorum commit,再 apply。

Follower 收到写请求 → 转发 Leader → 与直连 Leader 等价,只是多一跳。

4.2 读路径(Get / Range)

读请求默认不走 Raft propose(除非触发 ReadIndex 线性一致读)。Serializable 读直接查本地 WatchableKV——在 Follower 上可能读到未追平的旧状态。

这是「apiserver 读 etcd 是否 stale」争论的工程根源(第 8、13 篇展开)。

4.3 Watch

Watch 建立 gRPC 流后,由 watchableStoreapply 之后推送事件。historical revision 追赶从 bbolt 读历史——不经过 Raft,但依赖 MVCC 尚未 compact 的版本保留。

4.4 Lease

Grant/Revoke 等会走 Raft;KeepAlive 在 Leader 本地 lessor 续期,不经 Raft 提交——Leader 切换时 TTL 语义会变化(第 10 篇)。这是 Lease 轴独立于 Raft 轴的关键原因。


五、与 distributed/13 的分工与开放问题

问题 去看
PreVote、选举风暴、term 跳变 distributed/13 §5
ReadIndex / LeaseRead 实现 distributed/13 + 本系列 08
ConfChange V2、joint consensus distributed/13 + 本系列 14
单组拓扑、Learner promote、request 转发 本篇 + 14

开放问题(有文献或源码依据)

  1. Learner 跨地域部署:复制 lag 不影响 quorum,但 promote 时仍需要一次 ConfChange——跨洋带宽下 promote 窗口多长才安全,官方文档给运维指南(B 级),无统一公式。
  2. Follower 读默认可用性:Serializable 读降低 Leader 负载,但 K8s 控制面是否应允许 follower 读 etcd 在工程上仍有争议——默认 apiserver 连 Leader 仍是常见部署。
  3. 单 Raft 组扩展性上限:Li et al. 2017 与 etcd 官方 Hardware guidelines 都指向磁盘 fsync 与 bbolt 单写者,而非 CPU——Multi-Raft 是架构分叉,不是调参能弥合的 gap。

六、本篇不写什么


参考资料

规范 / 官方文档 / 源码(A)

论文(A)

站内对照(A/B)

实验台账


上一篇生产全景

下一篇WAL 与快照

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .