TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

閉域網 k0s マルチノード実測 3: ノード3台HA上に Rook-Ceph と CloudNativePG

ステータス: 実測(2026-10-31)。 TT Lab 実習用 VM(8 vCPU・8GiB 3台と取り込みサーバー1台)内に k0s v1.37.1+k0s.0 を構築して測定した数値です。同じ正解手順を修正しながら最初から4回構築し、1回目は途中で手順が途切れて段階が混ざったため未実測としました。数値のばらつきの幅がこの環境の実態の幅となります。VM のディスクは HDD 上にありました。ノードのアドレスは例示用アドレス(192.0.2.0/24)に変更して記載しています。記述ルール: 実測データを基本とし、測定できなかったものは「未実測」、原因を確認できなかった解釈は「推定」と表示します。

目次

1. 結論から

2. 3台をどのように分けるか

ノード3台の構成には2つの方法があります。

(a) コントローラー兼ワーカー3台(今回のシナリオ) (b) コントローラー1台 + ワーカー2台
etcd 3メンバー、1台の障害に耐える 1メンバー、コントロールプレーンが単一障害点となる
ワークロードを配置するノード 3台 2台
Ceph(mon 3、3重複製)、PostgreSQL 3台 ノードごとに1つずつ、1台の障害に耐える ストレージノードが2台しかないため3重複製ができない
API アドレス CPLB VIP + NLLB、外部ロードバランサー不要 コントローラーのアドレス1つ
代償 コントロールプレーンとワークロードでメモリを共有する。ノード8GiB、Ceph デーモンのメモリを削減 シンプルである
検証 この記事全体 第2部 第5節で実際に構築

(b) の方法では、この記事の目的であるストレージの高可用性を満たせません。Ceph のデフォルトのブロックプールは異なるホストに3つのレプリカを配置し(failureDomain: host)、mon は過半数を維持するために3つをそれぞれ異なるノードに配置します。ストレージノードが2台しかない場合、3つのレプリカを配置する場所がなくなりデータが undersized+degraded の状態のままになります。レプリカ数を2に減らすと、1台の障害時に書き込みが停止するか(min_size 2)、1つのレプリカだけで耐えることになります(min_size 1)。同じ理由により、2台以下の環境には Ceph と PostgreSQL 3台構成をデプロイしませんでした。Ceph の配置原理は Rook-Ceph 解説 シリーズに詳しく記載されています。

2.1 API アドレスを1つにまとめる3つの方法

コントローラーが3台ある場合、kubectl やノードがどのアドレスを使って API にアクセスすべきかを決定する必要があります。

  1. CPLB(control plane load balancing)。k0s が keepalived を起動して VRRP により仮想 IP をいずれかのノードに割り当て、そのノードが停止した場合は別のノードに引き継ぎます。 CPLB のドキュメントでは、これを「クラスター外からのアクセス」用であると説明しています。外部からの取り込み項目が増えない一方で、ノードが同一の L2 ネットワーク上に存在する必要があります(ドキュメント: virtual router ID はブロードキャストドメイン内で一意である必要があります)。コントローラー兼ワーカー構成では、k0s のユーザー空間リバースプロキシ方式のみがサポートされます。
  2. NLLB(node-local load balancing)。ワーカーごとにループバック(127.0.0.1:7443)で envoy を起動し、kubelet などのノードコンポーネントが稼働中の apiserver 3台に均等に接続できるようにします。 NLLB のドキュメントでは、NLLB はクラスター内部の耐障害性を目的としており、外部からのアクセスはサポートしないと記載されています。
  3. 外部ロードバランサー(HAProxy など)。 HA のドキュメントの例では、6443(API)・8132(konnectivity)・9443(JOIN 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 は1台のノードのみに立ち上がり、kubelet は自ノードの envoy を経由して3台の apiserver に接続する

2.2 k0sctl の設定

ホストを1台のみ記述します。残りの2台はアドレスのみが異なります。

  - 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}

