删掉的配额几秒后又回来了
目标
在真实的 k3s 上,为名为 TeamSpace 的平台 API 亲手接入一个小型控制器。你要逐步用具体的值确认:用户创建一个 TeamSpace 后,命名空间和配额会随之生成;即使有人手动修改,它们也会被改回来;删除时要先完成清理工作,对象才会消失。
为什么重要
CRD 只是向 API 服务器注册一个新的名词,赋予这个名词含义的是控制器。Kubernetes 的控制器是一个调谐循环:它不断比较期望状态(spec)与实际状态,并缩小两者的差距;Operator 模式则是把特定领域的运维知识放进这个循环。平台团队构建自助服务 API 时,如果沿用这种结构,存储、授权、审计和 watch 都由 API 服务器提供,团队只需编写调谐逻辑。 由于调谐以当前状态而不是事件为依据,即使漏掉了通知或控制器重启,最终也会收敛;用户则通过 status 中的 observedGeneration 读取“我的变更是否已生效”。本实验会逐一破坏这些性质,来确认它们成立。
步骤
- 编写并应用 CRD
teamspaces.platform.labhub.io,保存到/root/cnpa-op/crd.yaml。group 为platform.labhub.io,scope: Cluster,kind 为TeamSpace(plural 为teamspaces),版本为v1alpha1并启用subresources.status;schema 为spec.pods(integer,必填,1–50)、spec.cpu(string,默认值"1")、status.observedGeneration(integer)和status.namespace(string)。然后通过/root/cnpa-op/alpha.yaml创建 TeamSpacealpha(spec.pods: 4),并在/root/cnpa-op/before.json中写入uid(alpha 的 uid)和namespace_exists(命名空间team-alpha是否存在,布尔值)。 - 对 alpha 依次做三件事,并在每一步之后读取
metadata.generation:① 添加标签owner=platform;② 通过主资源地址发送{"status":{"observedGeneration":7}}的 merge 补丁;③ 把spec.pods改为6。在/root/cnpa-op/generation.json中写入uid、gen_initial(初始值)、gen_after_label、gen_after_spec(③ 之后)和status_via_main(② 之后 alpha 的.status值,没有则为 null)。 - 编写
/root/cnpa-op/reconcile.sh(需要可执行权限)。运行一次时,对每个 TeamSpace<이름>(占位符为 TeamSpace 名称),使命名空间team-<이름>(标签为platform.labhub.io/teamspace: <이름>,带有指向该 TeamSpace 的 ownerReference)以及其中的 ResourceQuotateam-quota(pods取 spec.pods,requests.cpu取 spec.cpu)与期望一致,并在 status 子资源中记录observedGeneration(当前 generation)和namespace。运行一次之后,在/root/cnpa-op/reconcile.json中写入quota_uid(team-alpha 中 team-quota 的 uid)、namespace_owner_uid(team-alpha 的 ownerReference uid)和second_pass_rv_changed(再运行一次时配额的 resourceVersion 是否发生变化,布尔值)。 - 创建
/etc/systemd/system/cnpa-op.service,让它每隔 3 秒无限循环执行reconcile.sh(Restart=always),然后执行 enable 并启动。创建新的 TeamSpace 后,即使没有人做任何操作,也应在几秒内生成命名空间和配额。 - 在控制器运行的状态下,手动删除
team-alpha中的team-quota,并测量它以新 uid 重新出现花了多少秒。接着把恢复出来的配额的pods用补丁改成99,观察它是否又变回6。在/root/cnpa-op/drift.json中写入deleted_uid、restored_uid、restore_seconds(整数)、edited_to(99)和reverted_to(变回的值,整数)。 - 修改
reconcile.sh,让它处理 finalizerplatform.labhub.io/cleanup。对没有删除请求的 TeamSpace,添加这个 finalizer(保留其他 finalizer);对已经打上deletionTimestamp的 TeamSpace,先把name、uid、pods写入/root/cnpa-op/archive/<이름>.json(占位符为 TeamSpace 名称),再摘掉 finalizer。然后创建 TeamSpacebeta(pods: 2,cpu: "500m"),确认命名空间已生成后将其删除。在/root/cnpa-op/cleanup.json中写入uid(beta 的 uid)、blocked_seen(删除请求刚发出时,是否看到 beta 带着 deletionTimestamp 仍然存在,布尔值)和namespace_gone(结束后 team-beta 是否已消失,布尔值)。 - 停止
cnpa-op.service,并把 alpha 的spec.pods改为8。等待 8 秒后,读取 alpha 的metadata.generation、status.observedGeneration以及配额的pods,然后重新启动服务,等待两者重新一致。在/root/cnpa-op/catchup.json中写入generation、observed_while_stopped、quota_pods_while_stopped、observed_after_start和quota_pods_after_start(配额值保持字符串原样)。 - 在
/root/cnpa-op/report.json中写入crd_alone_created_namespace(第 1 步)、status_bumps_generation(写入 status 是否让 generation 增加——在第 2 步或现在的 alpha 上确认)、restore_seconds(第 5 步)、finalizer(名称)、archived(archive 目录中 beta.json 里记录的 uid)、generations_behind_while_stopped(第 7 步的 generation 与 observed_while_stopped 之差)和trigger(level或edge中,这个控制器遵循的方式)。
参考
- VM 内已有 k3s,可以使用
kubectl、jq、python3、systemctl。不使用容器镜像。 - 写入 status:
kubectl patch teamspace <이름> --subresource=status --type merge -p '{"status":{...}}'(占位符为资源名称)。 - 常见错误:通过主资源地址给 status 打补丁。只要启用了 status 子资源,服务器就会悄悄丢弃这些内容,只输出
patched (no change)。 - 常见错误:添加 finalizer 的补丁把原有列表整个覆盖。其他控制器的 finalizer 会因此消失。
- 常见错误:让控制器处于停止状态就结束实验。第 4、5、6、7 步的评分器要求控制器正在运行才能通过。
- Controllers · Operator pattern · CRD status subresource · Finalizers · Owners and Dependents · CNCF Platforms White Paper
只注册类型,什么都不会发生
编写并应用 CRD teamspaces.platform.labhub.io,保存到 /root/cnpa-op/crd.yaml。group 为 platform.labhub.io,scope: Cluster,kind 为 TeamSpace(plural 为 teamspaces),版本为 v1alpha1 并启用 subresources.status;schema 为 spec.pods(integer,必填,1–50)、spec.cpu(string,默认值 "1")、status.observedGeneration(integer)和 status.namespace(string)。然后通过 /root/cnpa-op/alpha.yaml 创建 TeamSpace alpha(spec.pods: 4),并在 /root/cnpa-op/before.json 中写入 uid(alpha 的 uid)和 namespace_exists(命名空间 team-alpha 是否存在,布尔值)。
CRD 只是让 API 服务器认识一个新名词,目前还没有任何组件会根据这个名词去创建东西。命名空间是否存在,可以通过 kubectl get ns 的退出码判断。
什么会让 generation 增加
对 alpha 依次做三件事,并在每一步之后读取 metadata.generation:① 添加标签 owner=platform;② 通过主资源地址发送 {"status":{"observedGeneration":7}} 的 merge 补丁;③ 把 spec.pods 改为 6。在 /root/cnpa-op/generation.json 中写入 uid、gen_initial(初始值)、gen_after_label、gen_after_spec(③ 之后)和 status_via_main(② 之后 alpha 的 .status 值,没有则为 null)。
generation 是 API 服务器只在 spec 变化时才会递增的数字。只修改元数据时,resourceVersion 会增加,但 generation 保持不变。在启用了 status 子资源的 CRD 上,服务器会丢弃通过主资源发送的 status 变更。请观察 kubectl 此时输出了什么。
运行多少次,结果都一样
编写 /root/cnpa-op/reconcile.sh(需要可执行权限)。运行一次时,对每个 TeamSpace <이름>(占位符为 TeamSpace 名称),使命名空间 team-<이름>(标签为 platform.labhub.io/teamspace: <이름>,带有指向该 TeamSpace 的 ownerReference)以及其中的 ResourceQuota team-quota(pods 取 spec.pods,requests.cpu 取 spec.cpu)与期望一致,并在 status 子资源中记录 observedGeneration(当前 generation)和 namespace。运行一次之后,在 /root/cnpa-op/reconcile.json 中写入 quota_uid(team-alpha 中 team-quota 的 uid)、namespace_owner_uid(team-alpha 的 ownerReference uid)和 second_pass_rv_changed(再运行一次时配额的 resourceVersion 是否发生变化,布尔值)。
调谐关注的不是“什么变了”,而是“现在的期望状态与实际状态是否一致”,所以无论运行多少次,结果都应相同。内容相同时,kubectl apply 不会写入。status 要用 kubectl patch --subresource=status 来写。评分器会再把这个脚本运行两次,检查是否没有写入任何内容。
不是一次,而是持续运行
创建 /etc/systemd/system/cnpa-op.service,让它每隔 3 秒无限循环执行 reconcile.sh(Restart=always),然后执行 enable 并启动。创建新的 TeamSpace 后,即使没有人做任何操作,也应在几秒内生成命名空间和配额。
在 ExecStart 中用 bash -c 写入 while true; do ...; sleep 3; done 循环即可。评分器会创建一个临时 TeamSpace,检查命名空间是否会自动生成,然后把它删除。
删掉的配额几秒后又回来了
在控制器运行的状态下,手动删除 team-alpha 中的 team-quota,并测量它以新 uid 重新出现花了多少秒。接着把恢复出来的配额的 pods 用补丁改成 99,观察它是否又变回 6。在 /root/cnpa-op/drift.json 中写入 deleted_uid、restored_uid、restore_seconds(整数)、edited_to(99)和 reverted_to(变回的值,整数)。
控制器并不知道是谁删除的,它只是在下一轮把期望状态与实际状态做比较。因此恢复出来的配额虽然名称相同,却是一个新对象。请回想第 2 步中把 spec.pods 改成了多少。
删除之前有事情要先做
修改 reconcile.sh,让它处理 finalizer platform.labhub.io/cleanup。对没有删除请求的 TeamSpace,添加这个 finalizer(保留其他 finalizer);对已经打上 deletionTimestamp 的 TeamSpace,先把 name、uid、pods 写入 /root/cnpa-op/archive/<이름>.json(占位符为 TeamSpace 名称),再摘掉 finalizer。然后创建 TeamSpace beta(pods: 2,cpu: "500m"),确认命名空间已生成后将其删除。在 /root/cnpa-op/cleanup.json 中写入 uid(beta 的 uid)、blocked_seen(删除请求刚发出时,是否看到 beta 带着 deletionTimestamp 仍然存在,布尔值)和 namespace_gone(结束后 team-beta 是否已消失,布尔值)。
对带有 finalizer 的对象发送 delete 时,API 服务器不会真正删除,只会打上 deletionTimestamp。等列表清空后,对象才会真正消失。命名空间会由垃圾回收器沿着 ownerReference 删除,所以控制器不必亲自删除。只有集群外的记录这类无法用所有权关系表达的内容,才需要用 finalizer 来清理。想看到刚删除时的状态,可以用 kubectl delete --wait=false 只发送请求,然后立即读取。评分器还会再创建并删除一个临时 TeamSpace 来检查。
不会漏掉停止期间发生的变更
停止 cnpa-op.service,并把 alpha 的 spec.pods 改为 8。等待 8 秒后,读取 alpha 的 metadata.generation、status.observedGeneration 以及配额的 pods,然后重新启动服务,等待两者重新一致。在 /root/cnpa-op/catchup.json 中写入 generation、observed_while_stopped、quota_pods_while_stopped、observed_after_start 和 quota_pods_after_start(配额值保持字符串原样)。
这个控制器不接收变更事件来处理,而是每一轮读取当前状态,所以不需要知道停止期间发生了几次、什么样的变更。observedGeneration 小于 generation,表示“尚未反映这份 spec”,这是用户读取控制器进度的标准方法。
把控制器做过的事用值记录下来
在 /root/cnpa-op/report.json 中写入 crd_alone_created_namespace(第 1 步)、status_bumps_generation(写入 status 是否让 generation 增加——在第 2 步或现在的 alpha 上确认)、restore_seconds(第 5 步)、finalizer(名称)、archived(archive 目录中 beta.json 里记录的 uid)、generations_behind_while_stopped(第 7 步的 generation 与 observed_while_stopped 之差)和 trigger(level 或 edge 中,这个控制器遵循的方式)。
读取前面步骤留下的 JSON 来计算。这个控制器不会对每一个变更事件做出反应,而是每次读取整个当前状态再使其一致。评分器会把同一批文件与集群再次对照。