TT Lab
开始
学习 学习路径 课程

Kubernetes 运维实务

请只把昨天删掉的命名空间恢复回来

在 TT Lab 中继续学习

目标

以对象为单位导出一个混合了多种类型的命名空间内容,用脚本剔除仅在该集群中才有意义的字段,再恢复到另一个命名空间。亲眼看到过期的所有者引用让恢复出来的对象悄悄消失,确认恢复顺序出错时的错误,并让恢复验证由机器来完成。

为什么重要

etcd 快照是把整个集群回退到某一时间点的工具。所以对于“只恢复昨天被删除的一个命名空间”这样的请求无法使用,也无法迁移到另一个集群。这时需要的是对象级备份,但它有快照所没有的难点。导出的对象不能原样放回去。uid 和 resourceVersion 只在该集群中有意义,status 是控制器会重新写入的值,ownerReferences 的 uid 在恢复的集群中并不存在——最后这一条尤其麻烦。垃圾回收器会把它当作没有所有者的对象,没有错误也没有事件地删掉。

步骤

  1. 在 /root/ops-backup/crd.yaml 中写入 CustomResourceDefinition widgets.ops.example.com——组 ops.example.com,种类 Widget(复数形式 widgets),命名空间范围,版本 v1(served 和 storage 都为 true),并且 schema 中有整数 spec.size。然后在 /root/ops-backup/scene.yaml 中把命名空间 ops-shop 的四个对象写入同一个文件——Deployment catalog(副本数 2,标签 app: catalog,镜像 nginx:1.27.3),同名的 ClusterIP Service catalog(选择器 app: catalog,端口 80),ConfigMap catalog-config(currency: KRW,page-size: "20"),Widget w1(spec.size 为 3)。创建命名空间并全部应用。
  2. 把四个对象分别不加工、原样导出到 /root/ops-backup/raw/ 之下——deploy.yaml(Deployment catalog)、svc.yaml(Service catalog)、cm.yaml(ConfigMap catalog-config)、widget.yaml(Widget w1)。kubectl get <종류> <이름> -o yaml(占位符依次为种类与名称)的输出不要动它,原样保存。
  3. 创建 /root/ops-backup/clean.sh——读取作为参数传入的一个 YAML 文件,把清理后的结果仅输出到标准输出。要清除的有 metadata 中的 uid、resourceVersion、creationTimestamp、generation、managedFields、ownerReferences、namespace,顶层的 status,以及 Service 的 spec.clusterIP、spec.clusterIPs、spec.ports[].nodePort。用这个脚本清理 raw/ 中的四个文件,并以同样的文件名保存到 /root/ops-backup/clean/。
  4. 把命名空间 ops-shop 整个删除,确认已被删除后,把它写为一行记录到 /root/ops-backup/gone.txt——ops-shop 制表符 NotFound。删除之前,请先确认 /root/ops-backup/raw/ 中的四个文件都在。
  5. 创建命名空间 ops-shop-restored,并把 /root/ops-backup/clean/ 中的四个文件恢复到该命名空间(清理后的清单中没有命名空间,所以在命令中指定)。等到 Deployment 的 2 个 Pod 变为 Running 之后,把它保存为四行到 /root/ops-backup/restored.tsv——Deployment 制表符 catalog,Service 制表符 catalog,ConfigMap 制表符 catalog-config,Widget 制表符 w1,按种类名称升序。
  6. 在 /root/ops-backup/victim.yaml 中写入 ConfigMap orphan-victim——命名空间 ops-shop-restored,数据 note: restored-with-stale-owner,并在 metadata.ownerReferences 中写入这个集群中不存在的所有者(apiVersion: v1,kind: ConfigMap,name: catalog-config-old,uid 用任意 UUID)。应用之后,用条件循环等待该对象消失。然后在 /root/ops-backup/fixed.yaml 中不带 ownerReferences 写入数据相同的 ConfigMap orphan-fixed 并应用,再把结果按两行留在 /root/ops-backup/owner.tsv 中——orphan-victim 制表符 gone,orphan-fixed 制表符 alive。
  7. 在 /root/ops-backup/gadget.yaml 中写入自定义资源 Gadget g1——apiVersion: ops.example.com/v1,命名空间 ops-shop-restored,spec.color 为 blue。在还没有该种类的 CRD 的状态下先应用,把错误保存到 /root/ops-backup/order-error.txt(包含标准错误)。然后在 /root/ops-backup/gadget-crd.yaml 中写入 CRD gadgets.ops.example.com(组 ops.example.com,种类 Gadget,复数形式 gadgets,命名空间范围,版本 v1,字符串 spec.color)并应用,再重新应用自定义资源。最后在 /root/ops-backup/restore-order.txt 中用四行写下恢复顺序——namespace、crd、custom-resource、workload,按正确的顺序每行一个。
  8. 在 /root/ops-backup/inventory.tsv 中用四行写下恢复清单——格式为 <종류> 制表符 <이름> 制表符 <기대값>(占位符依次为种类、名称与期望值),期望值是:Deployment 为 spec.replicas,Service 为 spec.ports[0].port,ConfigMap 为 data 的键个数,Widget 为 spec.size。然后创建 /root/ops-backup/verify-restore.sh——读取这张表,与 ops-shop-restored 中的实际对象对照,相符则输出 OK <종류>/<이름> <값>(占位符依次为种类、名称与值),不符则输出 MISMATCH <종류>/<이름> 기대=<값> 실제=<값>(韩文,意为“期望”与“实际”,占位符依次为种类、名称、期望值与实际值),仅输出到标准输出,只要有一行不符,就必须以非 0 的退出码结束。把该输出保存到 /root/ops-backup/verify.txt。

