Kubernete 高可用集群
Zhongjun Qiu 元婴开发者

Kubernetes 的高可用(High Availability, HA)集群设计旨在确保集群在面对节点故障、网络分区或其他意外情况时,仍能持续提供服务。本文将详细介绍 K8S 的核心组件及其高可用实现方式。

K8S核心组件

image-20250830215147462

K8S API Server

K8S 控制平面的核心组件,运行在 Master 节点上,负责接收和处理所有针对集群的 REST API 调用。

  • 为用户、控制平面组件以及外部系统提供统一的访问入口。
  • 所有模块通过 API Server 将数据写入 etcd;当需要获取或操作数据时,也通过 API Server 提供的 REST 接口(如 GET/LIST/WATCH)进行交互。
  • 用户通常通过 kubectl/kubeadm 等工具间接访问 API Server,也可以直接发送 REST 请求。

👉 接收流量对象:Scheduler、Controller、Kubelet、Kube-Proxy、kubectl/外部客户端 👉 发送流量对象:etcd

K8S Scheduler

K8S 的调度器,运行在 Master 节点上。

  • 持续监听 API Server,发现处于 待调度状态 的 Pod(即 PodSpec.NodeName 为空)。
  • 根据资源需求、节点负载、亲和/反亲和策略等条件筛选候选节点(预选)。
  • 对候选节点进行打分排序,选出最优节点(优选),并通过 Binding 对象 将调度结果写回 API Server。

👉 发送流量对象:API Server

K8S Controller

控制器管理器,运行在 Master 节点上,负责运行多个控制循环(Controller)。

  • 每个 Controller 负责维持某类资源的期望状态,例如:

    • NodeController:检测和管理节点健康;
    • ReplicaSetController:确保副本数与期望一致;
    • EndpointController:维护 Service 与 Endpoint 的对应关系;
    • 其他控制器:NamespaceController、ServiceAccountController 等。
  • 通过监听 API Server 获取资源的当前状态,当发现与期望状态不符时,触发相应的修复操作。

👉 发送流量对象:API Server(所有状态监控与修复操作均经由 API Server)

etcd

K8S 的分布式键值存储,提供强一致性和高可用性。

  • 保存 K8S 的 完整集群状态数据(如 Pod、Service、Secrets、ConfigMap 等)。
  • 仅有 API Server 可以直接读写 etcd,其他组件必须通过 API Server 进行访问。

👉 接收流量对象:API Server

Kubelet

运行在每个节点上的节点代理。

  • 接收来自 API Server 的 PodSpec,确保容器按规范运行。
  • 定期上报节点与 Pod 的状态信息。
  • 与容器运行时(如 Docker、containerd、CRI-O)交互,负责镜像拉取、容器创建与销毁。
  • 执行存活探针(liveness)与就绪探针(readiness),并上报容器健康状态。

👉 发送流量对象:API Server(获取 PodSpec、上报状态)

Kube-Proxy

运行在每个节点上的网络代理组件。

  • 维护 iptables/ipvs 规则,确保 Service 的流量能正确分发至对应的 Pod。
  • 保障集群内部 Pod 与 Service 的网络通信。
  • 在某些生产环境中,可能被 Ingress Controller、Nginx 或 HAProxy 等方案替代。

👉 发送流量对象:API Server(获取 Service 与 Endpoint 信息)、各节点的网络栈

K8S高可用(HA)集群

组件 HA 类型 原理 / 实现方式 关键说明
etcd 多副本 + Raft 单活 部署奇数个节点,Leader 处理写操作并通过 Raft 日志复制到 Follower,确保一致性 支持多数节点容错,保证已提交日志不丢失,未提交日志可被覆盖;节点≥3且为奇数最优
API Server 多副本 + 负载均衡 无状态服务,前置 LB(如 Nginx/LVS)实现请求分发 可多活,任何副本都可处理请求,客户端无需感知副本切换
Scheduler 多副本 + Leader Election 通过 Lease 锁对象选主,只有 Leader 执行业务循环 Leader 出故障时备用实例自动接管;保证单活执行避免冲突
Controller Manager 多副本 + Leader Election 同 Scheduler Leader 故障切换同 Scheduler;确保资源状态一致性
节点级组件(Kubelet、Kube-Proxy、实际服务 Pod) 控制平面管理 本质上都是运行在节点上的pod,由 控制平面调度和管理 节点上独立运行,通过控制平面调度和副本管理实现整体高可用;节点故障时 Pod 会被重新调度

K8S 的高可用本质上是保证集群中 Pod 的高可用。由于 Pod 的高可用可以通过 Deployment 等控制器来管理多个副本实现,因此 集群高可用的核心目标在于保证 K8S 控制平面关键组件的高可用

