
Kubernetes | Leases
上一篇博客说到控制平面的部署方案和高可用思路,其中有几样东西没有提到。
K8S 的控制平面
K8S 很多东西是围绕着 API 进行的,比如说 Deployment、ReplicaSet 和 Pod 就是一组 API,分别负责应用的分片数量、版本和容器的定义。etcd 存储这些资源,apiserver 提供 api 服务,kubelet 执行或者销毁 pod 描述的容器。
Scheduler 和 Controller Manager
除此之外我们还会接触到 scheduler 和 controller manager,controller manager 是一组 K8S 控制器,各司其职,其中有几个控制器维护着这三个资源之间的关系。
而 scheduler 确认 pod 可以跑在什么节点上以后更新 pod 中 node 的字段以此确认在哪里运行这个 pod。
高可用
既然涉及到 K8S 主要的功能,我们也会需要这些组件的高可用,我们当然可以让所有的 controller 一起工作,无非就是浪费一些算力和带宽。可是 etcd 为了强一致性,已经牺牲了非常多的性能,所以再这么搞,怕是各种性能撑不住。
那么怎么办呢?我们就需要分布式锁,但是分布式锁怎么才能实现在一个节点 down 以后让其他节点自动接上呢?这里就需要一个概念叫租约 lease。
Lease
分布式锁 distributed lock
锁,一般就是说有多个过程要操作一个对象时会有竞态条件,为了解决竞态条件而产生的一种资源,同一时间同时发生的多个过程只能有一个成功获取到所有权,从而标记对对象进行操作的所有权归属。分布式锁也就是在分布式环境下,让多机上不同的实例可以访问到锁。
但是在分布式环境中情况比较特殊,一方面是持锁的顺序和流程逻辑不同的业务可能有不同的定义,另一方面是网络或者存储在一段时间内总有那么一些情况会有奇奇怪怪的问题,这种时候就可能产生死锁。
解决死锁的一个思路就是给他加上 timeout,让他变成 lease。
租约 lease
给 lock 加上 timeout,这个 lock 我们就可以叫他租约 lease。
顾名思义,这个 lock 在你持有期间就是你的,你可以续租,但是如果你不续了,那么别人就可以获取到这份租约。
Kubernetes 中的 lease
使用 lease 的组件
Kubernetes 中很多组件需要高可用,但是除了 etcd 提供存储(有状态,存储),apiserver 提供服务(无状态),kubelet 和 kube-proxy 负责根据 service 和 pod 配置具体的容器和各种网络资源以外(不提供服务,无状态,不存储),其他的组件都是会产生竞态且不需要提供服务的组件,这些组件就需要用 lease 来保证一主多备。比如说 kube-scheduler、kube-controller-manager、kube-cloud-controller-manager,或者是其他的第三方的 controller。
Kubernetes 中的租约实现
etcd 文档版本
这里其实就是 K8S 是怎么工作的问题。前面提到 K8S 其实是围绕着资源工作的,这些资源由 etcd 存储,由 apiserver 提供服务。资源更新就会需要对特定版本的 etcd 文档打一个补丁,文档就会产生一个带着补丁内容的新版本。
假设我们同时对一个版本同时做两次更新呢?会得到有两个更新的一个新版本吗?不会,其中只会有一个更新成功,其他所有更新都会失败,最终只有一个新版本带着其中一个更新。两次更新的内容一样呢?结果还是一样的,其他的会失败,只有一个会成功。
etcd 通过维护版本,可以保证一次性只有一个对当前版本的操作可以成功。那么我们就可以通过更新一个 etcd 的文档来实现 lease 的功能,也就是当大家都要竞争 leader 时,检查某一个资源的某个字段,是不是足够旧,旧到一定程度了就可以认为这玩意儿是无主之物了,我们就可以通过更新这个字段来尝试持有锁。更新成功了,自己就是锁的持有人,定期更新锁就好了,自己失败了那就继续检查字段是不是够旧。如果有多个实例一起发现锁过期了也没关系,创建或者更新都能保证一次只有一个能成功。
Kubernetes早期实现
kube-scheduler 和 kube-controller-manager 早期是通过更新 endpoint 或者 configmap 来实现 lease 的功能。
比如说这就是 1.16 中 lease 的实现。
当然这种方法也有问题,那就是 etcd 每次有 lease 的变动,不管是 holder 变了还是续租了,整个资源都会同步给 watch 了这个资源的所有 watcher,这会造成大量算力和带宽被占用。所以后来也做出了改进。
Lease API
为了解决用 configmap 和 endpoint 实现 lease 会造成大量带宽浪费的问题,从 2019 年开始,K8S 的整个生态就在通过同时使用多种 lease 实现逐渐过渡到 lease api 上,并且到现在也还在持续中。比如说1.17 中 lease 的实现
为了实现租约,组件需要的权限
上面提到租约的实现是基于某种资源或者多种资源的创建和更新机制,那么组件就需要有合理的机制。
对于老的 configmap 和 endpoint,组件需要有 这两个的 create 、get和 update 的机制。而新的 lease api 则需要对apiVersion: coordination.k8s.io/v1中的Lease 对象有 create、get 和 update 的权限
- verbs:
- create
apiGroups:
- coordination.k8s.io
resources:
- leases
- verbs:
- get
- update
apiGroups:
- coordination.k8s.io
resources:
- leases
resourceNames:
- kube-scheduler
- verbs:
- create
apiGroups:
- ''
resources:
- endpoints
- verbs:
- get
- update
apiGroups:
- ''
resources:
- endpoints
resourceNames:
- kube-scheduler
验证 K8S 组件在使用 lease api 进行选举
以 1.23为例,在选举时会打印几行日志,只要我们检查是不是在日志中打印了这一行日志就行。
如果获取到了 lease,被选举为 leader,那么日志应该有这几行
I0330 01:00:20.261875 1 leaderelection.go:243] attempting to acquire leader lease kube-system/kube-scheduler...
...
I0330 01:00:35.465138 1 leaderelection.go:253] successfully acquired lease kube-system/kube-scheduler
如果没有选举到,最起码也应该打印
I0329 23:00:25.615501 1 leaderelection.go:243] attempting to acquire leader lease kube-system/kube-scheduler...
有这个,说明 leader election 的配置已经生效而且没有问题了。
验证组件是不是 leader
由于选举机制是更新 configmap、endpoint 或者 lease 对象,那么我们也只要检查具体的实现就好了。
对于比较新的集群,比如说我们用的1.20,scheduler 和 controller manager 都已经用上了 lease api,我们只要检查
$ kubectl get leases -A
NAMESPACE NAME HOLDER AGE
kube-node-lease agent1 agent1 4d22h
kube-node-lease agent2 agent2 4d22h
kube-node-lease agent3 agent3 4d22h
kube-node-lease master1 master1 4d22h
kube-node-lease master2 master2 4d22h
kube-node-lease master3 master3 4d22h
kube-system kube-controller-manager master1_76cc8e5d-721c-45ab-9b2e-695a4f583e48 4d22h
kube-system kube-scheduler master3_a93bda5e-0f54-4ce0-bc56-e2c96fc22d21 4d22h
只要能看到 kube-scheduler 和 kube-controller-manager 这两项,而且对应的 holder 和最后一个成功获取到 lease 的客户端能对得上,就说明 leader 是这一个 pod。
其他 lease 常见的场景
各种实现
由于 lease 可以看成是带 timeout 的 lock,或者说他就是。其实这种实现有很多,比如说基于 redis 的 Redlock,还有比如说基于 MySQL 的 transaction 来实现,或者基于各种文档数据库的版本来实现。
我记忆非常深的就是在上海坚果云的时候,老板教的,用 Azure Table Storage 实现的 lease manager。
DHCP 服务用了 Lease
我们在路由器里面,尤其是自己用 Linux 做路由器时搭建的 dnsmasq 里面,可以看到 lease pool 的这么一个概念。每当有一个新的设备加入网络,都会按照某种规则选择 IP 然后 acquire lease,成功了就发给客户端,没成功就再来一次重新选一个 IP。
可以认为我们 DHCP 申请一个 IP 的过程就是申请 IP 并持锁的过程。如果不续租呢?你的 IP 就会分配给别人。
有一些只允许一个服务活着的供应商
我们之前接入电子发票的时候,他们提供了一个接口,结果我们发现请求经常失败,后来就发现是这个接口频率不能太高,同时访问的只能有一个。但是业务说崩就崩,我也不能只开一个是不是,最后就用 Azure Table Storage 实现了一个带 Lease 的 worker,同时跑俩实例,谁持锁谁干活儿。