KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
01 · 控制器 — keel 龙骨
类型:主题概念 · 应用课对应:01 章 · 声明式与调谐
类型:主题概念 · 应用课对应:01 章 · 声明式与调谐
这一页只回答四件事:它为什么会出现、它是什么、它长什么样、它怎么工作。
一、它为什么会出现
两个现象,都指向同一件事:
① apply 返回了 created,Pod 却还在 ContainerCreating
② 删掉一个 Pod,没人敲任何命令,过一会儿它自己回来了
这两个现象里都有一个"没人敲命令"的间隙。第一个间隙是时间,第二个间隙是动作——
而且那个动作还带方向:它把"少了 1 个"补成了"正好 2 个"。
问题在于:如果"达成"这件事必须由命令来推动,那它就退回到了命令式的老路——
你交完文件还得盯着它,或者写一堆"检查一下、没有就创建"的判断。那不叫省事。
它面对的需求是:让"达成期望状态"这件事自己发生,且不需要任何人(或任何脚本)去触发它。
二、它是什么
一句话:控制器就是一段一直在跑的循环——读一遍你声明的期望状态,再读一遍现在的实际状态,
两个不一样就动手把差值缩小,然后回到第一步。
它不是这三样东西——混淆会带来误判:
- 不是一次性的脚本。 它没有"执行完了"这个状态。跑完一轮就回到第一步,
永远没有出口。所以你不给它"开始"这个动作,也没有"重新运行它"这回事。 - 不是你提交时被触发的。 你交文件只是写入了期望状态;循环本来就在跑,
它下一轮读到了新值而已。这就是为什么apply的返回和"达成"之间没有因果关系。 - 不是替你判断对错的东西。 它只做一件事:让实际等于你写的那个。你写错了,
它会一丝不苟地把错的那个维持住。
三、它长什么样
它自己没有输出、没有界面。想看见它,只能从它做过的事回头看。
好在集群把"谁做的"记在了一个地方——事件里的 From 那一列。
一个 Deployment 刚把它要的副本数拉满时(kubectl describe deploy web):
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 11s deployment-controller Scaled up replica set web-565bc6668 from 0 to 2
From 那一列写着 deployment-controller,Message 里写着"从 0 拉到 2"——
这就是那次差值被算出来、并被执行的痕迹。 没有人下过这条命令。
同一份清单里的 Pod 拿到的事件,会让流水线上另外两个角色露出来:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 10s default-scheduler Successfully assigned default/web-xxxx to keel-lab-control-plane
Normal Pulled 10s kubelet spec.containers{web}: Container image "..." already present on machine ...
Normal Created 10s kubelet spec.containers{web}: Container created
Normal Started 9s kubelet spec.containers{web}: Container started
三条 From 是三个不同的角色:default-scheduler 决定放在哪台机器,kubelet 负责在那台机器上把它造出来。 控制器不干这两件事——它只管"该有几个"。
它认到的结果写在对象的 status 里,你用列表就能读到:
NAME READY UP-TO-DATE AVAILABLE AGE
web 2/2 2 2 55s
READY 2/2 不是哪个容器报上来的,是控制器数完写下的结论。
四、它怎么工作
一轮循环,六步:
① 读期望 读你写下的那份(要 2 个、用哪个镜像)
② 读实际 从集群里查现在到底是什么样(只有 1 个在跑)
③ 算差值 期望减实际(-1)
④ 动手 采取行动缩小差值(再造一个)
⑤ 写结论 把"我认到多少"写回对象的状态(READY 2/2)
⑥ 回到① 不等谁叫它,直接开始下一轮
第 ⑥ 步是全部要点:它没有"没事做"这个状态。 差值算出来是 0 的时候,
它什么都不做——但那一轮已经跑过了,下一轮还是会来。
第 ④ 步还有一个容易被忽略的性质:它不会去修原来那个东西。
少一个就造一个新的,容器坏了就把整个重造。对它来说"修"和"再造"没有区别。
五、它会带出哪些概念
- Pod:第 ④ 步"动手"造出来的东西是 Pod,所以"要补就再造一个"这句话的宾语是它。→ 00 Pod
- 节点:第 ④ 步只说"造一个","放在哪台机器上"不归它管——那是
default-scheduler的事。→ 03 节点 - 期望状态写在哪:
spec是你写的、status是它写的,两者分开存放,所以两件事永远分开读。→ 应用课 01 章 - 一层套一层:Deployment 的控制器管的不直接是 Pod,而是中间那层 ReplicaSet,所以同一件事会经过两次循环。→ 应用课 05 章
六、回到应用课
| 应用课里的那句话 | 现在你知道它为什么成立 |
|---|---|
01 章:"kubectl apply 成功后,你只完成了『把期望状态说清楚』这一步" |
因为"达成"由那个一直在跑的循环负责,它不在 apply 的调用栈里——返回成功时它可能还没跑到下一轮 |
| 01 章:"调谐(reconciliation)就是控制器不停做的那件事" | 它就是第 ④ 步到第 ⑥ 步那个闭环;"不停"指的是第 ⑥ 步没有出口,不是指它很忙 |
| 01 章:"手工删掉一个由 Deployment 管理的 Pod……会恢复" | 不是"删除"这个动作被监听了;是下一轮它读到的实际值比期望值少了 1,于是差值出现了 |
01 章:"Pending 的 Pod 等一会儿就好了" |
这一轮循环跑得没错,是第 ② 步读到的现实(没有节点装得下)一直没变——差值不为零,但它没有手段缩小它 |
这一页改变了你的哪个判断:以后看到一个"卡住"的对象,先问它卡在第几步——
不要去问"命令有没有执行成功"——是期望没被写进去、差值没被算出来,
还是它算出了差值却没有手段消除它。这三件事的排查方向完全不同。
自测题
- 为什么"重新运行一次控制器"这个说法是不成立的?
kubectl describe deploy的事件里,From那一列写着deployment-controller。为什么这一列能证明它一直在跑?