K8S 中 scheduler 组件的选主逻辑
概述在 k8s 中,kube-scheduler 和 kube-controller-manager 两个组件是有 leader 选举的,这个选举机制是 k8s 对于这两个组件的高可用保障,虽然 k8s 的存储使用了 etcd,但并没有使用 etcd 来实现选主,而是对 endpoint 这个资源做抢占,谁想抢到并将自己的信息写入 endpoint的 annotation 中,谁就获得了主。因为项
概述
在 k8s 中,kube-scheduler 和 kube-controller-manager 两个组件是有 leader 选举的,这个选举机制是 k8s 对于这两个组件的高可用保障,虽然 k8s 的存储使用了 etcd,但并没有使用 etcd 来实现选主,而是对 endpoint 这个资源做抢占,谁想抢到并将自己的信息写入 endpoint的 annotation 中,谁就获得了主。因为项目中需要写一个 k8s 的插件,同样需要选主逻辑,因此复用了 k8s 中的这个方法,即kubernetes 的 tools/leaderelection包,顺便理解一下这个原理,本文主要介绍下选主逻辑,基于 etcd client 实现选主可以看另一篇文章
本文以kube-scheduler为例,kube-controller-manager同理,k8s 版本为:1.18
表现
在多副本的kube-scheduler 的日志,可以看到这样一段
Apr 25 16:04:41 instance-o24xykos-1 kube-scheduler[31111]: I0425 16:04:41.743878 31111 round_trippers.go:443] GET https://100.64.230.53:6443/api/v1/namespaces/kube-system/endpoints/kube-scheduler?timeout=10s 200 OK in 4 milliseconds
Apr 25 16:04:41 instance-o24xykos-1 kube-scheduler[31111]: I0425 16:04:41.744158 31111 leaderelection.go:350] lock is held by instance-o24xykos-3_1ad55d32-2abe-49f7-9d68-33ec5eadb906 and has not yet expired
Apr 25 16:04:41 instance-o24xykos-1 kube-scheduler[31111]: I0425 16:04:41.744176 31111 leaderelection.go:246] failed to acquire lease kube-system/kube-scheduler
Bash
Copy
这是未选到主的日志,每2秒一次(配置–leader-elect-retry-period),获取 kube-system 下的kube-scheduler的 endpoint,发现锁仍然被instance-o24xykos-3持有,抢不到锁,继续重试。
这时的 endpoint 的值为
kubectl get endpoints kube-scheduler -n kube-system -o yaml
apiVersion: v1
kind: Endpoints
metadata:
annotations:
control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":"instance-o24xykos-3_1ad55d32-2abe-49f7-9d68-33ec5eadb906","leaseDurationSeconds":15,"acquireTime":"2020-04-23T06:45:07Z","renewTime":"2020-04-25T07:55:58Z","leaderTransitions":1}'
creationTimestamp: "2020-04-22T12:05:29Z"
name: kube-scheduler
namespace: kube-system
resourceVersion: "467853"
selfLink: /api/v1/namespaces/kube-system/endpoints/kube-scheduler
uid: f3535807-0575-483f-8471-f8d4fd9eeac6
YAML
Copy
annotation 中的 key 为:control-plane.alpha.kubernetes.io/leader,value 中holderIdentity:instance-o24xykos-3即为当前锁的获得者。
在主instance-o24xykos-3上,可以看到如下日志,每隔 2s renew 一次锁,保证主的地位。
Apr 24 03:26:33 instance-o24xykos-3 kube-scheduler[2497]: I0424 03:26:33.281923 2497 leaderelection.go:282] successfully renewed lease kube-system/kube-scheduler
配置
kube-scheduler 在启动时,与 leader election 相关的启动参数有以下几个:
-
leader-elect: 是否开启选举功能,默认开启
-
leader-elect-lease-duration: 锁的失效时间,类似于 session-timeout
-
leader-elect-renew-deadline: leader 的心跳间隔,必须小于等于 lease-duration
-
leader-elect-retry-period: non-leader 每隔 retry-period 尝试获取锁
-
–leader-elect-resource-lock:用什么对象来存放选主信息,默认为 endpoint,也可以用 configmap
-
–leader-elect-resource-name:endpoint 的名称,kube-scheduler
-
–leader-elect-resource-namespace: endpoint 的命名空间,kube-system
对于 k8s 的选主来说,锁节点指的是 kube-system 命名空间下的同名 endpoint。任一 goroutine 如果能成功在该 ep 的 annotation 中留下自身记号即成为 leader。成为 leader 后会定时续约,leader 可以通过更新 RenewTime 来确保持续保有该锁。non-leader 们会定时去获取该 ep 的 annotation,若发现过期等情况则进行抢占。
目前主节点的标识用的是hostname
原理
k8s 中选主逻辑位于:staging/src/k8s.io/client-go/tools/leaderelection
锁结构
type LeaderElectionRecord struct {
// leader 标识,通常为 hostname
HolderIdentity string `json:"holderIdentity"`
// 同启动参数 --leader-elect-lease-duration
LeaseDurationSeconds int `json:"leaseDurationSeconds"`
// Leader 第一次成功获得租约时的时间戳
AcquireTime metav1.Time `json:"acquireTime"`
// leader 定时 renew 的时间戳
RenewTime metav1.Time `json:"renewTime"`
// leader 更换
LeaderTransitions int `json:"leaderTransitions"`
}
Golang
Copy
Interface 接口
k8s 中的锁需实现 如下Interface 接口, 当前实现有三种,EndpointsResourceLock、ConfigMapsResourceLock、LeaseLock。LeaseLock是一种新的方式,会创建一种专门的含租期字段的Lease对象。
type Interface interface {
// Get returns the LeaderElectionRecord
Get() (*LeaderElectionRecord, error)
// Create attempts to create a LeaderElectionRecord
Create(ler LeaderElectionRecord) error
// Update will update and existing LeaderElectionRecord
Update(ler LeaderElectionRecord) error
// RecordEvent is used to record events
RecordEvent(string)
// Identity will return the locks Identity
Identity() string
// Describe is used to convert details on current resource lock
// into a string
Describe() string
}
Golang
Copy
以EndpointsResourceLock的选举过程为例:
// 创建
func (el *EndpointsLock) Create(ler LeaderElectionRecord) error {
recordBytes, err := json.Marshal(ler)
if err != nil {
return err
}
el.e, err = el.Client.Endpoints(el.EndpointsMeta.Namespace).Create(&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: el.EndpointsMeta.Name,
Namespace: el.EndpointsMeta.Namespace,
Annotations: map[string]string{
LeaderElectionRecordAnnotationKey: string(recordBytes),
},
},
})
return err
}
// 更新
func (el *EndpointsLock) Update(ler LeaderElectionRecord) error {
if el.e == nil {
return errors.New("endpoint not initialized, call get or create first")
}
recordBytes, err := json.Marshal(ler)
if err != nil {
return err
}
el.e.Annotations[LeaderElectionRecordAnnotationKey] = string(recordBytes)
el.e, err = el.Client.Endpoints(el.EndpointsMeta.Namespace).Update(el.e)
return err
}
Golang
Copy
选主核心逻辑:tryAcquireOrRenew
tryAcquireOrRenew 函数尝试获取租约,如果获取不到或者得到的租约已过期则尝试抢占,否则 leader 不变。函数返回 True 说明本 goroutine 已成功抢占到锁,获得租约合同,成为 leader。
func (le *LeaderElector) tryAcquireOrRenew() bool {
// 创建 leader election 租约
now := metav1.Now()
leaderElectionRecord := rl.LeaderElectionRecord{
HolderIdentity: le.config.Lock.Identity(),
LeaseDurationSeconds: int(le.config.LeaseDuration / time.Second),
RenewTime: now,
AcquireTime: now,
}
// 1\. 从 endpointslock 上获取 leader election 租约,也就是上边 endpoint 的 get 方法的实现
oldLeaderElectionRecord, err := le.config.Lock.Get()
if err != nil {
if !errors.IsNotFound(err) {
klog.Errorf("error retrieving resource lock %v: %v", le.config.Lock.Describe(), err)
return false
}
// 租约存在:于是将函数一开始创建的 leader election 租约放入同名 endpoint 的 annotation 中
if err = le.config.Lock.Create(leaderElectionRecord); err != nil {
klog.Errorf("error initially creating leader election record: %v", err)
return false
}
// 创建成功,成为 leader,函数返回 true
le.observedRecord = leaderElectionRecord
le.observedTime = le.clock.Now()
return true
}
// 2\. 更新本地缓存的租约,并更新观察时间戳,用来判断租约是否到期
if !reflect.DeepEqual(le.observedRecord, *oldLeaderElectionRecord) {
le.observedRecord = *oldLeaderElectionRecord
le.observedTime = le.clock.Now()
}
// leader 的租约尚未到期,自己暂时不能抢占它,函数返回 false
if len(oldLeaderElectionRecord.HolderIdentity) > 0 &&
le.observedTime.Add(le.config.LeaseDuration).After(now.Time) &&
!le.IsLeader() {
klog.V(4).Infof("lock is held by %v and has not yet expired", oldLeaderElectionRecord.HolderIdentity)
return false
}
// 3\. 租约到期,而 leader 身份不变,因此获得租约的时间戳 AcquireTime 保持不变
if le.IsLeader() {
leaderElectionRecord.AcquireTime = oldLeaderElectionRecord.AcquireTime
leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions
} else {
// 租约到期,leader 易主,transtions+1 说明 leader 更替了
leaderElectionRecord.LeaderTransitions = oldLeaderElectionRecord.LeaderTransitions + 1
}
// 尝试去更新租约记录
if err = le.config.Lock.Update(leaderElectionRecord); err != nil {
// 更新失败,函数返回 false
klog.Errorf("Failed to update lock: %v", err)
return false
}
// 更新成功,函数返回 true
le.observedRecord = leaderElectionRecord
le.observedTime = le.clock.Now()
return true
}
Golang
Copy
发起选主
scheduler 在启动时就会发起选主,代码位于:cmd/kube-scheduler/app/server.go
// If leader election is enabled, runCommand via LeaderElector until done and exit.
if cc.LeaderElection != nil {
cc.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{
OnStartedLeading: run,
OnStoppedLeading: func() {
klog.Fatalf("leaderelection lost")
},
}
leaderElector, err := leaderelection.NewLeaderElector(*cc.LeaderElection)
if err != nil {
return fmt.Errorf("couldn't create leader elector: %v", err)
}
leaderElector.Run(ctx)
return fmt.Errorf("lost lease")
}
// Leader election is disabled, so runCommand inline until done.
run(ctx)
return fmt.Errorf("finished without leader elect")
Golang
Copy
update的原子性
在抢锁的过程中,势必会存在同时 update endpoint 的操作,而解决这种竞争,Kubernetes 是通过版本号的乐观锁来实现的。它对比了 resourceVersion,而resourceVersion的取值最终又来源于etcd的modifiedindex,当key对应的val改变时,modifiedindex的值发生改变。
kubernetes 的 update 是原子的、安全的,通过resourceVersion字段判断对象是否已经被修改。当包含 ResourceVersion 的更新请求到达 Apiserver,服务器端将对比请求数据与服务器中数据的资源版本号,如果不一致,则表明在本次更新提交时,服务端对象已被修改,此时 Apiserver 将返回冲突错误(409),客户端需重新获取服务端数据,重新修改后再次提交到服务器端。
ResourceVersion 字段在 Kubernetes 中除了用在update的并发控制机制外,还用在 Kubernetes 的 list-watch 机制中。Client 端的 list-watch 分为两个步骤,先 list 取回所有对象,再以增量的方式 watch 后续对象。Client 端在list取回所有对象后,将会把最新对象的 ResourceVersion 作为下一步 watch 操作的起点参数,也即 Kube-Apiserver 以收到的 ResourceVersion 为起始点返回后续数据,保证了 list-watch 中数据的连续性与完整性。
服务推荐
更多推荐
所有评论(0)