参考

创建要恢复的一整套内容

在 /root/ops-backup/crd.yaml 中写入 CustomResourceDefinition widgets.ops.example.com——组 ops.example.com,种类 Widget(复数形式 widgets),命名空间范围,版本 v1(served 和 storage 都为 true),并且 schema 中有整数 spec.size。然后在 /root/ops-backup/scene.yaml 中把命名空间 ops-shop 的四个对象写入同一个文件——Deployment catalog(副本数 2,标签 app: catalog,镜像 nginx:1.27.3),同名的 ClusterIP Service catalog(选择器 app: catalog,端口 80),ConfigMap catalog-config(currency: KRW,page-size: "20"),Widget w1(spec.size 为 3)。创建命名空间并全部应用。

备份实验的一半,是决定要备份什么。故意创建混合了多种类型的一整套内容,是有原因的——工作负载、配置和自定义资源,在导出时要清除的字段不同,恢复顺序也各不相同。CRD 属于集群范围,所以即使删除命名空间也会保留。

原封不动地导出

把四个对象分别不加工、原样导出到 /root/ops-backup/raw/ 之下——deploy.yaml(Deployment catalog)、svc.yaml(Service catalog)、cm.yaml(ConfigMap catalog-config)、widget.yaml(Widget w1)。kubectl get <종류> <이름> -o yaml(占位符依次为种类与名称)的输出不要动它,原样保存。

先看一下原始的样子很重要。里面包含了下一步要清除的所有内容——只在该集群中有意义的标识符、由控制器填充的状态、托管字段的历史。是什么、为什么会成为问题,要在清除之前看一遍才会明白。

清除只在该集群中才有意义的内容

创建 /root/ops-backup/clean.sh——读取作为参数传入的一个 YAML 文件,把清理后的结果仅输出到标准输出。要清除的有 metadata 中的 uid、resourceVersion、creationTimestamp、generation、managedFields、ownerReferences、namespace,顶层的 status,以及 Service 的 spec.clusterIP、spec.clusterIPs、spec.ports[].nodePort。用这个脚本清理 raw/ 中的四个文件,并以同样的文件名保存到 /root/ops-backup/clean/。

连 metadata.namespace 也清除,是为了让恢复的位置由命令一侧来决定——这样同一份备份才能原样用于其他命名空间或其他集群。yq 的 del 即使要求删除不存在的路径也不会报错,所以不必按种类拆分脚本。

制造事故

把命名空间 ops-shop 整个删除,确认已被删除后,把它写为一行记录到 /root/ops-backup/gone.txt——ops-shop 制表符 NotFound。删除之前,请先确认 /root/ops-backup/raw/ 中的四个文件都在。

