Leader Election
Spring Cloud Kubernetes 领导者选举机制利用 Kubernetes ConfigMap 实现了 Spring Integration 的领导者选举 API。
多个应用程序实例竞争领导权,但领导权只会授予其中一个。当获得领导权时,领导者应用程序会收到带有领导权Context的OnGrantedEvent应用程序事件。应用程序会定期尝试获取领导权,领导权将授予第一个调用者。领导者将保持领导地位,直到它被从集群中移除,或者它主动放弃领导权。当领导权被移除时,前领导者会收到OnRevokedEvent应用程序事件。移除后,集群中的任何实例都可能成为新的领导者,包括旧的领导者。
要将其包含在你的项目中,请添加以下依赖项。Fabric8 Leader Implementation
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-fabric8-leader</artifactId>
</dependency>
要指定用于领导者选举的 ConfigMap 名称,请使用以下属性。
spring.cloud.kubernetes.leader.config-map-name=leader
Leader Election Info Contributor
Spring Cloud Kubernetes Leader 包含一个 InfoContributor,它将领导者选举信息添加到 Spring Boot 的 /actuator/info 端点。该贡献者提供有关当前领导者的信息,包括领导者 ID、角色以及当前应用程序实例是否为领导者。
示例输出:
{
"leaderElection": {
"leaderId": "my-app-pod-1",
"role": "my-role",
"isLeader": true
}
}
你可以通过在 application.[properties | yaml] 中将 management.info.leader.enabled 设置为 false 来禁用这个 InfoContributor:
management.info.leader.enabled=false
配置领导者选举还有另一种方式,它在 fabric8 和 kubernetes 客户端中都得到了原生支持。从长远来看,这将成为配置领导者选举的默认方式,而之前的方式将被弃用。你可以将其视为类似于 JDK 的“预览”功能。
要使用它,你需要设置属性:
spring.cloud.kubernetes.leader.election.enabled=true
与旧实现不同,新实现将根据集群版本使用 Lease 或 ConfigMap 作为锁。即使集群支持 Leases,您仍可通过以下方式强制使用 ConfigMap:
spring.cloud.kubernetes.leader.election.use-config-map-as-lock=true
该 Lease 或 ConfigMap 的名称可以通过以下属性定义(默认值为 spring-k8s-leader-election-lock):
spring.cloud.kubernetes.leader.election.lockName=other-name
锁创建所在的命名空间(如果没有显式设置,则默认为 default)也可以进行设置:
spring.cloud.kubernetes.leader.election.lockNamespace=other-namespace
在领导者选举过程开始之前,您可以等待 Pod 准备就绪(通过就绪检查)。此功能默认启用,但您可以根据需要禁用它:
spring.cloud.kubernetes.leader.election.waitForPodReady=false
与旧实现类似,默认情况下我们会发布事件,但可以禁用此功能:
spring.cloud.kubernetes.leader.election.publishEvents=false
有几个参数控制着领导者选举过程的具体执行方式。为了解释这些参数,我们需要先了解这一过程的高层实现机制。所有候选 Pod 都会尝试成为领导者,或者说尝试获取锁。如果锁已被占用,它们会继续尝试获取,每次重试的间隔为 spring.cloud.kubernetes.leader.election.retryPeriod(该值以 java.time.Duration 格式指定,默认值为 2 秒)。
如果锁未被占用,当前 Pod 将成为领导者。它通过向锁(Lease 或 ConfigMap)中插入一个所谓的“记录”来实现这一点。该“记录”包含的内容之一是 leaseDuration(您可以通过 spring.cloud.kubernetes.leader.election.leaseDuration 指定;默认值为 15 秒,类型为 java.time.Duration)。这类似于锁的 TTL:除非此期限(从最后一次续约时间起)已过期,否则其他候选者无法获取该锁。
一旦某个Pod通过获取锁确立自己为领导者,它将持续(每隔spring.cloud.kubernetes.leader.election.retryPeriod)尝试续租其租约,换句话说:它将尝试延长其领导地位。当续租发生时,存储在锁内的"记录"会被更新。例如,记录中的renewTime会被更新,以表示最后一次续租发生的时间。(您始终可以通过使用kubectl describe lease…等命令来查看这些字段的内容)。
续期必须在特定的时间间隔内完成,该间隔由 spring.cloud.kubernetes.leader.election.renewDeadline 指定。默认情况下,该间隔等于 10 秒,这意味着领导者 Pod 最多有 10 秒的时间来续期其领导权。如果未能完成续期,该 Pod 将失去领导权,并重新开始领导者选举。由于其他 Pod 默认每 2 秒尝试成为领导者,这意味着刚刚失去领导权的 Pod 可能会再次成为领导者。如果您希望其他 Pod 有更高的机会成为领导者,可以设置以下属性(以秒为单位指定,默认值为 3):
spring.cloud.kubernetes.leader.election.wait-after-renewal-failure=3
这意味着无法续租并失去领导权的 Pod 将等待这么多秒后,再尝试重新成为领导者。
让我们尝试通过一个例子来解释这些设置:有两个参与领导者选举的 Pod。为简化起见,我们称它们为 podA 和 podB。它们都在同一时间启动:12:00:00,但 podA 将自己确立为领导者。这意味着每两秒(retryPeriod),podB 都会尝试成为新的领导者。所以在 12:00:02,然后在 12:00:04,依此类推,它基本上会问:"我能成为领导者吗?"。在我们简化的例子中,这个问题的答案可以根据 podA 的活动来回答。
在 podA 成为领导者后,每 2 秒,它会尝试"延长"或续约其领导权。因此,在 12:00:02,然后是 12:00:04,依此类推,podA 会访问锁并更新其记录,以表明它仍然是领导者。在上一次成功续约和下一次续约之间,它正好有 10 秒的时间(renewalDeadline)。如果在这 10 秒内它未能续约其领导权(存在连接问题或长时间的 GC 暂停等),它将停止领导,此时 podB 可以获取领导权。当 podA 以优雅的方式停止担任领导者时,锁记录会被"清除",这基本上意味着 podB 可以立即获取领导权。
当 podA 因 OutOfMemory 等原因死亡时,情况就不同了,它无法优雅地更新锁记录,这时 leaseDuration 参数就变得至关重要。最简单的解释方式是通过一个例子:
podA 在 12:00:04 续租了其领导权,但在 12:00:05 被 OOMKiller 杀死。在 12:00:06,podB 将尝试成为领导者。它将检查"现在" (12:00:06) 是否晚于上次续租时间 + 租约持续时间,本质上它将检查:
12:00:06 > (12:00:04 + 00:00:10)
条件未满足,因此它无法成为领导者。在 12:00:08、12:00:10 等时间点也会得到相同的结果,直到 12:00:16,此时锁的 TTL(leaseDuration)将过期,podB 可以获取它。因此,较低的 leaseDuration 值意味着其他 Pod 能更快地获取领导权。
你可能需要授予适当的RBAC权限才能使用此功能,例如:
- apiGroups: [ "coordination.k8s.io" ]
resources: [ "leases" ]
resourceNames: [ "spring-k8s-leader-election-lock" ]
verbs: [ "get", "update", "create" ]
- apiGroups: [ "" ]
resources: [ "configmaps" ]
resourceNames: [ "spring-k8s-leader-election-lock" ]
verbs: [ "get", "update", "create" ]