接下来对每一个控制组件的高可用进行分析:

etcd

etcd 通过部署多个实例形成集群来实现高可用。

  • 每个 etcd 节点都维护一份状态机,集群中始终只有一个有效的领导者(Leader)。
  • 所有写操作必须由 Leader 处理,并通过 Raft 协议 将操作日志同步到其他节点。
  • Raft 协议确保集群中所有节点的日志保持一致,从而保证最终存储的键值对数据一致。

Raft 集群中的每个节点在任意时刻都处于以下三种状态之一:

  1. 跟随者(Follower):被动角色,只响应来自 Leader 或 Candidate 的请求。如果长时间未收到 Leader 消息,会转为候选者。
  2. 候选者(Candidate):由跟随者超时转变而来,发起选举请求以争取成为 Leader。
  3. 领导者(Leader):唯一主动角色,负责处理所有客户端请求并将日志复制到其他节点。集群在任意时刻只能有一个 Leader。

存在以下几种状态转换操作:

领导者选举(Leader Election)

领导者选举是Raft协议的基石,它确保了集群中总有一个领导者来协调工作。

  1. 任期(Term): Raft使用一个递增的整数来表示任期。每个任期都以一次选举开始。当一个节点发起选举时,它会增加自己的任期号。
  2. 超时(Timeout): 每个跟随者都有一个随机的选举超时时间(通常在150-300毫秒之间)。如果在超时时间内没有收到来自领导者的心跳消息,跟随者就会开始一次新的选举。
  3. 成为候选者: 当一个跟随者超时后,它会:
    • 增加自己的任期号。
    • 将自己的状态从跟随者转换为候选者。
    • 给自己投一票。
    • 向集群中的其他所有节点发送RequestVote(请求投票)RPC。
  4. 投票: 其他节点收到RequestVote请求后,会根据以下规则进行投票:
    • 它会首先比较自己的任期号和候选者的任期号,如果候选者的任期号小于自己的任期号,该节点会立即拒绝投票,因为它认为候选者已经过时了。
    • 如果候选者的任期号大于自己的任期号,该节点会更新自己的任期号为候选者的任期号,然后将自己的状态变为跟随者。
    • 如果候选者的任期号等于自己的任期号,此时需要进一步比较日志新旧:由于每条日志会记录(term,index),所以仍然先比较最后一条日志的term,更新的优先;如果相等,再比较index,更大的优先。
  5. 选举结果: 候选者会等待其他节点的回复,选举有三种可能的结果:
    • 赢得选举: 如果一个候选者从集群中的大多数节点(超过一半)那里获得投票,它就赢得了选举,成为新的领导者。
    • 分裂投票(Split Vote): 多个候选者同时发起选举,票数分散,没有一个候选者获得多数票。这种情况下,候选者会等待一个新的随机超时时间,然后再次发起选举。
    • 被新领导者取代: 如果在等待投票期间,一个节点发现另一个节点发来的RPC的任期号比自己的大,它会立即放弃成为领导者的尝试,将自己的状态转为跟随者,并更新自己的任期号。

一旦选举出领导者,它会周期性地发送心跳消息(AppendEntries RPC,不带日志条目)给所有跟随者,以维持自己的领导者地位。

投票机制中一个候选者成为领导者,必须得到半数以上的票数才行,所以etcd高可用集群中节点数量至少是大于等于3的奇数

  1. 如果只有 1 个节点:没有容错能力,一旦宕机整个集群不可用
  2. 如果只有 2 个节点:半数以上=2,任意一个节点宕机后,另一个节点只能得到自己的1票,无法选出 Leader
  3. 对于节点数大于等于3的集群,偶数个节点并不会提升容错能力:例如3个节点,只能容忍1个出故障,因为最少需要剩下半数以上的节点,他们这些票都投给同一个节点才能让这个节点成为Leader;但对于4个节点,半数以上=3,所以也最多只能容忍1个出故障。

Raft协议中的每个跟随者节点的选举超时时间都是随机且不同的,好处是:

由于每个节点的超时时间不同,一个节点(比如它的超时时间最短)会比其他节点更早地转换为候选者并发送投票请求。这个“先发制人”的候选者很有可能在其他节点超时之前就获得了多数票,从而成功当选为领导者。一旦有领导者产生,其他节点就会转为跟随者,接受新领导者的心跳,避免了分裂投票的结果(防止一直超时,一直在选举)

日志复制(Log Replication)

