状态:实测 (2026-10-31)。 这是在 TT Lab 实验 VM(三台 8 vCPU·8GiB 和一台导入服务器)中搭建 k0s v1.37.1+k0s.0 测得的值。我们在修改相同标准答案的同时,从头到尾连续搭建了四次,其中第 1 轮由于中间标准答案中断导致步骤错乱,故作未实测处理。数值分散的幅度即为该环境的实际波动范围。VM 磁盘位于 HDD 上。节点地址已替换为示例网段(192.0.2.0/24)。编写规则:以实测数据为基础,无法测量的标注为「未实测」,原因未明的解释标注为「推测」。
目录
- 1. 从结论开始
- 2. 如何划分 3 个节点
- 3. 将镜像镜像到内部镜像仓库
- 4. 安装:k0sctl 不等待控制工作一体机的 Ready
- 5. 停止一个或两个控制节点时
- 6. Rook-Ceph
- 7. CloudNativePG
- 8. 如何运行耗时较长的步骤
- 9. 资源
- 10. 局限与未实测
- 11. 总结
1. 从结论开始
- 3 个节点全部设为主工作一体机。 etcd 的 3 个成员可承受 1 个节点故障。API 地址在没有外部负载均衡器的情况下,通过 k0s 内置的 CPLB(keepalived 虚拟 IP)和 NLLB(每个节点的 envoy)组合而成。
- 当停止持有虚拟 IP (VIP) 的节点时,四次测试中 API 均在 0~3 秒内恢复。 测量写入的两次测试中,恢复时间分别为 1 秒和 3 秒。当停止两个节点时,30 秒内写入全部失败;重新启动后,1~2 秒内即恢复。
- 将 18 个镜像(1.8GB)镜像到内部镜像仓库,并以 containerd 的
hosts.toml运行。 没有修改任何 Chart 和清单文件中的镜像名称。 - Rook-Ceph 从安装 Chart 到达到 HEALTH_OK 状态耗时 437~605 秒(7~10 分钟)。只需调整 k0s 的 kubelet 路径即可。
- CloudNativePG 在删除主 Pod 后 35.4~62.6 秒内由新主 Pod 接收写入,统计行数的两次测试结果均完全一致。 然而,被删除的旧主 Pod 在四次中有三次在 Terminating 状态停留超过了 5 分钟。通过 Ceph 对象存储备份(4~53 秒)并恢复到新集群(1 分钟 54 秒~4 分钟 6 秒)后,备份之后写入的行也通过 WAL 成功恢复了。
- 四个坑:k0sctl 不会等待控制工作一体机的 Ready 状态。从 PostgreSQL 17 开始,
pg_walfile_name的边界规则发生变化,导致等待 WAL 归档的脚本无法结束。如果在桶名称生成之前创建备份配置,WAL 归档会损坏。systemctl is-active对正在运行的 oneshot 单元返回「否」。
2. 如何划分 3 个节点
划分 3 个节点有两种方式。
| (a) 三台主工作一体机(本周场景) | (b) 1 个控制节点 + 2 个工作节点 | |
|---|---|---|
| etcd | 3 成员,可承受 1 台故障 | 1 成员,控制平面为单点故障 |
| 放置工作负载的节点 | 3 台 | 2 台 |
| Ceph(3 个 mon,3 副本),3 个 PostgreSQL | 每个节点一个,可承受 1 台故障 | 只有 2 个存储节点,无法实现 3 副本 |
| API 地址 | CPLB VIP + NLLB,无需外部负载均衡器 | 单个控制节点地址 |
| 代价 | 控制平面和工作负载共享内存。节点 8GiB,需降低 Ceph 守护进程内存 | 简单 |
| 测试 | 本文全篇 | 第 2 篇 第 5 节实际搭建 |
采用 (b) 无法实现本文目标的存储高可用。Ceph 的默认块存储池会将 3 个副本放在不同的主机上(failureDomain: host),为了保证多数派,3 个 mon 会被放在不同的节点上。如果只有两个存储节点,则没有地方放置 3 个副本,导致数据保持在 undersized+degraded 状态。如果降为 2 个副本,则在 1 台故障时写入会停止(min_size 2),或者只能靠 1 个副本勉强维持(min_size 1)。出于同样的原因,在 2 台或更少节点的环境中,我们没有部署 Ceph 和 PostgreSQL 集合。Ceph 的放置原理详细记载于
Rook-Ceph 解析 系列文章中。
2.1 将 API 地址合并为一个的三种途径
当有 3 个控制节点时,需要确定 kubectl 和节点通过哪个地址来访问 API。
- CPLB(control plane load balancing)。k0s 启动 keepalived 通过 VRRP 将虚拟 IP 挂载到一个节点上,当该节点宕机时会转移到其他节点。 CPLB 文档将其解释为用于「集群外部访问」的用途。这样既不会增加需要导入的东西,但要求节点必须在同一个 L2 网络中(文档:虚拟路由器 ID 在广播域内必须唯一)。在主工作一体机中,仅支持 k0s 的用户空间反向代理方式。
- NLLB(node-local load balancing)。在每个节点的回环地址(127.0.0.1:7443)上运行 envoy,使 kubelet 等节点组件能够均匀地连接到存活的 apiserver 集合。 NLLB 文档指出 NLLB 仅用于集群内部的容错,不提供外部访问支持。
- 外部负载均衡器(如 HAProxy 等)。 HA 文档的示例将 6443(API)、8132(konnectivity)、9443(加入 API)组合起来。该负载均衡器本身需要做双机热备,并且会增加需要导入的软件包。本次未进行测量(未实测)。
CPLB 文档建议,如果没有外部负载均衡器,应同时使用 CPLB 和 NLLB。本次配置正是如此。
2.2 k0sctl 配置
仅编写单个主机。其余两个主机仅地址不同。
- role: controller+worker
noTaints: true # 컨트롤러에도 워크로드를 놓는다
ssh: {address: 192.0.2.3, user: root, keyPath: /root/.ssh/id_ed25519}
hostname: ctrl01
privateInterface: eth1
privateAddress: 192.0.2.3
uploadBinary: true
k0sBinaryPath: /opt/airgap/bin/k0s-v1.37.1+k0s.0-amd64
installFlags: [--kubelet-extra-args=--node-ip=192.0.2.3]
files:
- {name: k0s-airgap-bundle, src: /opt/airgap/images/k0s-airgap-bundle-v1.37.1+k0s.0-linux-amd64.tar, dstDir: /var/lib/k0s/images/, perm: "0644"}
- {name: apps-bundle, src: /opt/airgap/images/apps-bundle.tar, dstDir: /var/lib/k0s/images/, perm: "0644"}
- name: cri-registry
dst: /etc/k0s/containerd.d/cri-registry.toml
data: |
version = 3
[plugins."io.containerd.cri.v1.images".registry]
config_path = "/etc/k0s/containerd.d/certs.d"
- name: mirror-quay.io # docker.io·ghcr.io·registry.k8s.io·gcr.io 도 같은 모양
dst: /etc/k0s/containerd.d/certs.d/quay.io/hosts.toml
data: |
server = "https://quay.io"
[host."http://192.0.2.2:5000/v2/quay.io"]
capabilities = ["pull", "resolve"]
override_path = true
k0s:
version: v1.37.1+k0s.0
config:
spec:
api: {onlyBindToAddress: true, sans: [192.0.2.100]}
network:
provider: kuberouter
podCIDR: 172.20.0.0/16
serviceCIDR: 172.21.0.0/16
controlPlaneLoadBalancing:
enabled: true
type: Keepalived
keepalived:
vrrpInstances:
- {virtualIPs: ["192.0.2.100/24"], authPass: <클러스터마다 다른 값>, interface: eth1}
nodeLocalLoadBalancing: {enabled: true, type: EnvoyProxy}
images: {default_pull_policy: Never}
telemetry: {enabled: false}
这里也体现了存在两个网卡的事实,涉及三个地方。
onlyBindToAddress: true是基于第 2 篇 3.1 节的原因而加入的。如果控制节点有 3 个,每个主机都没有地方填写address,因此这是唯一的途径。- 还必须填写 VRRP 的
interface: eth1。根据 CPLB 文档,如果留空,k0s 将选择 具有默认路由的 NIC 。在此 VM 中它是 NAT NIC,将 VIP 挂载到那里其他节点将无法访问。 sans在中放入 VIP。如果不放入,当通过 VIP 访问时,API 服务器证书中将不包含该地址,从而导致 TLS 失败。
authPass 如文档所述,不是加密而是用于防止不同集群意外混淆的值。需确保它与同一网络中的其他集群不冲突。安装后,将
k0sctl kubeconfig 提供的 kubeconfig 的服务器地址改为了 VIP。如果指定单个控制节点,当该节点宕机时 kubectl 也会失效。
3. 将镜像镜像到内部镜像仓库
k0s 系统镜像像第 1 篇一样打包导入。然而,Rook-Ceph、CSI 驱动、CloudNativePG 和 cert-manager 的镜像数量众多且体积庞大。与其在每个节点上分发打包文件,不如在导入服务器上搭建一个镜像仓库并进行镜像同步。
| 镜像包 | 数量 | 示例 |
|---|---|---|
| Rook·Ceph | 2 | rook/ceph:v1.21.1(565MB), ceph/ceph:v20.2.4(543MB) |
| Ceph CSI | 7 | ceph-csi-operator v1.1.0, cephcsi v3.18.1, 5 个 sig-storage 边车 |
| CloudNativePG | 4 | operator 1.30.1, postgresql 18.6-minimal-trixie, barman-cloud 插件与边车 v0.15.1 |
| cert-manager | 3 | v1.21.3 的 controller·webhook·cainjector |
| 应用 | 2 | nginx 1.30.6-alpine, busybox 1.36.1-1 |
导入服务器以 skopeo copy 的方式将 192.0.2.2:5000/quay.io/ceph/ceph:v20.2.4 像 在路径前附加原始仓库名称 那样导入。节点端的 containerd 为每个仓库使用一个 hosts.toml 指向该路径。根据 containerd 的 hosts.md说明,
override_path = true 意味着「API 的根目录写在 URL 路径中」,因此 containerd 不会重新附加 /v2/ 而是像 http://192.0.2.2:5000/v2/quay.io/ceph/ceph/manifests/...
这样请求。因此, Chart 和清单文件中的镜像名称一个也没有改动。 在封闭网络中,如果逐个修改值文件中的镜像名称,必定会漏掉一个,而那一个会在几分钟后通过 ImagePullBackOff 暴露出来。
分发文件的格式遵循了 k0s 运行时文档。文档指出,此版本的 k0s 必须使用 version = 3 和
io.containerd.cri.v1.images 插件路径,如果使用 version = 2 或旧的插件路径,预检将会卡住。
需要镜像的列表基于 Rook 仓库的 deploy/examples/images.txt,并去掉了未使用的 COSI、csi-addons 边车和 snapshot-metadata。如果有遗漏,通过 ImagePullBackOff 可以立即发现,因此在精简列表时无需害怕。
| 项目 | 实测 |
|---|---|
| 导入(工具 27 秒 → 二进制及打包 → helm 图表及清单 57 秒 → 镜像 18 个镜像同步 → 封存) | 182秒, 镜像仓库 1.8GB |
| rook-ceph 的容器 46 个 | 镜像全部在同步列表中 |
| 镜像仓库访问记录 | 每个节点 96~104 条(所有节点组合均从镜像同步获取) |
在此记录一些局限性。测试环境的镜像仓库是内网中的 HTTP。在实际现场中会使用 TLS 并 hosts.toml 会提供内网 CA(ca = ...)(未实际测量)。连 k0s 系统镜像也从镜像仓库拉取的方式(spec.images.repository)也未进行测量(未实际测量)。将系统镜像打包放置的原因是,即使导入服务器的镜像仓库挂掉,当节点重启时也能让 CNI·kube-proxy·coredns 正常启动。
4. 安装:k0sctl 不等待控制面兼工作节点的 Ready 状态
| 轮次 | k0sctl apply(k0sctl 报告) |
三者皆 Ready(从 apply 开始算) |
|---|---|---|
| 1 | 89秒 | 未测量(答案中断导致阶段混合) |
| 2 | 93秒 | 节点注册在 apply 结束后 +5秒·+104秒·+125秒 |
| 3 | 72秒 | 217秒 |
| 4 | 109秒 | 324秒 |
apply 结束并不意味着集群完全就绪。在第 2 轮中,apply 在 93 秒时成功结束,但其中的两台节点在之后的 104 秒和 125 秒后,kubelet 才留下了「Successfully registered node」日志。
查看 k0sctl 源码后,原因就很明显了。 install_workers.go中的「waiting for node to become ready」仅在 Workers() 中运行,且该函数精确挑选角色为 worker 的主机。 controller+worker 在控制面安装阶段就已结束,因此它不会等待 kubelet 变为 Ready。所以在 apply 之后衔接的自动化脚本必须自行确认节点组是否为 Ready 状态。如果不加检查直接进入下一阶段,就会在漏掉一个节点的情况下安装 Ceph。
5. 停止一个或两个控制面节点时
5.1 测量方法
在导入服务器上以每秒 1 次的频率通过 VIP 测量两项指标。
- API:
kubectl get --raw /readyz(4秒超时)。 - 写入:创建 ConfigMap(6秒超时)。每次都赋予全新的名称。如果再次使用相同的名称创建,会因为
AlreadyExists失败,从而无法与「因失去多数派而失败」区分开来。最初的尝试确实留下了这种虚假的失败记录。
首先找到持有 VIP 的节点(内网 NIC 上绑定了 VIP 的节点),停止其 k0scontroller ,并测量从停止时刻到 API 和写入功能恢复的时间。接着停止第二个控制面节点,观察 30 秒内写入是否能成功哪怕一次,然后重新启动这两个节点,测量写入功能恢复所需的时间。
5.2 结果
| 轮次 | VIP 漂移 | API 恢复 | 写入恢复 | 停止两个(1/3) | 重新启动后写入 |
|---|---|---|---|---|---|
| 1 | 节点 3 → 节点 2 | 0秒 | 1秒 | 30秒内全部失败 | 1秒 |
| 2 | 节点 1 → 节点 3 | 3秒 | 3秒 | 30秒内全部失败 | 1秒 |
| 3 | 节点 1 → 节点 3 | 0秒 | 未记录 | 30秒内全部失败 | 2秒 |
| 4 | 节点 1 → 节点 3 | 1秒 | 未记录 | 30秒内全部失败 | 1秒 |
停止一台节点时,存活节点的 k0s etcd member-list 中仍然保留着完整的成员集合。停止节点并不等于将其从成员中移除,因此多数派依然是 2,存活的两个节点凑齐了多数派并接受了写入。如果停止两个节点,剩下的一个节点无法满足多数派,因此写入会停止。这与第 2 篇中的 etcd 表格完全一致。
这里需要注意一点。即使停止了 kubelet,Node 对象的 Ready 也会在一段时间内保持旧值。因为控制器管理器将节点更改为「无响应」需要等待一段时间(--node-monitor-grace-period, 文档中默认为 50 秒)。“在发生故障期间节点依然是 Ready”并不能作为高可用性的证据。因此上面的表格不是通过 Node 状态,而是通过实际请求来测量的。
6. Rook-Ceph
6.1 k0s 带来的差异:kubelet 路径
k0s 的 kubelet 使用的工作目录不是 /var/lib/kubelet 而是 /var/lib/k0s/kubelet (k0s 存储文档)。CSI 节点插件通过将 kubelet 目录作为 hostPath 挂载来挂载卷,因此如果此路径错误,即使 PVC 绑定成功也无法挂载到 Pod 上。
Rook v1.21 将 CSI 驱动程序从 ceph-csi-operator 中拆分出来,作为单独的图表(ceph-csi-drivers)。在 rook-ceph 图表的 values 中找不到 kubelet 路径,将其填入了驱动程序图表的 values 中。
# ceph-csi-drivers 1.1.0 의 values(Rook 이 권하는 값에 이 줄만 더함)
operatorConfig:
driverSpecDefaults:
nodePlugin:
kubeletDirPath: /var/lib/k0s/kubelet
另一种方法是将 k0sctl 主机的 kubeletRootDir 软链接到 kubelet 端的 /var/lib/kubelet ,但并未进行此项测量(未实际测量)。由于驱动程序 DaemonSet 是由 operator 根据 Driver 对象创建的,因此在图表安装刚结束时(23秒)它还不存在。安装刚结束时检查 DaemonSet 的自动化脚本必须等待其创建完成。
6.2 实际测量
图表集(rook-ceph、ceph-csi-drivers、rook-ceph-cluster v1.21.1、Ceph v20.2.4)通过 helm 从导入的 tgz 中安装。每个节点使用一个 17GiB 的空磁盘作为 OSD,并且由于节点同时承担工作负载,因此将 OSD 的 osd_memory_target 降低到了 1GiB。
| 项目 | 值 |
|---|---|
| 图表安装 → HEALTH_OK(OSD 3 up·in, mon 3) | 546秒 · 605秒 · 437秒(第 2·3·4 轮) |
| 原始容量 / 3副本块存储池可用容量 | 51GiB / 15GiB |
| 存储池与 PG | 10 个存储池(块存储 1,RGW 对象存储 8,.mgr),121 个 PG active+clean |
HEALTH_OK 是在确认了 Ceph v20 发出的 cephx 密钥格式警告集(AUTH_INSECURE_*)的原因并将其静默处理后的数值。在跑完所有步骤直到最后后,变为了
BLUESTORE_SLOW_OP_ALERT(慢盘警告)导致 HEALTH_WARN。这是在 HDD 上的虚拟机中施加 PostgreSQL 负载的结果。在现场设备中,这是一个需要从磁盘开始重新审视的数值。
7. CloudNativePG
7.1 安装顺序与备份方式
按照 cert-manager → CNPG 运营商(1.30.1)→ barman-cloud 插件(v0.15.1)的顺序导入了清单。CNPG 清单按照
安装文档使用 kubectl apply --server-side 进行应用。
选择插件进行备份的原因是版本演进的趋势。CNPG 1.30 的 备份文档原文指出,本体中包含的 barmanObjectStore 方式从 1.26 开始停止支持,1.31 是支持该方式的最后一个版本,并将在 1.32 中移除。没有理由用一种即将淘汰的方式来启动一个新建的集群。插件需要依赖 cert-manager(插件安装文档)。由于备份工具包含在插件边车(sidecar)中,因此 PostgreSQL 镜像使用精简的 18.6-minimal-trixie 就足够了。
备份的目标位置是同一集群中的 Ceph 对象存储(RGW)。当通过 ObjectBucketClaim 创建存储桶时,Rook 会提供同名的 ConfigMap(存储桶名称、地址)和 Secret(密钥)。不过, 同一集群中的 RGW 并不属于独立的故障域。 如果整个集群丢失,备份也会随之丢失,因此在生产环境中必须将其额外发送到外部存储。
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata: {name: pg, namespace: db}
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:18.6-minimal-trixie
storage: {size: 2Gi, storageClass: ceph-block}
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters: {barmanObjectName: ceph-rgw}
在第 2 轮中,所有实例组变为 Ready 大约耗时 13 分 40 秒(initdb 70秒,其余为副本镜像拉取与加入)。由于受限于 HDD 上的虚拟机这一条件,该数值受很大影响,不能直接泛化,但很明显,在设定自动化等待上限时,应该以“十几分钟”而不是“几分钟”来考量。
7.2 陷阱:在存储桶名称生成前创建 ObjectStore 时
在初次尝试中,ObjectBucketClaim 未能在 2 分钟内变为 Bound,但脚本在等待结束后直接进入了下一步。因此在存储桶名称为空的情况下创建了 ObjectStore,导致 destinationPath 变成了 s3:/// 。WAL 归档因 Invalid s3 URL address 而持续失败。
在这种状态下删除 primary Pod 事情就闹大了。虽然新 primary 在 39.8 秒内启动,但由于携带着旧 primary 未能妥善保存的 WAL,导致其在 Terminating 状态下停留了 18 分钟以上 。由于 CNPG Pod 的 terminationGracePeriodSeconds 为 1800 秒,Kubernetes 便等待了这么久。现在我们改为等待 ConfigMap 的 BUCKET_NAME 生成并且 OBC 达到 Bound 状态,如果最终为空则直接中止。
7.3 删除 primary 时
当实例组全部为 Ready 时删除 primary Pod,并每隔 0.5 秒向新 primary 询问 select pg_is_in_recovery() 以测量直到出现 f(可写入)为止的时间。
重要的一点是:仅在三者皆为 Ready 时才开始。 曾在答案中断的情况下,删除了一个连一个副本都还没启动的 primary,导致集群丢失。
| 轮次 | 到新 primary 可写入的时间 | 行 | 旧 primary |
|---|---|---|---|
| 2 a(归档损坏状态,7.2) | 39.8秒 | 未记录 | Terminating 超过 18 分钟 |
| 2 b | 49.5秒 | 保持 1001 行 | 恢复为副本 |
| 3 | 62.6秒 | 未记录 | Terminating 超过 1418 秒(23 分钟) |
| 4 | 35.4秒 | 保持 1001 行 | Terminating 超过 320 秒(仅测量了 5 分钟) |
新 primary 的升格与写入没有问题。问题在于旧 primary。在归档正常的 3 次中,有 2 次的 Terminating 也超过了 5 分钟,且该 Pod 的日志以如下内容结束。
"msg":"unable to run controller-runtime manager",
"error":"failed waiting for all runnables to end within grace period of 30s: context deadline exceeded"
PostgreSQL 已经通过 server stopped 正常退出,但 CNPG 的 instance manager 留下了一个未能在 30 秒内完成内部清理工作的错误,并且进程并未结束。子 postgres 进程作为僵尸进程(<defunct>)残留下来,Pod 走向了耗尽 1800 秒宽限期(grace period)的流程。原因未能确定。虽然这看起来与启用 barman-cloud 边车会导致 primary Pod 终止耗时约 3 分钟的问题 plugin-barman-cloud#1039(开启)属于同一类问题,但本次观测到的时间要长得多(推测)。
为什么需要关注这一点。在旧 primary 处于 Terminating 期间,集群仅依靠 2 个 Ready 实例支撑。若在此期间再有一个节点死亡,就会陷入只剩一个副本的状态。即便尝试对该 Pod 所在的节点进行 drain 也无法完成。在故障转移之后,必须确认旧 primary 是否真的恢复成了副本。减少此现象的配置(调整宽限期等)并未进行测试(未实际测量)。
7.4 备份与恢复,以及如何等待 WAL
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata: {name: pg-b1, namespace: db}
spec:
cluster: {name: pg}
method: plugin
pluginConfiguration: {name: barman-cloud.cloudnative-pg.io}
备份结束后,再多写入一行(1002),切换 WAL 文件,确认该 WAL 已上传至存储桶,然后 bootstrap.recovery 创建了新的集群 pg-restore 。
如果在备份时间点(1001行)之后写入的 1002行也能恢复,就说明 WAL 恢复成功了。
| 轮次 | 备份 | 创建恢复集群 → Ready | 备份源的 count, max(id) |
|---|---|---|---|
| 2(首次尝试) | 53秒 | 4分6秒 | 1001, 1001(WAL 仅等待 15秒) |
| 2(再次) | 4秒 | 3分15秒 | 1002, 1002 |
| 3 | 未记录 | 2分0秒 | 1002, 1002 |
| 4 | 37秒 | 1分54秒 | 1002, 1002 |
首次尝试时切换了 WAL 且只等待了 15秒,恢复源停在了备份时间点(1001)。因为恢复操作只读取到那时为止上传到存储桶的 WAL。因此,加入了「等待切换的 WAL 归档完成」的逻辑,但这次等待在 300秒内未能结束。
最初编写的代码使用 pg_walfile_name(pg_switch_wal()) 获取了要等待的文件名。根据 PostgreSQL 文档, pg_switch_wal() 返回「刚结束的 WAL 文件的结束位置 + 1」,即 下一个文件开始的边界。将边界位置传入 pg_walfile_name 后会得出什么名称,取决于具体的版本。
- 直至 PostgreSQL 16 文档都写着:「如果位置正好是边界,则返回 前一个 文件的名称,在处理归档时这通常是期望的行为」。因此,过去文章中的这种惯用法是正确的。
- PostgreSQL 17 发行说明的「迁移到 17」一节中记载,这两个函数已被修改为在边界处返回 当前文件,而不是前一个文件。本次测试所用的 PostgreSQL 版本为 18.6。
因此,在 17 及以上版本中, pg_walfile_name(pg_switch_wal()) 是尚未开始写入的下一个文件的名称,该文件在发生下一次切换之前不会被归档。这就是等待没有结束的原因。修复的方法是改变顺序。
-- 1002 를 넣은 뒤, 넘기기 「전에」 지금 파일의 이름을 적는다
select pg_walfile_name(pg_current_wal_lsn()); -- 예: ...0B
select pg_switch_wal();
-- 그다음 last_archived_wal 이 적어 둔 이름 이상이 될 때까지 기다린다
select last_archived_wal from pg_stat_archiver;
按此修改后,三次尝试均成功恢复到了 1002行。值得一提的是,恢复结束后实例会重启一次(「Primary instance is being restarted without a switchover」)。如果只看到一次 Ready 就结束,就会刚好遇到 Ready 为空的瞬间。我们通过以 10秒间隔观察到两次「healthy」来结束。
8. 如何运行耗时的步骤
apply、Ceph、CNPG、故障测试、备份与恢复通常需要几分钟到十多分钟不等。为了避免守在终端前等待,我们用 systemd-run --property Type=oneshot 在后台运行,并单独查询是否结束。在此过程中,我们遇到了一个陷阱和两个小惊奇。
systemctl is-active对于正在运行的 oneshot 单元会回答「不是」。 systemd.service 文档指出,Type=oneshot单元在程序结束前一直处于「activating」状态。由于is-active仅在「active」时为真,因此未能判定为「仍在运行中」,并在需要等待的地方导致失败。查看systemctl show -p ActiveState --value <유닛>是否为activating。- helm v4.3.0 에서는
helm list -a的-a不存在,以unknown shorthand flag: 'a'结束。 - 同版本的
helm status -o json每次发布耗时 19秒。状态检查最好使用helm list -o json一次。
9. 资源
部署好所有组件后的三个节点(8 vCPU·8GiB)。
| 项目 | 值 |
|---|---|
| 内存使用 | 2.2~2.6 / 7.9GiB |
/var/lib/k0s |
6.4~6.9GiB(工作磁盘) |
| 根磁盘(2.4GiB) | 74~94% |
/var/lib/k0s 虽然将
和 Pod 日志移到了工作磁盘,但 journald 和 k0s 的日志仍堆积在根目录。如果根目录空间较小,必须设定 journald 的上限。设定上限后并未长期运行观察(未实测)。
- 10. 局限与未实测环境
- :由于是 KubeVirt VM 中的 k0s,绝对时间可能比物理服务器慢,且磁盘为 HDD。特别是 Ceph 和 CNPG 的耗时很大程度上取决于磁盘性能。重复次数
- :3个场景各四次,故障转移四次。请将其作为案例而非统计分布来阅读。未测内容
spec.images.repository:使用外部 HAProxy 绑定 API 的配置、用k0sctl apply --restore-from从企业内部镜像仓库拉取系统镜像、kubeletRootDir恢复、升级 k0s 版本、节点重启后的行为、TLS 仓库、用
更改 kubelet 路径、缩短旧 primary 的 Terminating 时间的配置、限制 journald 上限的长时间运行。
| 11. 总结 | 检查项 |
|---|---|
| 原因(本文实测结果) | 3台时三台皆为控制兼工作节点 |
etcd 3个成员可容忍1台故障,每个节点部署一个 Ceph·PG interface 编写 VRRP sans 和 |
留空会在默认网卡上浮现 VIP,如果没有 sans,TLS 将会失败 |
| kubeconfig 指向 VIP | 即使停止持有 VIP 的节点,API 延迟 0~3秒,写入延迟 1~3秒 |
| apply 后直接检查节点 Ready 状态 | k0sctl 不等待控制兼工作节点(+104·+125秒) |
镜像名称保持不变,使用 hosts.toml 镜像 |
无修改地拉取了 18 个镜像,共 1.8GB |
| Ceph CSI 的 kubelet 路径 | k0s 是 /var/lib/k0s/kubelet |
| 使用 barman-cloud 插件进行备份 | 原生方式将在 1.32 中移除 |
| 等待存储桶名称后 ObjectStore | 为空时 s3:///,归档中断,旧 primary 持续了 18 分钟以上 |
| 记录切换前的 WAL 名称 | 从 PostgreSQL 17 开始 pg_walfile_name(pg_switch_wal()) 是下一个文件 |
| 故障转移后甚至检查旧 primary | 四次中有三次 Terminating 超过 5 分钟 |
oneshot 单元使用 ActiveState 处理 |
is-active 对于运行中状态回答「不是」 |
本文的实测可以在 TT Lab 的实践课程「闭网 k0s:1、2、3节点及之上的 Ceph·PostgreSQL」(网站路径 /course/k0s-airgap-multinode)的第三模块「3节点 HA」中,使用相同的版本和半成品列表进行跟做练习。VIP 迁移、Rook-Ceph、CNPG 故障转移以及备份恢复均在真实 VM 上进行,评分器会逐阶段检查结果。
TT 小贴士。 如果选三个班长,即使有一个生病了,另外两个也可以商量决定。但如果两个人生病了,一个人什么也决定不了。而且搬走的班长(旧 primary)可能东西没收拾完在门口站了 30 分钟,所以不要看到新班长上台就结束,请一定要确认旧班长是否已经回到了座位!