ここでも NIC が2つあるという事実が3箇所に関わってきます。

authPass はドキュメントにもある通り、暗号化のためではなく、同じネットワーク上の他のクラスターと誤って混ざるのを防ぐための値です。同一ネットワーク内の他のクラスターと重複しないように決定します。インストール後には k0sctl kubeconfig が出力した kubeconfig のサーバーアドレスを VIP に変更しました。単一のコントローラーを指定した場合、そのノードが停止すると kubectl も一緒に動作しなくなります。

3. イメージを社内レジストリにミラーリングする

k0s のシステムイメージは第1部と同様にアーカイブとして一括導入しました。しかし Rook-Ceph、CSI ドライバー、CloudNativePG、cert-manager のイメージは数が多く、サイズも大きいです。ノードごとにアーカイブファイルを転送する代わりに、取り込みサーバーにレジストリを1つ立ててミラーリングしました。

イメージアーカイブ 数 例
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、sig-storage サイドカー5個
CloudNativePG 4 オペレーター 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 を1つ指定してこのパスを指し示します。containerd の hosts.mdによると、 override_path = true は「API のルートが URL パスに記述されている」という意味になるため、containerd は /v2/ を再度付与せず http://192.0.2.2:5000/v2/quay.io/ceph/ceph/manifests/... のようにリクエストします。そのため、 チャートやマニフェストのイメージ名は一切修正しませんでした。 閉域網においてバリューファイルのイメージ名を1つずつ書き換えると必ず1つ見落としが発生し、その1つが数分後に 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-pressY・coredns が起動するようにするためです。

4. インストール:k0sctl はコントローラー兼ワーカーの Ready を待機しない

回数 k0sctl apply(k0sctl のレポート) 3つすべて Ready(apply 開始から)
1 89秒 未計測(正解データが途切れ、ステージが混在)
2 93秒 ノード登録が apply 終了後 +5秒・+104秒・+125秒
3 72秒 217秒
4 109秒 324秒

apply が完了したからといって、クラスターの構築が完全に終わったわけではありませんでした。第 2 回目では apply は 93秒で成功に終わりましたが、2台のノードはその後の 104秒と 125秒を経過してようやく kubelet が「Successfully registered node」のログを残しました。

k0sctl のソースコードを確認すると、その理由は明快でした。 install_workers.goの「waiting for node to become ready」は Workers() でのみ実行され、この関数は役割が正確に worker であるホストのみを選択します。 controller+worker はコントローラーのインストール段階で終了するため、kubelet が Ready になるのを待機しません。そのため、apply に続く自動化処理では、ノードセットが Ready であるかを自ら確認する必要があります。そのまま次の段階に進むと、ノードが1台欠けた状態で Ceph がインストールされることになります。

5. コントローラーを1台、2台と停止した場合

5.1 計測方法

持ち込みサーバーから VIP に対して次の2つの項目を 1秒ごとに計測しました。