日志复制是Raft协议的核心功能,它确保了集群中的所有状态机都执行相同的、顺序一致的命令,以实现最终所有机器存储的信息都一样。

  1. 客户端请求: 所有的客户端请求都必须先发送给领导者。

  2. 日志条目(Log Entry): 领导者将每个客户端请求作为一个日志条目追加到自己的日志中。每个条目包含:

    • 命令:要执行的操作(etcd里就是一个键值对存储信息)。
    • 任期号:该日志条目创建时的任期号。
    • 索引:日志条目在日志中的位置。
  3. 发送AppendEntries: 领导者收到日志条目后,会并发地向所有跟随者发送AppendEntries(追加日志条目)RPC。

    1
    2
    3
    4
    5
    6
    7
    8
    AppendEntries(
    term, # Leader 当前任期
    leaderId,
    prevLogIndex, # 新日志前一条日志的下标
    prevLogTerm, # 该日志的任期
    entries[], # 要追加的新日志(可能为空,表示心跳)
    leaderCommit # Leader 已提交的索引
    )
  4. 跟随者响应:

    • 如果跟随者的日志和领导者不一致,它会回复一个失败的响应。领导者会根据失败信息,尝试发送一个更旧的日志条目,直到找到匹配的点,然后从该点开始同步日志(假设最终匹配到的索引是t,那么代表索引t及其之前的日志也一定和领导者是相同的)
    • 如果跟随者成功接收并追加日志条目,它会回复领导者一个成功的响应
    • 最终目的是让跟随者的日志和当前领导者日志保持强一致
  5. 日志提交(Commit)

    • 当某条日志被复制到大多数节点时,Leader 将其标记为已提交;
    • Leader 将命令应用到状态机并返回客户端结果;
    • Leader 通过心跳通知 Follower 哪些日志已提交,Follower 随后应用到状态机。

日志提交的安全性保障

已提交日志的安全性

Raft的一个核心安全属性是领导者完全性(Leader Completeness)。这个属性保证了:如果一个日志条目在一个任期内被提交了(即被多数节点复制成功),那么这个条目一定会存在于后续所有当选的领导者的日志中。

  • 一个日志条目要被提交,必须被多数节点成功复制。
  • 一个候选者要当选为领导者,也必须获得多数节点的投票。
  • 这两个“多数”集合之间必然存在重叠。这意味着,一个新当选的领导者,它的日志中必然包含了所有在上一任期被提交的日志条目。

所以无论Leader还是Follower宕机,已提交日志都不会丢失,都会被慢慢同步到所有节点上。

未提交日志的潜在丢失

Raft 在处理日志冲突时,会强制覆盖未提交的日志。

这样做是安全的,因为:

  • 未提交日志尚未获得多数节点确认,不被视为集群的最终状态;
  • 客户端也不会收到成功响应,因此客户端知道该操作未被持久化,可以选择重试;
  • 覆盖这些日志正是为了保证集群一致性。

例如,Leader A 写了一条新日志,只成功发给了 1 个 Follower(没到多数)。这时候 Leader A 宕机,新的 Leader B 当选,它的日志没有这条未提交日志。那么 A 和那个 Follower 的日志就包含了“未提交条目”,此时Learder B会强制覆盖掉所有节点上这个日志。

但由于是未提交的,Leader A并不会向客户端发送成功响应,所以客户端是知道这条日志对应的操作是没有被写入集群中的,所以此时客户端会根据自己的需要判断是否重传这个操作,或者是执行其他操作。

因此未提交日志就是错误的日志,需要用新日志覆盖掉,只有提交的日志才被客户端认可。

API Server

API Server 作为集群中其他组件与 etcd 之间的中间层,本身并不存储任何状态数据,因此是一个 无状态服务

其高可用方案相对简单:通过部署多个 API Server 副本,并在其前面添加一个负载均衡组件(如 Nginx、LVS 或云负载均衡),即可保证外部访问的连续性与可靠性。

image-20250831173231803

K8S Scheduler & K8S Controller

SchedulerController Manager 都是 K8S 控制平面的无状态进程。它们的工作模式相同:从 API Server 读取各类资源(如 Pod、Node 和 ReplicaSet 等),执行决策或计算,然后将结果通过 API Server 写回(例如,为 Pod 创建 Binding 或更新资源状态)。

因此,为了实现高可用性,通常会部署多个 Scheduler 或 Controller Manager 实例。

然而,与 API Server 不同,它们不是被动地等待请求,而是主动地持续访问 API Server,以读取信息并执行相应的操作。如果多个实例同时执行相同类型的写入操作(例如,同时为同一个 Pod 绑定 Node,或同时创建/删除某个资源),可能会导致冲突或重复操作。

