TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

封闭式网络 k0s 多节点实测 3:3 节点 HA 上的 Rook-Ceph 与 CloudNativePG

状态:实测 (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 个节点有两种方式。

(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。

  1. CPLB(control plane load balancing)。k0s 启动 keepalived 通过 VRRP 将虚拟 IP 挂载到一个节点上,当该节点宕机时会转移到其他节点。 CPLB 文档将其解释为用于「集群外部访问」的用途。这样既不会增加需要导入的东西,但要求节点必须在同一个 L2 网络中(文档:虚拟路由器 ID 在广播域内必须唯一)。在主工作一体机中,仅支持 k0s 的用户空间反向代理方式。
  2. NLLB(node-local load balancing)。在每个节点的回环地址(127.0.0.1:7443)上运行 envoy,使 kubelet 等节点组件能够均匀地连接到存活的 apiserver 集合。 NLLB 文档指出 NLLB 仅用于集群内部的容错,不提供外部访问支持。
  3. 外部负载均衡器(如 HAProxy 等)。 HA 文档的示例将 6443(API)、8132(konnectivity)、9443(加入 API)组合起来。该负载均衡器本身需要做双机热备,并且会增加需要导入的软件包。本次未进行测量(未实测)。

CPLB 文档建议,如果没有外部负载均衡器,应同时使用 CPLB 和 NLLB。本次配置正是如此。

외부 로드밸런서 없이 컨트롤러 셋 controller+worker 셋, CPLB + NLLB, 주소는 예시 kubectl k0sctl VIP 192.0.2.100 한 노드에만(VRRP) 반입 서버 레지스트리 미러 노드 1 VIP 보유 etcd kube-apiserver envoy(NLLB) Ceph mon · osd PostgreSQL 노드 2 etcd kube-apiserver envoy(NLLB) Ceph mon · osd PostgreSQL 노드 3 etcd kube-apiserver envoy(NLLB) Ceph mon · osd PostgreSQL kubelet 은 자기 노드의 envoy(127.0.0.1:7443)를 거쳐 세 apiserver 로 이미지는 containerd hosts.toml 로 반입 서버의 미러에서 etcd 3멤버: 1대 장애는 견디고, 2대가 멈추면 쓰기가 멈춘다
图:无外部负载均衡器的 3 个控制节点:VIP 仅在一个节点上启动,kubelet 通过自身节点的 envoy 访问三个 apiserver

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}

这里也体现了存在两个网卡的事实,涉及三个地方。

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 测量两项指标。

首先找到持有 VIP 的节点(内网 NIC 上绑定了 VIP 的节点),停止其 k0scontroller ,并测量从停止时刻到 API 和写入功能恢复的时间。接着停止第二个控制面节点,观察 30 秒内写入是否能成功哪怕一次,然后重新启动这两个节点,测量写入功能恢复所需的时间。

5.2 结果

멈추고 다시 서기까지(실측) 같은 정답지로 처음부터 네 번 세운 결과 VIP 를 가진 컨트롤러 정지 → 다시 되기까지 API(/readyz, VIP) 쓰기(ConfigMap 생성) 1회차 0초 1초 2회차 3초 3초 3회차 0초 · 쓰기 시각은 기록하지 않음 4회차 1초 · 쓰기 시각은 기록하지 않음 둘을 멈추면 30초 동안 쓰기 전부 실패, 다시 켜고 1~2초 CNPG primary 파드 삭제 → 새 primary 에 쓰기 1회차 미실측(정답지가 끊겨 단계가 섞임) 2회차 a 39.8초 2회차 b 49.5초 3회차 62.6초 4회차 35.4초 2회차 a 는 WAL 아카이빙이 깨진 상태(함정 2) 옛 primary 는 4번 중 3번이 5분 넘게 Terminating
图表:停止持有 VIP 的控制面节点时的 API·写入恢复时间(4次)以及删除 CNPG primary 后到向新 primary 写入的时间(4次)
轮次 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秒内未能结束。

넘긴 WAL 이 올라갔는지 기다리는 법 PostgreSQL 17 부터 경계 위치의 파일 이름 규칙이 바뀌었다 1002 를 넣은 곳 WAL 파일 N WAL 파일 N+1 pg_switch_wal() 이 돌려준 위치 = 이 경계 pg_walfile_name(경계) 의 답 PostgreSQL 16 까지 → N(앞 파일) 기다릴 대상으로 맞다 PostgreSQL 17 부터 → N+1(지금 파일) 다음 전환 전엔 안 올라간다 넘기기 전에 pg_current_wal_lsn() 의 파일 이름(N)을 적고 pg_stat_archiver.last_archived_wal 이 N 이상이 되면 복원
图:pg_switch_wal 返回的位置是文件边界,从 PostgreSQL 17 开始,该边界的文件名是下一个文件

最初编写的代码使用 pg_walfile_name(pg_switch_wal()) 获取了要等待的文件名。根据 PostgreSQL 文档, pg_switch_wal() 返回「刚结束的 WAL 文件的结束位置 + 1」,即 下一个文件开始的边界。将边界位置传入 pg_walfile_name 后会得出什么名称,取决于具体的版本。

因此,在 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 在后台运行,并单独查询是否结束。在此过程中,我们遇到了一个陷阱和两个小惊奇。

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 的上限。设定上限后并未长期运行观察(未实测)。

更改 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 分钟,所以不要看到新班长上台就结束,请一定要确认旧班长是否已经回到了座位!

登录后即可点赞

评论

还没有评论。

登录后即可发表评论