这次的主题不是介绍 k8s,所以只做简单的概括,之前对 k8s 不熟悉的话,可以提问我们现场沟通😂。
docker 等容器化技术解决了应用标准化部署的痛点。接下来的问题就是如何把容器分发到不同的机器上执行,这也被称为编排 orchestrating。
k8s 就是一个编排系统,简而言之主要就是做了这件事:
本次主题我们只讨论最简单的资源:delployment -> replicaset -> pod
k8s 的资源管理逻辑流程可以概括为三大件:
apiserver
: 负责存储资源,并且提供查询、修改资源的接口controller
: 负责操作(修改)本级和下级资源。
比如 deploymentController 就负责管理 deployment 资源 和下一级的 rs 资源。
kubelet
: 安装在 node 节点上,负责部署资源一些理念:
理想中,每一层控制器(controller)只管理本级和子两层资源。
但因为每一个资源都是被上层定义和创建的,所以实际上每一层资源都对下层资源的定义有完全的了解。 所以由上至下的操作是可以跨级的。
A -> B -> C -> D (此时 A 可以预知 D 的资源格式)
但是跨级操作需要非常小心,最好不要跨级,跨级的话只读而不要修改,因为会干扰下层的其他控制器。
UID
: 每一个被创建(提交给 apiserver)的资源都有一个全局唯一的 UUID。metadata
: 每一个资源都有的元数据,包括 label、owner、uid 等label
: 每个资源都要定义的标签selector
: 父资源通过 labelSelector 查询归其管理的子资源。
不允许指定空 selector(全匹配)。OwnerReferences
,
指向其父级资源。列表中第一个有效的指针会被视为生效的父资源。
selector 实际上只是一个 adoption 的机制,
真实起作用的父子级关系是靠 owner 来维持的,
而且 owner 优先级高于 selector。控制器的核心代码可以概括为:
for {
for {
key, empty := queue.Get()
if empty {
break
}
defer queue.Done(key)
syncHandler(key)
}
time.sleep(time.Second)
}
一个 deployment 的定义:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
但其实 k8s 内部还为每一个创建的资源分配了一个 UID(UUID)。每一个资源还有一个 OwnerReferences
属性,保存了父资源的 UID。
所以 k8s 内部的上下级关系其实是通过 owner 指针来维系的。owner 的优先级,要高于 label/selector。
拿 deployment 来举例,每次 syncHandler 处理一个 deployments 时:
如果 owner 不匹配,即使 label 匹配,也不会被视为是父子关系
前文提到了 k8s 的资源有父子关联,那么这个关联有什么用呢?
实际上这个关联就是用来做级联删除(cascading deletion)的。
删除一个资源有三种方式(propagationPolicy/DeletionPropagation
):
Foreground
(default): Children are deleted before the parent (post-order)Background
: Parent is deleted before the children (pre-order)Orphan
: ignore owner references(可以通过 kubelet delete --cascade=???
使用)
k8s 中,资源的 metadata 中有几个对删除比较重要的属性:
ownerRerences
: 指向父资源的 UIDdeletionTimestamp
: 如果不为空,表明该资源正在被删除中finalizers
: 一个字符串数组,列举删除前必须执行的操作blockOwnerDeletion
: 布尔,当前资源是否会阻塞父资源的删除流程无论是什么删除策略,都需要先把所有的 finalizer 逐一执行完,每完成一个,就从 finalizers 中移除一个。在 finalizers 为空后,才能正式的删除资源。
deletionTimestamp
,表明该资源的状态为正在删除中("deletion in progress"
)。metadata.finalizers
为 "foregroundDeletion"
。ownerReference.blockOwnerDeletion=true
的子资源每一个子资源的 owner 列表的元素里,都有一个属性 ownerReference.blockOwnerDeletion
,这是一个 bool
,表明当前资源是否会阻塞父资源的删除流程。删除父资源前,应该把所有标记为阻塞的子资源都删光。
在当前资源被删除以前,该资源都通过 apiserver 持续可见。
触发 FinalizerOrphanDependents
,将所有子资源的 owner 清空,也就是令其成为 orphan。然后再删除当前资源。
立刻删除当前资源,然后在后台任务中删除子资源。
foreground 和 orphan 删除策略是通过 finalizer 实现的
const (
FinalizerOrphanDependents = "orphan"
FinalizerDeleteDependents = "foregroundDeletion"
)
因为这两个策略有一些删除前必须要做的事情。而 background 则就是走标准删除流程:删自己 -> 删依赖。
前文中提到的 controller、GC 等都依赖对资源事件的监听,而且动辄就会有大量的查询甚至遍历。如果每次都直接去请求 apiserver 的话,会造成较大的压力和资源的浪费。
Informer 也经历了两代演进,从最早各管各的 Informer,到后来统一监听,各自 filter 的 sharedIndexer。
所有的 controller 都在一个 controller-manager 进程内,所以完全可以共享同一个 informer, 不同的 controller 注册不同的 filter(kind、labelSelector),来订阅自己需要的消息。
简而言之,现在的 sharedIndexer,就是一个统一的消息订阅器,而且内部还维护了一个资源存储,对外提供可过滤的消息分发和资源查询功能。
各个 controller 的订阅和查询绝大部分都在 sharedIndexer 的内存内完成,提高资源利用率和效率。
一般 controller 的消息来源就通过两种方式:
<namespace>/<name>
AddEventHandler
方法注册 Add/Update/Delete
事件的处理函数。这里有个值得注意的地方是,资源事件的格式是字符串,形如 <namespace>/<name>
,这其中没有包含版本信息。
那么某个版本的 controller 拿到这个信息后,并不知道查询出来的资源是否匹配自己的版本,也许会查出一个很旧版本的资源。
所以 controller 对于资源必须是向后兼容的,新版本的 controller 必须要能够处理旧版资源。 这样的话,只需要保证运行的是最新版的 controller 就行了。
核心代码:https://github.com/kubernetes/kubernetes,所有的 controller 代码都在 pkg/controller/
中。
所有的 clientset、informer 都被抽象出来在 https://github.com/kubernetes/client-go 库中,供各个组件复用。