为了解决这一问题,K8S 内部采用了领导者选举(Leader Election)机制。该机制确保了在任何给定时间,多个 Scheduler 或 Controller 实例中只有一个处于活跃状态并执行任务,而其余的则保持备用。当活跃实例发生故障时,备用实例会自动通过选举接管其职责,从而保证了服务不中断。

下面简单说一下 K8S 的领导者选举机制:

锁对象与实现方式

K8S 的Leader Election依赖于在 API Server 上操作一个“锁对象”,从而实现分布式互斥

常见的锁类型有:

  • 传统方案:使用 Endpoints 或 ConfigMap 作为锁。这两种方式是早期实现,目前已不再推荐。
  • 推荐方案:使用 Leasecoordination.k8s.io/v1 的 Lease 资源)。Lease 资源专为租约型选主设计,具有轻量、更新开销小、语义清晰等优点,因此是目前的首选方案。

这个锁对象会记录关键元数据,例如当前持有者身份(holder identity)、租约持续时间、上次续约时间等,以便判断锁是否有效或已过期。

参与者的身份标识

每个参与选举的进程在启动时都会生成一个唯一的身份标识(例如,podname_uuidhostname+pid)。这个标识会被写入锁对象的 holder 字段,明确地表明“当前谁是领导者”。

三个重要时间参数

Leader Election涉及三个关键的时间参数,它们共同管理着选主流程:

  • LeaseDuration(租约时长):Leader在不续约的情况下,仍然被视为持有锁的最长时间。
  • RenewDeadline(续约截止):Leader必须在这个时间窗口内完成续约。如果续约失败并超过此截止时间,它将被认为续约失败。
  • RetryPeriod(重试间隔):当候选者尝试获取或续约锁失败时,需要等待的时间间隔。

这些参数的关系通常为:RetryPeriod 较短,而 RenewDeadline 则小于 LeaseDuration。这种配置确保了领导者会在租约到期前尽早尝试续约,从而降低因网络波动等原因导致误判的风险。

这些参数在 client-goleader-election 实现中是可配置的,可以根据具体的网络稳定性和部署环境进行调整。

Leader Election 主循环过程

核心是在每一个Scheduler或Controller实例上持续运行的循环,可以分解为以下几个步骤:

  1. 启动与初始化 每个进程副本启动后,首先生成唯一的身份标识,并读取或创建指定的锁对象。如果锁对象不存在,则会创建它。

  2. 尝试获取锁

    • 进程读取锁对象的当前状态(包括持有者和上次续约时间)。
    • 如果锁没有持有者,或者锁已过期,该候选者会尝试通过条件更新(乐观锁)来将自己的身份写入 holder 字段。
    • 如果写入成功,该实例即成为领导者,并触发 OnStartedLeading 回调函数,开始执行其业务逻辑(例如调度或控制器循环)。
  3. 领导者续约

    • 成为领导者后,实例会在一个周期内定期向 API Server 更新锁对象,以刷新 renewTime 字段。此举表明自己依然活跃并持有锁。
    • 必须在 LeaseDuration 到期前完成续约。
    • 如果领导者连续续约失败并达到设定的阈值,client-go 会触发 OnStoppedLeading,该实例将放弃领导者身份,并停止执行其专属任务。
  4. 候选者等待与重试

    • 如果锁已被其他实例持有且未过期,候选者不会立刻进行抢夺。它会等待 RetryPeriod 指定的时间后,再次重试读取并检查锁状态。
    • 如果检测到锁的持有者发生变更(例如发现新的 holderIdentity),会触发 OnNewLeader 回调,常用于日志记录或监控。
  5. 领导权争夺 当领导者无法续约(例如因网络断连或进程崩溃)导致租约过期时,多个备用候选者会几乎同时尝试条件更新锁对象。由于 API Server 对资源的更新是原子化的,最终只有一个请求会成功写入。其他请求会因为版本冲突而失败并进入重试,从而选出新的领导者。

原子性与一致性保障

  • 领导者选举的互斥性完全依赖于 API Server 对资源的原子更新。当多个候选者同时尝试更新锁对象时,API Server 的乐观并发控制机制(通常基于 resourceVersion)会确保只有第一个成功写入的请求被接受,其他请求会因版本不匹配而失败。
  • 由于所有操作都通过 API Server 并最终写入 etcd,这利用了 etcd 集中式存储的一致性语义,有效地避免了 脑裂(split-brain)问题,确保在任何时刻都不会有两个实例同时被长期认定为领导者。
 REWARD AUTHOR
 Comments
Comment plugin failed to load
Loading comment plugin