まず VIP を保持するノード(社内網用 NIC に VIP がバインドされているノード)を特定して k0scontroller を停止し、停止した時点から API と書き込みが復旧するまでを計測しました。続いて 2台目のコントローラーも停止して 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 の復旧 書き込みの復旧 2台停止(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秒

1台を停止した際、稼働中のノードの k0s etcd member-list にはメンバーセットがそのまま残っていました。停止したノードはメンバーから除外されたわけではないため、過半数は依然として 2であり、稼働中の 2台がその過半数を満たして書き込みを受け付けました。2台を停止すると残りの 1台では過半数を満たせないため、書き込みが停止します。第 2部の etcd の表の通りです。

1点注意すべきことがあります。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 はバインドされてもポッドにアタッチされません。

Rook v1.21 では CSI ドライバーが ceph-csi-operator と別のチャート(ceph-csi-drivers)に分割されました。rook-ceph チャートの値では kubelet のパスが見つからなかったため、ドライバーチャートの値に指定しました。

# ceph-csi-drivers 1.1.0 의 values(Rook 이 권하는 값에 이 줄만 더함)
operatorConfig:
  driverSpecDefaults:
    nodePlugin:
      kubeletDirPath: /var/lib/k0s/kubelet

もう一つの方法は k0sctl ホストの kubeletRootDir で kubelet 側を /var/lib/kubelet に変更することですが、計測はしていません(未計測)。ドライバーのデーモンセットは operator が Driver オブジェクトを参照して作成するため、チャートインストール直後(23秒)にはまだ存在していませんでした。インストール直後にデーモンセットを確認する自動化処理では、作成されるまで待機する必要があります。

6.2 実測

チャートセット(rook-ceph、ceph-csi-drivers、rook-ceph-cluster v1.21.1、Ceph v20.2.4)を helm で持ち込んだ tgz からインストールしました。ノードごとに空きディスク 17GiB を 1つ 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)、PG 121個 active+clean

HEALTH_OK は Ceph v20 が出力する cephx キー形式の警告群(AUTH_INSECURE_*)の原因を確認し、ミュート処理した後の値です。最後まですべてのステージを実行したあとには、 BLUESTORE_SLOW_OP_ALERT(遅いディスクの警告)によって HEALTH_WARN になりました。HDD 上の VM に 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 が必要です(プラグインインストールドキュメント)。バックアップツールがプラグインのサイドカーに含まれているため、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}

インスタンスセットがすべて Ready になるまでに第 2回目では 約 13分 40秒がかかりました(initdb 70秒、残りはレプリカのイメージ取得と合流)。HDD 上の VM という条件が大きく影響している値のため、そのまま一般化することはできませんが、自動化の待機上限を設定する際は「数分」ではなく「十数分」を見込む必要があるということは明確です。

7.2 罠:バケット名が生成される前に ObjectStore を作成した場合

最初の試行では ObjectBucketClaim が 2分以内に Bound にならなかったものの、スクリプトは待機終了後にそのまま次へ進んでしまいました。そのため、バケット名が空のまま ObjectStore を作成し、 destinationPath が s3:/// になりました。WAL のアーカイビングが Invalid s3 URL address でエラーし続けました。

この状態で primary ポッドを削除したところ、事態が深刻化しました。新しい primary は 39.8秒で起動したものの、古い primary が保持しきれなかった WAL を抱えたまま 18分以上 Terminating の状態にとどまりました。CNPG ポッドの terminationGracePeriodSeconds が 1800秒であるため、Kubernetes がその時間だけ待機したためです。現在は ConfigMap の BUCKET_NAME が生成され、OBC が Bound になるまで待機し、最終的に空であれば停止するように修正しています。

7.3 primary を削除した場合

インスタンスセットがすべて Ready のときに primary ポッドを削除し、0.5秒ごとに新しい primary に select pg_is_in_recovery() を問い合わせ、 f(書き込み可能)が返されるまでを計測しました。 すべてが Ready のときのみ実行することが重要です。 途切れた正解データの後にレプリカが1つも起動していない状態で primary を削除し、クラスターを喪失した回数がありました。

回数 新しい primary への書き込みまで 行 古い primary
2 a(アーカイビングが破損した状態、7.2) 39.8秒 記録なし 18分以上 Terminating
2 b 49.5秒 1001行を維持 レプリカとして復帰
3 62.6秒 記録なし 1418秒(23分)以上 Terminating
4 35.4秒 1001行を維持 320秒以上 Terminating(5分までのみ計測)

新しい primary の昇格と書き込みに問題はありませんでした。問題は古い primary です。アーカイビングが正常であった 3回のうち 2回も 5分以上 Terminating であり、そのポッドのログは次のように終了していました。