删除命名空间会把其中的对象全部带走。CRD 属于集群范围,会保留下来,但用该 CRD 创建的自定义资源会随命名空间一起消失。请等到删除结束之后再记录——kubectl delete ns 会等待,但亲自确认更稳妥。

恢复到另一个命名空间

创建命名空间 ops-shop-restored,并把 /root/ops-backup/clean/ 中的四个文件恢复到该命名空间(清理后的清单中没有命名空间,所以在命令中指定)。等到 Deployment 的 2 个 Pod 变为 Running 之后,把它保存为四行到 /root/ops-backup/restored.tsv——Deployment 制表符 catalog,Service 制表符 catalog,ConfigMap 制表符 catalog-config,Widget 制表符 w1,按种类名称升序。

恢复的目标命名空间由命令来决定,这是对象级备份的优点——可以建立起这样的流程:用同一份备份先在验证用命名空间中搭建一遍,再进行真正的恢复。新命名空间的 default ServiceAccount 需要过一会儿才会生成。

留下过期的所有者,恢复出来的对象就会消失

在 /root/ops-backup/victim.yaml 中写入 ConfigMap orphan-victim——命名空间 ops-shop-restored,数据 note: restored-with-stale-owner,并在 metadata.ownerReferences 中写入这个集群中不存在的所有者(apiVersion: v1,kind: ConfigMap,name: catalog-config-old,uid 用任意 UUID)。应用之后,用条件循环等待该对象消失。然后在 /root/ops-backup/fixed.yaml 中不带 ownerReferences 写入数据相同的 ConfigMap orphan-fixed 并应用,再把结果按两行留在 /root/ops-backup/owner.tsv 中——orphan-victim 制表符 gone,orphan-fixed 制表符 alive。

如果备份中原样保留 ownerReferences,恢复出来的对象会悄悄消失。所有者的 uid 因集群而异,如果在恢复的集群中找不到拥有该 uid 的对象,垃圾回收器就会认为所有者已经消失而将其删除。由于既没有错误也没有事件留下,这是一种很难找到原因的事故。

顺序出错,恢复就会失败

在 /root/ops-backup/gadget.yaml 中写入自定义资源 Gadget g1——apiVersion: ops.example.com/v1,命名空间 ops-shop-restored,spec.color 为 blue。在还没有该种类的 CRD 的状态下先应用,把错误保存到 /root/ops-backup/order-error.txt(包含标准错误)。然后在 /root/ops-backup/gadget-crd.yaml 中写入 CRD gadgets.ops.example.com(组 ops.example.com,种类 Gadget,复数形式 gadgets,命名空间范围,版本 v1,字符串 spec.color)并应用,再重新应用自定义资源。最后在 /root/ops-backup/restore-order.txt 中用四行写下恢复顺序——namespace、crd、custom-resource、workload,按正确的顺序每行一个。

不认识某个种类的 API 服务器,根本不会接收该清单。请原样读一下错误句子要你先安装什么。把恢复流程写成文档时,如果不写下这个顺序,恢复当天就会遇到同样的错误,而那时不会像现在这样有余裕。

不让人用眼睛去数恢复是否成功

在 /root/ops-backup/inventory.tsv 中用四行写下恢复清单——格式为 <종류> 制表符 <이름> 制表符 <기대값>(占位符依次为种类、名称与期望值),期望值是:Deployment 为 spec.replicas,Service 为 spec.ports[0].port,ConfigMap 为 data 的键个数,Widget 为 spec.size。然后创建 /root/ops-backup/verify-restore.sh——读取这张表,与 ops-shop-restored 中的实际对象对照,相符则输出 OK <종류>/<이름> <값>(占位符依次为种类、名称与值),不符则输出 MISMATCH <종류>/<이름> 기대=<값> 실제=<값>(韩文,意为“期望”与“实际”,占位符依次为种类、名称、期望值与实际值),仅输出到标准输出,只要有一行不符,就必须以非 0 的退出码结束。把该输出保存到 /root/ops-backup/verify.txt。

恢复之后如果让人看着画面去数是否都在,就一定会漏掉一个。而且,对象存在和值相同是两回事——只数名称的话,副本数从 2 减到 1 的恢复也会看起来成功。如果脚本直接写文件,评分器再次运行时会覆盖学员的产出,所以请只通过标准输出输出。