- はじめに — Postgresをオペレーターに任せるということ
- 第1部 — インストール: マニフェスト一つ
- 第2部 — 3インスタンスのクラスターを作る
- 第3部 — 複製の確認
- 第4部 — プライマリを殺す: フェイルオーバー23.1秒
- 第5部 — 正直な数字と落とし穴
- おわりに
- 参考資料
はじめに — Postgresをオペレーターに任せるということ
Kubernetesでステートフル(stateful)なデータベースを動かすことは、長らくタブーのように見なされてきました。CloudNativePG(CNPG)は、そのタブーを正面から打ち破るオペレーターです — Postgresの高可用性・フェイルオーバー・バックアップ・ローリングアップグレードをCRD一つで宣言すれば、オペレーターがプライマリ選出とレプリカ管理を代わりに行ってくれます。この記事は紹介ではなく、実測です。本物の8ノードクラスターにCNPGを入れ、3インスタンスのPostgresを立ち上げ、本当にプライマリを殺し、フェイルオーバーが何秒かかるのかを測ってみました。オペレーターを自分で書いてみたRust GPUオペレーター編と対になる、「よくできたオペレーターを使う」側の話です。
第1部 — インストール: マニフェスト一つ
CNPGのインストールはシンプルです。リリースマニフェストを一つapplyすれば完了です。
kubectl apply --server-side -f \
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.30/releases/cnpg-1.30.0.yaml
この一行が、複数のCRD(clusters・poolers・scheduledbackups・publications・subscriptionsなど)、cnpg-systemネームスペースのコントローラーのDeployment、RBAC、Webhookをすべてインストールします。コントローラーが起動するまで20秒ほどかかりました。
$ kubectl -n cnpg-system rollout status deploy/cnpg-controller-manager
deployment "cnpg-controller-manager" successfully rolled out
NAME READY UP-TO-DATE AVAILABLE AGE
cnpg-controller-manager 1/1 1 1 16s
第2部 — 3インスタンスのクラスターを作る
PostgresクラスターはCluster CRで宣言します。プライマリ1 + レプリカ2、つまり3インスタンスにしました。
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg-test
namespace: cnpg-test
spec:
instances: 3
storage:
size: 1Gi
storageClass: nfs-client # ホームラボのNFS — 落とし穴は第5部で
bootstrap:
initdb:
database: appdb
owner: appuser
applyすると、オペレーターがブートストラップを開始します。状態遷移をリアルタイムで見守りました。
Setting up primary
→ Waiting for the instances to become active
→ ready=1 Creating a new replica ← プライマリ起動後にレプリカの複製を開始
→ ready=2 Creating a new replica
→ ready=3 Cluster in healthy state ← 約2分で3/3が正常
3つのインスタンスは、それぞれ異なるノード(cubi02・cubi03・cubi04)へ自動的に分散されました — オペレーターがanti-affinityで一つのノードに偏らないようにします。CNPGは接続用のサービスも3つ作ってくれます。
サービス 役割
──────────── ─────────────────────────────
pg-test-rw 読み書き → 常に現在のプライマリへルーティング
pg-test-ro 読み取り専用 → レプリカへロードバランシング
pg-test-r 任意のインスタンス(読み取り)
-rwサービスが肝心です — アプリケーションはこの名前一つだけを見ていれば、フェイルオーバーでプライマリが変わっても自動的に新しいプライマリへ接続されます。
第3部 — 複製の確認
プライマリに1000行を入れ、3つのインスタンスがすべて同じかどうかを見ました。
$ プライマリ(pg-test-1)にINSERT 1000行
INSERT 0 1000
$ 各インスタンスの行数
pg-test-1 (primary): 1000 rows
pg-test-2 (replica): 1000 rows ← 複製された
pg-test-3 (replica): 1000 rows ← 複製された
ストリーミングレプリケーションが即座に3つのノードを同期しました。いよいよ本当の実験です。
第4部 — プライマリを殺す: フェイルオーバー23.1秒
最も気になっていたこと — プライマリが突然消えたら、何秒で復旧するのか? --grace-period=0 --forceでプライマリのPodを即死させ、新しいプライマリが現れるまで0.5秒間隔でポーリングしながら時間を計りました。
=== FAILOVER TEST: killing primary pg-test-1 ===
deleted at t0; polling for new primary...
=== NEW PRIMARY: pg-test-2 (was pg-test-1) ===
failover time: 23.1 s
rows after failover: 1000 ← データ無損失
23.1秒でpg-test-2が新しいプライマリに昇格し、1000行がそのまま生きていました。そして死んでいたpg-test-1は捨てられません — オペレーターが自動的に連れ戻し、レプリカとして再編入させます。
=== 自己回復後の最終的な役割 ===
pg-test-1: replica ← 死んで蘇りレプリカへ降格して復帰
pg-test-2: primary ← 昇格した新しいプライマリ
pg-test-3: replica
$ 新しいプライマリへ追加書き込み → 正常、合計1500行
新しいプライマリはすぐに書き込みを受け付け(1000→1500行)、クラスターは再び3/3 healthyに戻りました。**人の介入は0回。**これがオペレーターの価値です。
第5部 — 正直な数字と落とし穴
ブログは検証したことだけを書くという原則どおり、この実験の限界もそのまま残します。
- 23秒は速いのか? 状況によって異なります。CNPGのフェイルオーバー時間は、プライマリの死の検知(ヘルスチェック周期)、レプリカの昇格、そして
-rwサービスのエンドポイント更新の合計です。本番環境ではPodの削除ではなくノード障害のほうが一般的で、その場合はノード検知時間(node-monitor-grace-periodなど)が加わってさらに長くなることがあります。逆にチューニングすればさらに短くなります。「23秒」はこのホームラボ・この設定の実測値であって、普遍的な定数ではありません。 - NFSストレージの落とし穴。 私は
nfs-client(NFS provisioner)ストレージを使いましたが、PostgresをNFSの上に載せるのは本番環境では推奨されません — fsyncの保証・ファイルロックの問題のためです。ホームラボのテストではうまく動きましたが、実サービスならローカルSSDやブロックストレージ(Ceph RBDなど)を使うべきです。 - 同期 vs 非同期レプリケーション。 このテストはデフォルト(非同期)レプリケーションでした。非同期では理論上、プライマリの死の直前にごく少量の未複製トランザクションが失われる可能性があります。無損失が必要なら、CNPGの同期レプリケーション(
minSyncReplicas)をオンにする必要があり、その代償として書き込み遅延が増えます。
おわりに
CNPGは「KubernetesではDBは危険だ」という通念を実測で反論します — マニフェスト一つで3ノードのHA Postgresが立ち上がり、プライマリを即死させても23秒で自ら復旧し、死んだノードはレプリカへ戻ります。もちろんストレージ・レプリケーションモード・フェイルオーバーのチューニングという本当の宿題は残りますが、それは「DBをオペレーターに任せられるか」の問題ではなく「どうやってうまく任せるか」の問題です。次回はバックアップ(ScheduledBackup)とポイントインタイムリカバリ(PITR)を同じやり方で殺してみて検証するつもりです。
参考資料
현재 단락 (1/68)
Kubernetesでステートフル(stateful)なデータベースを動かすことは、長らくタブーのように見なされてきました。[CloudNativePG(CNPG)](https://cloudnat...