"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>)として残り、ポッドは猶予期間(grace period)の 1800秒を使い切る挙動を示しました。原因は特定できていません。barman-cloud サイドカーを有効にすると primary ポッドの終了に約 3分かかるという問題のissue plugin-barman-cloud#1039(オープン)と同系統に見えますが、今回の観測はそれよりもはるかに長時間に及びます(推測)。

なぜ気にする必要があるのか。古い primary が Terminating の間、クラスターは 2つの Ready インスタンスで耐え忍びます。その間にさらにノードが 1台停止すると、レプリカが 1つだけの状態になります。そのポッドが配置されているノードを drain しようとしても完了しません。フェイルオーバー後には、古い primary が実際にレプリカとして復帰したかどうかも確認する必要があります。この現象を軽減する設定(grace period の調整など)は検証していません(未計測)。

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}

バックアップが完了したら行を1行(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;

このように修正した後、3回とも1002行目まで復元されました。復元が完了するとインスタンスが一度再起動する点も記憶しておく価値があります(「Primary instance is being restarted without a switchover」)。Readyを一度見ただけで終了すると、すぐ後ろのReadyが空になっている瞬間に出くわします。「healthy」を10秒間隔で2回確認して終了としました。

8. 時間のかかるステップを実行する方法

apply、Ceph、CNPG、障害テスト、バックアップと復元には数分から十数分かかります。ターミナルに張りついて待つことのないよう、 systemd-run --property Type=oneshot でバックグラウンド実行し、完了したかどうかを個別に問い合わせました。ここで1つの落とし穴と2つの小さな驚きに直面しました。

9. リソース

すべてをデプロイした後の3台のノード(8 vCPU・8GiB)の状況です。

項目 値
メモリ使用量 2.2〜2.6 / 7.9GiB
/var/lib/k0s 6.4〜6.9GiB(作業ディスク)
ルートディスク(2.4GiB) 74~94%

/var/lib/k0s とポッドログは作業ディスクに移動させましたが、journaldとk0sのログはルートに蓄積されました。ルートディスクが小さいマシンの場合、journaldの上限をあらかじめ設定する必要があります。上限を設定して長期間稼働させたテストは行っていません(未計測)。

10. 制限事項と未計測項目

11. まとめ

チェック項目 理由(本記事の実測値)
3台構成なら3台すべてコントロールプレーン兼ワーカー etcdを3メンバーにして1台の障害に耐え、CephおよびPostgreSQLを各ノードに1つずつ配置
VRRPの interface と sans を記述する 空欄にするとデフォルトの経路NICに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:///、アーカイビングが破損し、旧プライマリが18分以上残存した
切り替え前にWAL名を記録する PostgreSQL 17以降、 pg_walfile_name(pg_switch_wal()) は次のファイルを指す
フェイルオーバー後に旧プライマリの状態まで確認 4回中3回で5分以上Terminating状態が継続
oneshotユニットは ActiveState で判定 is-active は実行中である状態を「いいえ」と返す

本記事の実測は、TT Labの実習コース「閉域網k0s:1・2・3台のノードとその上のCeph・PostgreSQL」(サイトパス /course/k0s-airgap-multinode)の第3モジュール「3ノードHA」にて、同一のバージョンおよび同一のマニフェスト一覧を用いて実際に追体験していただけます。VIPの移動、Rook-Ceph、CNPGのフェイルオーバーとバックアップ・復元を実際のVM上で実行し、自動採点システムがステップごとに結果を検証します。

ティティの一言。 学級委員を3人選んでおけば、1人が体調を崩しても2人で話し合って決められます。でも2人とも体調を崩してしまうと、1人では何も決められません。それに、お引越しした委員長(旧プライマリ)が荷物をまとめきれずにドアのところで30分立ち尽くしているかもしれないので、新しい委員長が決まったからといってそこで終わりにせず、古い委員長がちゃんと自分の席に戻ったかどうかもぜひ確認してあげてください!

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます