最初由博客发布
kubernetes 标准库充满了宝石,隐藏在生态系统中的许多不同的子包中。我最近发现了一个这样的例子 k8s.io/client-go/tools/leaderelection,它可用于向 kubernetes 集群内运行的任何应用程序添加领导者选举协议。本文将讨论什么是领导者选举,它是如何在这个 kubernetes 包中实现的,并提供一个示例来说明如何在我们自己的应用程序中使用这个库。
领导人选举
领导者选举是一个分布式系统概念,是高可用软件的核心构建块。它允许多个并发进程相互协调并选举一个“领导者”进程,然后该进程负责执行同步操作,例如写入数据存储。
这在分布式数据库或缓存等系统中非常有用,其中多个进程正在运行以针对硬件或网络故障创建冗余,但无法同时写入存储以确保数据一致性。如果领导者进程在未来某个时刻变得无响应,剩余进程将启动新的领导者选举,最终选择一个新进程作为领导者。
利用这个概念,我们可以创建具有单个领导者和多个备用副本的高可用软件。
在 kubernetes 中,controller-runtime 包使用领导者选举来使控制器具有高可用性。在控制器部署中,仅当进程是领导者并且其他副本处于等待状态时才会发生资源协调。如果leader pod没有响应,剩余的副本将选举一个新的leader来执行后续的协调并恢复正常运行。
kubernetes 租赁
这个库使用 kubernetes lease,或者分布式锁,可以由进程获取。租约是由单一身份在给定期限内持有的原生 kubernetes 资源,并具有续订选项。 这是文档中的示例规范:
apiversion: coordination.k8s.io/v1
kind: lease
metadata:
labels:
apiserver.kubernetes.io/identity: kube-apiserver
kubernetes.io/hostname: master-1
name: apiserver-07a5ea9b9b072c4a5f3d1c3702
namespace: kube-system
spec:
holderidentity: apiserver-07a5ea9b9b072c4a5f3d1c3702_0c8914f7-0f35-440e-8676-7844977d3a05
leasedurationseconds: 3600
renewtime: "2023-07-04t21:58:48.065888z"
k8s 生态系统通过三种方式使用租约:
- 节点心跳:每个节点都有对应的lease资源,并不断更新其renewtime字段。如果 lease 的 renewtime 一段时间没有更新,该 node 将被污染为不可用,并且不会再为其调度 pod。
- leader election:在这种情况下,lease 用于通过让 leader 更新 lease 的holderidentity 来协调多个进程。具有不同身份的备用副本陷入等待租约到期的状态。如果租约确实到期,并且领导者没有续订,则会进行新的选举,其中剩余的副本尝试通过用自己的持有人身份更新其持有者身份来获得租约的所有权。由于 kubernetes api 服务器不允许更新过时的对象,因此只有一个备用节点能够成功更新租约,此时它将作为新的领导者继续执行。
- api 服务器身份:从 v1.26 开始,作为测试功能,每个 kube-apiserver 副本将通过创建专用租约来发布其身份。由于这是一个相对较小的新功能,因此除了运行的 api 服务器数量之外,无法从 lease 对象派生出太多其他内容。但这确实为未来的 k8s 版本中的这些 lease 添加更多元数据留下了空间。
现在让我们通过编写示例程序来探索租赁的第二个用例,以演示如何在领导者选举场景中使用它们。
示例程序
在此代码示例中,我们使用 leaderelection 包来处理领导者选举和租约操作细节。
package main
import (
"context"
"fmt"
"os"
"time"
"k8s.io/client-go/tools/leaderelection"
rl "k8s.io/client-go/tools/leaderelection/resourcelock"
ctrl "sigs.k8s.io/controller-runtime"
)
var (
// lockname and locknamespace need to be shared across all running instances
lockname = "my-lock"
locknamespace = "default"
// identity is unique to the individual process. this will not work for anything,
// outside of a toy example, since processes running in different containers or
// computers can share the same pid.
identity = fmt.sprintf("%d", os.getpid())
)
func main() {
// get the active kubernetes context
cfg, err := ctrl.getconfig()
if err != nil {
panic(err.error())
}
// create a new lock. this will be used to create a lease resource in the cluster.
l, err := rl.newfromkubeconfig(
rl.leasesresourcelock,
locknamespace,
lockname,
rl.resourcelockconfig{
identity: identity,
},
cfg,
time.second*10,
)
if err != nil {
panic(err)
}
// create a new leader election configuration with a 15 second lease duration.
// visit https://pkg.go.dev/k8s.io/client-go/tools/leaderelection#leaderelectionconfig
// for more information on the leaderelectionconfig struct fields
el, err := leaderelection.newleaderelector(leaderelection.leaderelectionconfig{
lock: l,
leaseduration: time.second * 15,
renewdeadline: time.second * 10,
retryperiod: time.second * 2,
name: lockname,
callbacks: leaderelection.leadercallbacks{
onstartedleading: func(ctx context.context) { println("i am the leader!") },
onstoppedleading: func() { println("i am not the leader anymore!") },
onnewleader: func(identity string) { fmt.printf("the leader is %sn", identity) },
},
})
if err != nil {
panic(err)
}
// begin the leader election process. this will block.
el.run(context.background())
}
leaderelection 包的优点在于它提供了一个基于回调的框架来处理领导者选举。这样,您可以以精细的方式对特定的状态变化采取行动,并在选举新领导者时适当地释放资源。通过在单独的 goroutine 中运行这些回调,该包利用 go 强大的并发支持来有效地利用机器资源。
测试一下
为了测试这一点,让我们使用 kind 启动一个测试集群。
$ kind create cluster
将示例代码复制到 main.go 中,创建一个新模块(go mod init leaderelectiontest)并整理它(go mod tidy)以安装其依赖项。运行 go run main.go 后,您应该看到如下输出:
$ go run main.go
i0716 11:43:50.337947 138 leaderelection.go:250] attempting to acquire leader lease default/my-lock...
i0716 11:43:50.351264 138 leaderelection.go:260] successfully acquired lease default/my-lock
the leader is 138
i am the leader!
确切的领导者身份将与示例(138)中的不同,因为这只是撰写本文时在我的计算机上运行的进程的 pid。
这是在测试集群中创建的租约:
$ kubectl describe lease/my-lock
name: my-lock
namespace: default
labels: <none>
annotations: <none>
api version: coordination.k8s.io/v1
kind: lease
metadata:
creation timestamp: 2024-07-16t15:43:50z
resource version: 613
uid: 1d978362-69c5-43e9-af13-7b319dd452a6
spec:
acquire time: 2024-07-16t15:43:50.338049z
holder identity: 138
lease duration seconds: 15
lease transitions: 0
renew time: 2024-07-16t15:45:31.122956z
events: <none></none></none></none>
看到“holder identity”与进程的pid相同,138。
现在,让我们打开另一个终端并在单独的进程中运行相同的 main.go 文件:
$ go run main.go
i0716 11:48:34.489953 604 leaderelection.go:250] attempting to acquire leader lease default/my-lock...
the leader is 138
第二个进程将永远等待,直到第一个进程没有响应。让我们终止第一个进程并等待大约 15 秒。现在,第一个进程不再更新其对租约的声明,因此 .spec.renewtime 字段将不再更新。这最终将导致第二个进程触发新的领导者选举,因为租约的更新时间早于其持续时间。因为这个进程是唯一正在运行的进程,所以它将选举自己作为新的领导者。
the leader is 604
I0716 11:48:51.904732 604 leaderelection.go:260] successfully acquired lease default/my-lock
I am the leader!
如果初始 leader 退出后还有多个进程仍在运行,则第一个获得 lease 的进程将成为新的 leader,其余进程继续处于待命状态。
没有单一领导者的保证
这个包并不是万无一失的,因为它“不能保证只有一个客户端充当领导者(又名击剑)”。例如,如果领导者暂停并让其租约到期,则另一个备用副本将获取租约。然后,一旦原来的领导者恢复执行,它就会认为自己仍然是领导者,并继续与新当选的领导者一起工作。这样,你最终可以有两个领导者同时运行。
要解决此问题,需要在向服务器发出的每个请求中包含引用租约的隔离令牌。隔离令牌实际上是一个整数,每次租约易手时该整数就会增加 1。因此,具有旧防护令牌的客户端的请求将被服务器拒绝。在这种情况下,如果旧领导者从睡眠中醒来,并且新领导者已经增加了防护令牌,则旧领导者的所有请求都将被拒绝,因为它发送的令牌比服务器从服务器看到的令牌更旧(更小)。新领导者。
如果不修改核心 api 服务器来考虑每个 lease 的相应 fencing 令牌,那么在 kubernetes 中实现 fencing 将会很困难。然而,k8s api 服务器本身在一定程度上减轻了拥有多个领导者控制器的风险。由于对过时对象的更新会被拒绝,因此只有拥有最新版本对象的控制器才能修改它。因此,虽然我们可以运行多个控制器领导者,但如果一个控制器错过了另一个领导者所做的更改,资源的状态将永远不会回归到旧版本。相反,协调时间将会增加,因为两位领导者都需要刷新自己的内部资源状态,以确保他们正在执行最新版本。
不过,如果您使用此包使用不同的数据存储来实现领导者选举,这是一个需要注意的重要警告。
结论
领导者选举和分布式锁定是分布式系统的关键构建块。当尝试构建容错和高可用性的应用程序时,拥有此类工具至关重要。 kubernetes 标准库为我们提供了一个经过实战检验的原语包装器,允许应用程序开发人员轻松地将领导者选举构建到他们自己的应用程序中。
虽然这个特定库的使用确实限制了您在 kubernetes 上部署应用程序,但这似乎是最近世界的发展方向。如果事实上这是一个破坏者,您当然可以分叉该库并修改它以适用于任何符合 acid 且高度可用的数据存储。
请继续关注更多 k8s 源码深入研究!