Operator 多可用区选主优化与 Lease 版本分析
什么是 Lease? Lease 对象的核心作用是表示某个实体(通常是一个 Pod 或进程)对某项资源或角色的“持有权”,并且这种持有权是有时间限制的。通过定期续约(renew),持有者可以保持其控制权。如果持有者未能续约,租约到期后,其他实体可以接管。 常见的应用场景包括: 领导选举:在分布式系统中,确保只有一个实例(Leader)执行特定任务,其他实例作为 Follower。 资源协调:跟踪和管理资源的临时所有权。 心跳机制:通过续约时间(RenewTime)检测持有者是否仍然活跃。 Kubernetes 内部的一些组件(如 kube-controller-manager 和 kube-scheduler)就使用 Lease 来实现高可用性和领导选举。 Lease 的工作原理 创建租约: 一个进程(如你的代码)创建一个 Lease 对象,并声明自己为持有者(HolderIdentity)。 续约: 持有者需要定期更新 RenewTime,证明自己仍然活跃。通常通过客户端(如 kubectl 或 Go 客户端)调用 Kubernetes API 来更新。 失效与接管: 如果持有者未能及时续约(例如进程崩溃),其他进程可以通过检查 RenewTime 和 LeaseDurationSeconds 判断租约是否过期,并尝试接管。 领导选举: 多个实例竞争同一 Lease 对象时,只有成功创建或更新它的实例成为 Leader。 示例场景 假设你用这个 Lease 来实现领导选举: 你有一个分布式应用,有 3 个 Pod:pod-1、pod-2、pod-3。 它们都尝试创建或更新同一个 Lease 对象(例如 my-leader-lease)。 pod-1 成功创建,设置 HolderIdentity: “pod-1”,成为 Leader。 pod-1 每 10 秒更新 RenewTime,保持领导地位。 如果 pod-1 崩溃,RenewTime 未更新,pod-2 检测到租约过期,接管并更新 HolderIdentity: “pod-2”。 现有逻辑 // Copyright 2018 The Operator-SDK Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "time" "github.com/operator-framework/operator-sdk/pkg/k8sutil" // Operator-SDK 提供的工具包,用于获取运行环境信息(如命名空间、Pod 信息) corev1 "k8s.io/api/core/v1" // Kubernetes Core v1 API,包含 Pod、ConfigMap 等资源定义 apierrors "k8s.io/apimachinery/pkg/api/errors" // Kubernetes API 错误处理包 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // Kubernetes 元数据定义包,包含 ObjectMeta 等 "k8s.io/apimachinery/pkg/util/wait" // 提供等待和退避逻辑的工具 crclient "sigs.k8s.io/controller-runtime/pkg/client" // controller-runtime 提供的通用客户端,用于操作 Kubernetes 资源 "sigs.k8s.io/controller-runtime/pkg/client/config" // 获取 Kubernetes 配置(如 kubeconfig) ) // maxBackoffInterval 定义了尝试成为 Leader 时,最大退避等待时间(16秒)。 // 在多次尝试失败后,等待时间会逐渐增加,但不会超过这个值。 const maxBackoffInterval = time.Second * 16 // Become 函数确保当前 Pod 成为其命名空间内的 Leader。 // 如果代码运行在集群外(本地),会跳过领导选举直接返回 nil。 // 它通过创建一个带有当前 Pod 作为 OwnerReference 的 ConfigMap 来实现领导选举。 // ConfigMap 的名称是 lockName,同一时间只能有一个同名 ConfigMap 存在,成功创建者即为 Leader。 // 当 Leader Pod 终止时,垃圾回收机制会删除 ConfigMap,其他 Pod 可以接管。 func leaderBecome(ctx context.Context, lockName string) error { log.Info("Trying to become the leader.") // 日志:尝试成为 Leader // 获取当前 Operator 运行的命名空间 ns, err := k8sutil.GetOperatorNamespace() if err != nil { // 如果无法获取命名空间(可能是本地运行或无权限) if err == k8sutil.ErrNoNamespace || err == k8sutil.ErrRunLocal { log.Info("Skipping leader election; not running in a cluster.") // 日志:不在集群中,跳过选举 return nil } return err // 其他错误,返回 } // 获取 Kubernetes 配置(通常来自 kubeconfig 或 ServiceAccount) config, err := config.GetConfig() if err != nil { return err // 配置获取失败,返回错误 } // 创建 controller-runtime 的客户端,用于操作 Kubernetes 资源 client, err := crclient.New(config, crclient.Options{}) if err != nil { return err // 客户端创建失败,返回错误 } // 获取当前 Pod 的 OwnerReference,表示当前 Pod 是谁 owner, err := myOwnerRef(ctx, client, ns) if err != nil { return err // 获取 OwnerReference 失败,返回错误 } // 检查是否已存在由当前 Pod 拥有的 ConfigMap(可能是进程重启的情况) existing := &corev1.ConfigMap{} key := crclient.ObjectKey{Namespace: ns, Name: lockName} // ConfigMap 的键(命名空间+名称) err = client.Get(ctx, key, existing) switch { case err == nil: // ConfigMap 已存在 // 检查现有 ConfigMap 的 OwnerReferences for _, existingOwner := range existing.GetOwnerReferences() { if existingOwner.Name == owner.Name { // 如果 Owner 是当前 Pod log.Info("Found existing lock with my name. I was likely restarted.") // 日志:发现已有锁,可能是重启 log.Info("Continuing as the leader.") // 日志:继续作为 Leader return nil // 直接返回,当前 Pod 已是 Leader } log.Info("Found existing lock", "LockOwner", existingOwner.Name) // 日志:发现其他 Pod 持有的锁 } case apierrors.IsNotFound(err): // ConfigMap 不存在 log.Info("No pre-existing lock was found.") // 日志:没有预先存在的锁 default: // 其他错误 log.Error(err, "Unknown error trying to get ConfigMap") // 日志:获取 ConfigMap 时发生未知错误 return err } // 创建一个新的 ConfigMap,用作领导锁 cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: lockName, // ConfigMap 名称,由 lockName 指定 Namespace: ns, // 命名空间 OwnerReferences: []metav1.OwnerReference{*owner}, // 设置当前 Pod 为 Owner }, } // 尝试创建 ConfigMap,成为 Leader backoff := time.Second // 初始退避时间为 1 秒 for { // 无限循环,直到成功或上下文取消 err := client.Create(ctx, cm) // 尝试创建 ConfigMap switch { case err == nil: // 创建成功 log.Info("Became the leader.") // 日志:成功成为 Leader return nil // 返回,当前 Pod 成为 Leader case apierrors.IsAlreadyExists(err): // ConfigMap 已存在(其他 Pod 是 Leader) existingOwners := existing.GetOwnerReferences() // 获取当前 ConfigMap 的 Owner switch { case len(existingOwners) != 1: // ConfigMap 应该只有一个 Owner log.Info("Leader lock configmap must have exactly one owner reference.", "ConfigMap", existing) case existingOwners[0].Kind != "Pod": // Owner 必须是 Pod log.Info("Leader lock configmap owner reference must be a pod.", "OwnerReference", existingOwners[0]) default: // 正常情况:ConfigMap 由一个 Pod 拥有 leaderPod := &corev1.Pod{} key = crclient.ObjectKey{Namespace: ns, Name: existingOwners[0].Name} // 当前 Leader Pod 的键 err = client.Get(ctx, key, leaderPod) // 获取 Leader Pod 信息 switch { case apierrors.IsNotFound(err): // Leader Pod 已删除 log.Info("Leader pod has been deleted, waiting for garbage collection do remove the lock.") // 日志:等待垃圾回收删除 ConfigMap case err != nil: // 获取 Pod 失败 return err case isPodEvicted(*leaderPod) && leaderPod.GetDeletionTimestamp() == nil: // Leader Pod 被驱逐但未标记删除 log.Info("Operator pod with leader lock has been evicted.", "leader", leaderPod.Name) // 日志:Leader 被驱逐 log.Info("Deleting evicted leader.") // 日志:删除被驱逐的 Leader err := client.Delete(ctx, leaderPod) // 删除 Leader Pod if err != nil { log.Error(err, "Leader pod could not be deleted.") // 日志:删除失败 } case leaderPod.GetDeletionTimestamp() != nil: // Leader Pod 已被标记删除(新增逻辑) log.Info("Operator pod with leader lock has been terminating/completed.", "leader", leaderPod.Name) // 日志:Leader Pod 正在终止 log.Info("Deleting a-lock cm.") // 日志:删除 ConfigMap errCm := client.Delete(ctx, cm) // 删除 ConfigMap if errCm != nil { log.Error(errCm, "Leader pod could not be deleted.") // 日志:删除 ConfigMap 失败 } err := client.Delete(ctx, leaderPod) // 删除 Leader Pod if err != nil { log.Error(err, "Leader pod could not be deleted.") // 日志:删除 Pod 失败 } default: // Leader Pod 仍然活跃 log.Info("Not the leader. Waiting.") // 日志:当前不是 Leader,继续等待 } } // 等待一段时间后重试,带有随机抖动(Jitter)避免所有 Follower 同时竞争 select { case <-time.After(wait.Jitter(backoff, .2)): // 等待 backoff 时间(带 20% 随机抖动) if backoff < maxBackoffInterval { // 如果退避时间未达到最大值 backoff *= 2 // 退避时间翻倍 } continue // 继续下一次循环 case <-ctx.Done(): // 上下文取消 return ctx.Err() // 返回上下文错误 } default: // 创建 ConfigMap 时发生其他错误 log.Error(err, "Unknown error creating ConfigMap") // 日志:未知错误 return err } } } // myOwnerRef 返回当前运行 Pod 的 OwnerReference。 // 它依赖环境变量 POD_NAME(通过 downwards API 设置)来识别当前 Pod。 func myOwnerRef(ctx context.Context, client crclient.Client, ns string) (*metav1.OwnerReference, error) { // 获取当前 Pod 的信息 myPod, err := k8sutil.GetPod(ctx, client, ns) if err != nil { return nil, err // 获取失败,返回错误 } // 构造 OwnerReference owner := &metav1.OwnerReference{ APIVersion: "v1", // API 版本 Kind: "Pod", // 资源类型 Name: myPod.ObjectMeta.Name, // Pod 名称 UID: myPod.ObjectMeta.UID, // Pod 的唯一标识符 } return owner, nil } // isPodEvicted 检查 Pod 是否被驱逐(Evicted)。 // Pod 被驱逐时,Phase 为 Failed,且 Reason 为 "Evicted"。 func isPodEvicted(pod corev1.Pod) bool { podFailed := pod.Status.Phase == corev1.PodFailed // Pod 状态为 Failed podEvicted := pod.Status.Reason == "Evicted" // 原因是 Evicted return podFailed && podEvicted // 两者都满足则为被驱逐 } 现有逻辑总结 目标: 通过创建一个 ConfigMap(名称为 a-lock)来实现领导选举,成功创建者成为 Leader。 ConfigMap 的 OwnerReference 指向当前 Pod,当 Pod 删除时,ConfigMap 会被垃圾回收。 流程: 检查环境:如果不在集群中运行,跳过领导选举。 检查现有锁:如果已有由当前 Pod 拥有的 ConfigMap,直接恢复为 Leader。 创建锁:尝试创建新的 ConfigMap,如果失败(已有其他 Leader),检查当前 Leader 状态。 处理失效: 如果 Leader Pod 已删除,等待垃圾回收。 如果 Leader Pod 被驱逐或标记删除,主动删除 Pod 和 ConfigMap。 否则,等待并重试(带有退避机制)。 与 Lease 的对比 相似点:都用于领导选举,依赖 Kubernetes API 的原子性操作。 不同点: Lease 是专门为领导选举设计的轻量资源,内置续约机制(RenewTime)。 ConfigMap 是通用资源,通过 OwnerReference 和垃圾回收间接实现接管,逻辑更复杂。 请求开销: Lease 需要定期续约和查询。 此代码使用轮询和退避,Follower 需反复尝试创建 ConfigMap,也会产生多次请求。 为什么基于 ConfigMap 的方案在主机房断电后表现不佳?(另一个Operator没能及时成为Leader) 1. ConfigMap 依赖垃圾回收机制 机制: 在现有代码中,ConfigMap 的 OwnerReference 指向 Leader Pod。当 Leader Pod 被删除时,Kubernetes 的垃圾回收器(Garbage Collector, GC)会自动删除 ConfigMap,从而允许新的 Pod 成为 Leader。 主机房断电的影响: 如果主机房断电,运行 Leader Pod 的节点会突然下线。此时,Kubernetes 集群需要检测到节点不可用,并将该节点上的 Pod 标记为删除(DeletionTimestamp)。 问题:节点失联后,Kubernetes 不会立即认为 Pod 已删除,而是依赖 Node Controller 和 Taint-based Eviction 等机制来更新状态,这个过程可能需要几分钟甚至更久。 延迟来源: Node NotReady 检测:Master 检测节点失联通常依赖 node.kubernetes.io/not-ready 和 node.kubernetes.io/unreachable 污点,默认超时(node-monitor-grace-period)是 40 秒。 Pod Eviction 超时:Pod 被驱逐的默认超时(eviction-hard 或 eviction-soft)可能配置为几分钟(常见默认值如 5 分钟)。 垃圾回收延迟:即使 Pod 被标记删除,GC 删除 ConfigMap 也有一定延迟(通常几秒到几分钟,取决于集群负载和 GC 调度)。 2. 代码中的轮询和退避逻辑 机制: 现有代码使用轮询方式尝试创建 ConfigMap,如果失败(IsAlreadyExists),会检查现有 Leader Pod 状态,并以指数退避(backoff)等待。 退避时间从 1 秒开始,每次翻倍,直到最大值 maxBackoffInterval = 16 秒。 主机房断电的影响: 当 Leader Pod 的节点断电后,Follower Pod 检测到 ConfigMap 仍存在,但无法立即接管。它会进入等待循环,每次等待时间逐渐增加(1s → 2s → 4s → 8s → 16s)。 问题:如果 GC 迟迟未删除 ConfigMap,Follower 的等待时间会累积。例如,等待 5 次退避的总时间是 1 + 2 + 4 + 8 + 16 = 31 秒,但如果需要更多次(因为 GC 延迟),时间会更长。 延迟来源: 退避累积:退避逻辑本身会拖慢接管速度。 未优化检测:代码依赖主动轮询,而非实时监听(Watch),无法立即感知 ConfigMap 删除。 3. Kubernetes 集群的容错机制 机制: Kubernetes 在设计时倾向于“谨慎”处理节点失联,避免误判(false positive)。这意味着在确认节点和 Pod 真正失效前,会有较长的宽限期。 主机房断电的影响: 如果整个主机房断电,多个节点可能同时失联,Master 的 Node Controller 和 API Server 负载会激增,导致状态更新变慢。 问题:Pod 和 ConfigMap 的状态更新可能被推迟,进一步延长新 Leader 上任时间。 延迟来源: Node Controller 过载:大规模节点失效可能导致状态同步延迟。 API Server 压力:大量资源状态变更可能使 API 调用变慢。 4. 代码中的改进逻辑(DeletionTimestamp) 机制: 现有代码新增了检查 leaderPod.GetDeletionTimestamp() != nil,当 Leader Pod 被标记删除时,主动删除 ConfigMap 和 Pod。 主机房断电的影响: 这个逻辑依赖 Pod 被标记删除,但断电后 Pod 状态更新依赖 Node Controller,可能需要几分钟。 问题:如果节点失联时间过长,DeletionTimestamp 不会立即设置,主动删除 ConfigMap 的逻辑无法触发。 延迟来源: 状态更新延迟:节点失联到 Pod 被标记删除的时间(默认可能 5-10 分钟)。 断电场景:15 分钟的延迟虽然不常见,但并非完全不合理,尤其在以下条件下: Node Eviction 超时较长:如果集群配置了较长的宽限期(如 kube-controller-manager 的 –pod-eviction-timeout=5m 或更高),Pod 状态更新可能延迟 5-10 分钟。 GC 延迟:大规模断电后,GC 处理大量资源,调度延迟可能累加到几分钟。 退避累积:Follower 的退避逻辑在极端情况下可能执行多次(比如 10-20 次),每次 16 秒,总计 160-320 秒(约 3-5 分钟)。 集群负载:如果 Master 或 API Server 因断电事件过载,状态同步可能进一步推迟。 估算: Node 失联检测:5 分钟(默认宽限期)。 Pod 驱逐 + GC:2-5 分钟。 Follower 退避:3-5 分钟。 与 Lease 方案的对比 Lease 的优势: 主动续约:Leader 定期更新 RenewTime,Follower 可通过时间戳判断租约是否过期,无需依赖 GC。 更快接管:租约过期后(通常 15-30 秒),Follower 可立即尝试接管。 典型延迟:在断电场景下,接管时间通常在几十秒到 1-2 分钟(取决于 LeaseDurationSeconds 和网络恢复)。 现有 ConfigMap 方案: 接管时间受限于 Node 状态更新和 GC,异常场景下可能高达数十分钟。 Lease 资源在 Kubernetes 中的版本局限性分析 背景 Kubernetes 中的 Lease 资源(隶属 coordination.k8s.io API 组)用于节点心跳和领导选举等协调机制,是一个不算核心但也不可忽视的功能。然而,官方文档(https://kubernetes.io/docs/reference/)对Lease 的版本演进记录极为有限: ...